-
Notifications
You must be signed in to change notification settings - Fork 10.3k
[Blazor] SupplyParameterFromForm complex type support. #48567
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
12 changes: 12 additions & 0 deletions
12
src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints.Binding; | ||
|
||
internal class CompiledComplexTypeConverter<T>(CompiledComplexTypeConverter<T>.ConverterDelegate body) : FormDataConverter<T> | ||
{ | ||
public delegate bool ConverterDelegate(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found); | ||
|
||
internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found) => | ||
body(ref context, type, options, out result, out found); | ||
} |
9 changes: 9 additions & 0 deletions
9
...ents/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints.Binding; | ||
|
||
internal abstract class ComplexTypeExpressionConverterFactory | ||
{ | ||
internal abstract FormDataConverter CreateConverter(Type type, FormDataMapperOptions options); | ||
} |
160 changes: 160 additions & 0 deletions
160
...s/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Linq.Expressions; | ||
using Microsoft.Extensions.Internal; | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints.Binding; | ||
|
||
internal sealed class ComplexTypeExpressionConverterFactory<T> : ComplexTypeExpressionConverterFactory | ||
{ | ||
internal override CompiledComplexTypeConverter<T> CreateConverter(Type type, FormDataMapperOptions options) | ||
{ | ||
var body = CreateConverterBody(type, options); | ||
return new CompiledComplexTypeConverter<T>(body); | ||
} | ||
|
||
private CompiledComplexTypeConverter<T>.ConverterDelegate CreateConverterBody(Type type, FormDataMapperOptions options) | ||
{ | ||
var properties = PropertyHelper.GetVisibleProperties(type); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the same logic MVC uses to resolve bindable properties. |
||
|
||
var (readerParam, typeParam, optionsParam, resultParam, foundValueParam) = CreateFormDataConverterParameters(); | ||
var parameters = new List<ParameterExpression>() { readerParam, typeParam, optionsParam, resultParam, foundValueParam }; | ||
|
||
// Variables | ||
var propertyFoundValue = Expression.Variable(typeof(bool), "foundValueForProperty"); | ||
var succeeded = Expression.Variable(typeof(bool), "succeeded"); | ||
var localFoundValueVar = Expression.Variable(typeof(bool), "localFoundValue"); | ||
|
||
var variables = new List<ParameterExpression>() { propertyFoundValue, succeeded, localFoundValueVar }; | ||
var propertyLocals = new List<ParameterExpression>(); | ||
|
||
var body = new List<Expression>() | ||
{ | ||
Expression.Assign(succeeded, Expression.Constant(true)), | ||
}; | ||
|
||
// Create the property blocks | ||
|
||
// var propertyConverter = options.ResolveConverter(typeof(string)); | ||
// reader.PushPrefix("Property"); | ||
// succeeded &= propertyConverter.TryRead(ref reader, typeof(string), options, out propertyVar, out foundProperty); | ||
// found ||= foundProperty; | ||
// reader.PopPrefix("Property"); | ||
for (var i = 0; i < properties.Length; i++) | ||
{ | ||
// Declare variable for the converter | ||
var property = properties[i].Property; | ||
var propertyConverterType = typeof(FormDataConverter<>).MakeGenericType(property.PropertyType); | ||
var propertyConverterVar = Expression.Variable(propertyConverterType, $"{property.Name}Converter"); | ||
variables.Add(propertyConverterVar); | ||
|
||
// Declare variable for property value. | ||
var propertyVar = Expression.Variable(property.PropertyType, property.Name); | ||
propertyLocals.Add(propertyVar); | ||
|
||
// Resolve and assign converter | ||
|
||
// Create the block to try and map the property and update variables. | ||
// returnParam &= { PushPrefix(property.Name); var res = TryRead(...); PopPrefix(...); return res; } | ||
// var propertyConverter = options.ResolveConverter<TProperty>()); | ||
var propertyConverter = Expression.Assign( | ||
propertyConverterVar, | ||
Expression.Call( | ||
optionsParam, | ||
nameof(FormDataMapperOptions.ResolveConverter), | ||
new[] { property.PropertyType }, | ||
Array.Empty<Expression>())); | ||
body.Add(propertyConverter); | ||
|
||
// reader.PushPrefix("Property"); | ||
body.Add(Expression.Call( | ||
readerParam, | ||
nameof(FormDataReader.PushPrefix), | ||
Array.Empty<Type>(), | ||
Expression.Constant(property.Name))); | ||
|
||
// succeeded &= propertyConverter.TryRead(ref reader, typeof(string), options, out propertyVar, out foundProperty); | ||
var callTryRead = Expression.AndAssign( | ||
succeeded, | ||
Expression.Call( | ||
propertyConverterVar, | ||
nameof(FormDataConverter<T>.TryRead), | ||
Type.EmptyTypes, | ||
readerParam, | ||
typeParam, | ||
optionsParam, | ||
propertyVar, | ||
propertyFoundValue)); | ||
body.Add(callTryRead); | ||
|
||
javiercn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// reader.PopPrefix("Property"); | ||
body.Add(Expression.Call( | ||
readerParam, | ||
nameof(FormDataReader.PopPrefix), | ||
Array.Empty<Type>(), | ||
Expression.Constant(property.Name))); | ||
|
||
body.Add(Expression.OrAssign(localFoundValueVar, propertyFoundValue)); | ||
} | ||
|
||
body.Add(Expression.IfThen( | ||
localFoundValueVar, | ||
Expression.Block(CreateInstanceAndAssignProperties(type, resultParam, properties, propertyLocals)))); | ||
|
||
// foundValue && !failures; | ||
|
||
body.Add(Expression.Assign(foundValueParam, localFoundValueVar)); | ||
body.Add(succeeded); | ||
|
||
variables.AddRange(propertyLocals); | ||
|
||
return CreateConverterFunction(parameters, variables, body); | ||
|
||
static IEnumerable<Expression> CreateInstanceAndAssignProperties( | ||
Type model, | ||
ParameterExpression resultParam, | ||
PropertyHelper[] props, | ||
List<ParameterExpression> variables) | ||
{ | ||
if (!model.IsValueType) | ||
{ | ||
yield return Expression.Assign(resultParam, Expression.New(model)); | ||
} | ||
|
||
for (var i = 0; i < props.Length; i++) | ||
{ | ||
yield return Expression.Assign(Expression.Property(resultParam, props[i].Property), variables[i]); | ||
} | ||
} | ||
} | ||
|
||
private static CompiledComplexTypeConverter<T>.ConverterDelegate CreateConverterFunction( | ||
List<ParameterExpression> parameters, | ||
List<ParameterExpression> variables, | ||
List<Expression> body) | ||
{ | ||
var lambda = Expression.Lambda<CompiledComplexTypeConverter<T>.ConverterDelegate>( | ||
Expression.Block(variables, body), | ||
parameters); | ||
|
||
return lambda.Compile(); | ||
} | ||
|
||
private static FormDataConverterReadParameters CreateFormDataConverterParameters() | ||
{ | ||
return new( | ||
Expression.Parameter(typeof(FormDataReader).MakeByRefType(), "reader"), | ||
Expression.Parameter(typeof(Type), "type"), | ||
Expression.Parameter(typeof(FormDataMapperOptions), "options"), | ||
Expression.Parameter(typeof(T).MakeByRefType(), "result"), | ||
Expression.Parameter(typeof(bool).MakeByRefType(), "foundValue")); | ||
} | ||
|
||
private record struct FormDataConverterReadParameters( | ||
ParameterExpression ReaderParam, | ||
ParameterExpression TypeParam, | ||
ParameterExpression OptionsParam, | ||
ParameterExpression ResultParam, | ||
ParameterExpression FoundValueParam); | ||
} |
119 changes: 119 additions & 0 deletions
119
src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.Extensions.Internal; | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints.Binding; | ||
|
||
// This factory is registered last, which means, dictionaries and collections, have already | ||
// been processed by the time we get here. | ||
internal class ComplexTypeConverterFactory : IFormDataConverterFactory | ||
{ | ||
internal static readonly ComplexTypeConverterFactory Instance = new(); | ||
|
||
public bool CanConvert(Type type, FormDataMapperOptions options) | ||
{ | ||
if (type.GetConstructor(Type.EmptyTypes) == null && !type.IsValueType) | ||
{ | ||
// For right now, require a public parameterless constructor. | ||
return false; | ||
} | ||
if (type.IsGenericTypeDefinition) | ||
{ | ||
return false; | ||
} | ||
|
||
// Check that all properties have a valid converter. | ||
var propertyHelper = PropertyHelper.GetVisibleProperties(type); | ||
foreach (var helper in propertyHelper) | ||
{ | ||
if (options.ResolveConverter(helper.Property.PropertyType) == null) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
// We are going to compile a function that maps all the properties for the type. | ||
// Beware that the code below is not the actual exact code, just a simplification to understand what is happening at a high level. | ||
// The general flow is as follows. For a type like Address { Street, City, Country, ZipCode } | ||
// we will generate a function that looks like: | ||
// public bool TryRead(ref FormDataReader reader, Type type, FormDataSerializerOptions options, out Address? result, out bool found) | ||
// { | ||
// bool foundProperty; | ||
// bool succeeded = true; | ||
// string street; | ||
// string city; | ||
// string country; | ||
// string zipCode; | ||
// FormDataConveter<string> streetConverter; | ||
// FormDataConveter<string> cityConverter; | ||
// FormDataConveter<string> countryConverter; | ||
// FormDataConveter<string> zipCodeConverter; | ||
|
||
// var streetConverter = options.ResolveConverter(typeof(string)); | ||
// reader.PushPrefix("Street"); | ||
// succeeded &= streetConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty); | ||
// found ||= foundProperty; | ||
// reader.PopPrefix("Street"); | ||
// | ||
// var cityConverter = options.ResolveConverter(typeof(string)); | ||
// reader.PushPrefix("City"); | ||
// succeeded &= ciryConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty); | ||
// found ||= foundProperty; | ||
// reader.PopPrefix("City"); | ||
// | ||
// var countryConverter = options.ResolveConverter(typeof(string)); | ||
// reader.PushPrefix("Country"); | ||
// succeeded &= countryConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty); | ||
// found ||= foundProperty; | ||
// reader.PopPrefix("Country"); | ||
// | ||
// var zipCodeConverter = options.ResolveConverter(typeof(string)); | ||
// reader.PushPrefix("ZipCode"); | ||
// succeeded &= zipCodeConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty); | ||
// found ||= foundProperty; | ||
// reader.PopPrefix("ZipCode"); | ||
// | ||
// if(found) | ||
// { | ||
// result = new Address(); | ||
// result.Street = street; | ||
// result.City = city; | ||
// result.Country = country; | ||
// result.ZipCode = zipCode; | ||
// } | ||
// else | ||
// { | ||
// result = null; | ||
// } | ||
// | ||
// return succeeded; | ||
// } | ||
// | ||
// The actual blocks above are going to be generated using System.Linq.Expressions. | ||
// Instead of resolving the property converters every time, we might consider caching the converters in a dictionary and passing an | ||
// extra parameter to the function with them in it. | ||
// The final converter is something like | ||
// internal class CompiledComplexTypeConverter | ||
// (ConverterDelegate<FormDataReader, Type, FormDataSerializerOptions, out object, out bool> converterFunc) | ||
// { | ||
// public bool TryRead(ref FormDataReader reader, Type type, FormDataSerializerOptions options, out object? result, out bool found) | ||
// { | ||
// return converterFunc(ref reader, type, options, out result, out found); | ||
// } | ||
// } | ||
|
||
public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) | ||
{ | ||
if (Activator.CreateInstance(typeof(ComplexTypeExpressionConverterFactory<>).MakeGenericType(type)) | ||
is not ComplexTypeExpressionConverterFactory factory) | ||
{ | ||
throw new InvalidOperationException($"Could not create a converter factory for type {type}."); | ||
} | ||
|
||
return factory.CreateConverter(type, options); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.