Skip to content

[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 4 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
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);
}
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);
Copy link
Member Author

Choose a reason for hiding this comment

The 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);

// 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);
}
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public FormDataMapperOptions()
_factories.Add((type, options) => ParsableConverterFactory.Instance.CanConvert(type, options) ? ParsableConverterFactory.Instance.CreateConverter(type, options) : null);
_factories.Add((type, options) => NullableConverterFactory.Instance.CanConvert(type, options) ? NullableConverterFactory.Instance.CreateConverter(type, options) : null);
_factories.Add((type, options) => CollectionConverterFactory.Instance.CanConvert(type, options) ? CollectionConverterFactory.Instance.CreateConverter(type, options) : null);
_factories.Add((type, options) => ComplexTypeConverterFactory.Instance.CanConvert(type, options) ? ComplexTypeConverterFactory.Instance.CreateConverter(type, options) : null);
}

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