Skip to content

Commit 8dea0b8

Browse files
authored
[Blazor] SupplyParameterFromForm complex type support. (#48567)
* Adds support for binding complex types
1 parent ed984e3 commit 8dea0b8

File tree

6 files changed

+409
-0
lines changed

6 files changed

+409
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
5+
6+
internal class CompiledComplexTypeConverter<T>(CompiledComplexTypeConverter<T>.ConverterDelegate body) : FormDataConverter<T>
7+
{
8+
public delegate bool ConverterDelegate(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found);
9+
10+
internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found) =>
11+
body(ref context, type, options, out result, out found);
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
5+
6+
internal abstract class ComplexTypeExpressionConverterFactory
7+
{
8+
internal abstract FormDataConverter CreateConverter(Type type, FormDataMapperOptions options);
9+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq.Expressions;
5+
using Microsoft.Extensions.Internal;
6+
7+
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
8+
9+
internal sealed class ComplexTypeExpressionConverterFactory<T> : ComplexTypeExpressionConverterFactory
10+
{
11+
internal override CompiledComplexTypeConverter<T> CreateConverter(Type type, FormDataMapperOptions options)
12+
{
13+
var body = CreateConverterBody(type, options);
14+
return new CompiledComplexTypeConverter<T>(body);
15+
}
16+
17+
private CompiledComplexTypeConverter<T>.ConverterDelegate CreateConverterBody(Type type, FormDataMapperOptions options)
18+
{
19+
var properties = PropertyHelper.GetVisibleProperties(type);
20+
21+
var (readerParam, typeParam, optionsParam, resultParam, foundValueParam) = CreateFormDataConverterParameters();
22+
var parameters = new List<ParameterExpression>() { readerParam, typeParam, optionsParam, resultParam, foundValueParam };
23+
24+
// Variables
25+
var propertyFoundValue = Expression.Variable(typeof(bool), "foundValueForProperty");
26+
var succeeded = Expression.Variable(typeof(bool), "succeeded");
27+
var localFoundValueVar = Expression.Variable(typeof(bool), "localFoundValue");
28+
29+
var variables = new List<ParameterExpression>() { propertyFoundValue, succeeded, localFoundValueVar };
30+
var propertyLocals = new List<ParameterExpression>();
31+
32+
var body = new List<Expression>()
33+
{
34+
Expression.Assign(succeeded, Expression.Constant(true)),
35+
};
36+
37+
// Create the property blocks
38+
39+
// var propertyConverter = options.ResolveConverter(typeof(string));
40+
// reader.PushPrefix("Property");
41+
// succeeded &= propertyConverter.TryRead(ref reader, typeof(string), options, out propertyVar, out foundProperty);
42+
// found ||= foundProperty;
43+
// reader.PopPrefix("Property");
44+
for (var i = 0; i < properties.Length; i++)
45+
{
46+
// Declare variable for the converter
47+
var property = properties[i].Property;
48+
var propertyConverterType = typeof(FormDataConverter<>).MakeGenericType(property.PropertyType);
49+
var propertyConverterVar = Expression.Variable(propertyConverterType, $"{property.Name}Converter");
50+
variables.Add(propertyConverterVar);
51+
52+
// Declare variable for property value.
53+
var propertyVar = Expression.Variable(property.PropertyType, property.Name);
54+
propertyLocals.Add(propertyVar);
55+
56+
// Resolve and assign converter
57+
58+
// Create the block to try and map the property and update variables.
59+
// returnParam &= { PushPrefix(property.Name); var res = TryRead(...); PopPrefix(...); return res; }
60+
// var propertyConverter = options.ResolveConverter<TProperty>());
61+
var propertyConverter = Expression.Assign(
62+
propertyConverterVar,
63+
Expression.Call(
64+
optionsParam,
65+
nameof(FormDataMapperOptions.ResolveConverter),
66+
new[] { property.PropertyType },
67+
Array.Empty<Expression>()));
68+
body.Add(propertyConverter);
69+
70+
// reader.PushPrefix("Property");
71+
body.Add(Expression.Call(
72+
readerParam,
73+
nameof(FormDataReader.PushPrefix),
74+
Array.Empty<Type>(),
75+
Expression.Constant(property.Name)));
76+
77+
// succeeded &= propertyConverter.TryRead(ref reader, typeof(string), options, out propertyVar, out foundProperty);
78+
var callTryRead = Expression.AndAssign(
79+
succeeded,
80+
Expression.Call(
81+
propertyConverterVar,
82+
nameof(FormDataConverter<T>.TryRead),
83+
Type.EmptyTypes,
84+
readerParam,
85+
typeParam,
86+
optionsParam,
87+
propertyVar,
88+
propertyFoundValue));
89+
body.Add(callTryRead);
90+
91+
// reader.PopPrefix("Property");
92+
body.Add(Expression.Call(
93+
readerParam,
94+
nameof(FormDataReader.PopPrefix),
95+
Array.Empty<Type>(),
96+
Expression.Constant(property.Name)));
97+
98+
body.Add(Expression.OrAssign(localFoundValueVar, propertyFoundValue));
99+
}
100+
101+
body.Add(Expression.IfThen(
102+
localFoundValueVar,
103+
Expression.Block(CreateInstanceAndAssignProperties(type, resultParam, properties, propertyLocals))));
104+
105+
// foundValue && !failures;
106+
107+
body.Add(Expression.Assign(foundValueParam, localFoundValueVar));
108+
body.Add(succeeded);
109+
110+
variables.AddRange(propertyLocals);
111+
112+
return CreateConverterFunction(parameters, variables, body);
113+
114+
static IEnumerable<Expression> CreateInstanceAndAssignProperties(
115+
Type model,
116+
ParameterExpression resultParam,
117+
PropertyHelper[] props,
118+
List<ParameterExpression> variables)
119+
{
120+
if (!model.IsValueType)
121+
{
122+
yield return Expression.Assign(resultParam, Expression.New(model));
123+
}
124+
125+
for (var i = 0; i < props.Length; i++)
126+
{
127+
yield return Expression.Assign(Expression.Property(resultParam, props[i].Property), variables[i]);
128+
}
129+
}
130+
}
131+
132+
private static CompiledComplexTypeConverter<T>.ConverterDelegate CreateConverterFunction(
133+
List<ParameterExpression> parameters,
134+
List<ParameterExpression> variables,
135+
List<Expression> body)
136+
{
137+
var lambda = Expression.Lambda<CompiledComplexTypeConverter<T>.ConverterDelegate>(
138+
Expression.Block(variables, body),
139+
parameters);
140+
141+
return lambda.Compile();
142+
}
143+
144+
private static FormDataConverterReadParameters CreateFormDataConverterParameters()
145+
{
146+
return new(
147+
Expression.Parameter(typeof(FormDataReader).MakeByRefType(), "reader"),
148+
Expression.Parameter(typeof(Type), "type"),
149+
Expression.Parameter(typeof(FormDataMapperOptions), "options"),
150+
Expression.Parameter(typeof(T).MakeByRefType(), "result"),
151+
Expression.Parameter(typeof(bool).MakeByRefType(), "foundValue"));
152+
}
153+
154+
private record struct FormDataConverterReadParameters(
155+
ParameterExpression ReaderParam,
156+
ParameterExpression TypeParam,
157+
ParameterExpression OptionsParam,
158+
ParameterExpression ResultParam,
159+
ParameterExpression FoundValueParam);
160+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.Internal;
5+
6+
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
7+
8+
// This factory is registered last, which means, dictionaries and collections, have already
9+
// been processed by the time we get here.
10+
internal class ComplexTypeConverterFactory : IFormDataConverterFactory
11+
{
12+
internal static readonly ComplexTypeConverterFactory Instance = new();
13+
14+
public bool CanConvert(Type type, FormDataMapperOptions options)
15+
{
16+
if (type.GetConstructor(Type.EmptyTypes) == null && !type.IsValueType)
17+
{
18+
// For right now, require a public parameterless constructor.
19+
return false;
20+
}
21+
if (type.IsGenericTypeDefinition)
22+
{
23+
return false;
24+
}
25+
26+
// Check that all properties have a valid converter.
27+
var propertyHelper = PropertyHelper.GetVisibleProperties(type);
28+
foreach (var helper in propertyHelper)
29+
{
30+
if (options.ResolveConverter(helper.Property.PropertyType) == null)
31+
{
32+
return false;
33+
}
34+
}
35+
36+
return true;
37+
}
38+
39+
// We are going to compile a function that maps all the properties for the type.
40+
// Beware that the code below is not the actual exact code, just a simplification to understand what is happening at a high level.
41+
// The general flow is as follows. For a type like Address { Street, City, Country, ZipCode }
42+
// we will generate a function that looks like:
43+
// public bool TryRead(ref FormDataReader reader, Type type, FormDataSerializerOptions options, out Address? result, out bool found)
44+
// {
45+
// bool foundProperty;
46+
// bool succeeded = true;
47+
// string street;
48+
// string city;
49+
// string country;
50+
// string zipCode;
51+
// FormDataConveter<string> streetConverter;
52+
// FormDataConveter<string> cityConverter;
53+
// FormDataConveter<string> countryConverter;
54+
// FormDataConveter<string> zipCodeConverter;
55+
56+
// var streetConverter = options.ResolveConverter(typeof(string));
57+
// reader.PushPrefix("Street");
58+
// succeeded &= streetConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty);
59+
// found ||= foundProperty;
60+
// reader.PopPrefix("Street");
61+
//
62+
// var cityConverter = options.ResolveConverter(typeof(string));
63+
// reader.PushPrefix("City");
64+
// succeeded &= ciryConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty);
65+
// found ||= foundProperty;
66+
// reader.PopPrefix("City");
67+
//
68+
// var countryConverter = options.ResolveConverter(typeof(string));
69+
// reader.PushPrefix("Country");
70+
// succeeded &= countryConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty);
71+
// found ||= foundProperty;
72+
// reader.PopPrefix("Country");
73+
//
74+
// var zipCodeConverter = options.ResolveConverter(typeof(string));
75+
// reader.PushPrefix("ZipCode");
76+
// succeeded &= zipCodeConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty);
77+
// found ||= foundProperty;
78+
// reader.PopPrefix("ZipCode");
79+
//
80+
// if(found)
81+
// {
82+
// result = new Address();
83+
// result.Street = street;
84+
// result.City = city;
85+
// result.Country = country;
86+
// result.ZipCode = zipCode;
87+
// }
88+
// else
89+
// {
90+
// result = null;
91+
// }
92+
//
93+
// return succeeded;
94+
// }
95+
//
96+
// The actual blocks above are going to be generated using System.Linq.Expressions.
97+
// Instead of resolving the property converters every time, we might consider caching the converters in a dictionary and passing an
98+
// extra parameter to the function with them in it.
99+
// The final converter is something like
100+
// internal class CompiledComplexTypeConverter
101+
// (ConverterDelegate<FormDataReader, Type, FormDataSerializerOptions, out object, out bool> converterFunc)
102+
// {
103+
// public bool TryRead(ref FormDataReader reader, Type type, FormDataSerializerOptions options, out object? result, out bool found)
104+
// {
105+
// return converterFunc(ref reader, type, options, out result, out found);
106+
// }
107+
// }
108+
109+
public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
110+
{
111+
if (Activator.CreateInstance(typeof(ComplexTypeExpressionConverterFactory<>).MakeGenericType(type))
112+
is not ComplexTypeExpressionConverterFactory factory)
113+
{
114+
throw new InvalidOperationException($"Could not create a converter factory for type {type}.");
115+
}
116+
117+
return factory.CreateConverter(type, options);
118+
}
119+
}

src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public FormDataMapperOptions()
1818
_factories.Add((type, options) => NullableConverterFactory.Instance.CanConvert(type, options) ? NullableConverterFactory.Instance.CreateConverter(type, options) : null);
1919
_factories.Add((type, options) => DictionaryConverterFactory.Instance.CanConvert(type, options) ? DictionaryConverterFactory.Instance.CreateConverter(type, options) : null);
2020
_factories.Add((type, options) => CollectionConverterFactory.Instance.CanConvert(type, options) ? CollectionConverterFactory.Instance.CreateConverter(type, options) : null);
21+
_factories.Add((type, options) => ComplexTypeConverterFactory.Instance.CanConvert(type, options) ? ComplexTypeConverterFactory.Instance.CreateConverter(type, options) : null);
2122
}
2223

2324
// Not configurable for now, this is the max number of elements we will bind. This is important for

0 commit comments

Comments
 (0)