From 322fa1ca9455b034dc188834563b100aee87d5e8 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 30 May 2023 18:30:50 +0200 Subject: [PATCH 1/4] Basic object mapping support --- .../CompiledComplextTypeConverter.cs | 14 ++ .../ComplexTypeExpressionConverterFactory.cs | 9 + ...omplexTypeExpressionConverterFactoryOfT.cs | 154 ++++++++++++++++++ .../Factories/ComplexTypeConverterFactory.cs | 119 ++++++++++++++ .../src/Binding/FormDataMapperOptions.cs | 1 + .../test/Binding/FormDataMapperTests.cs | 68 ++++++++ 6 files changed, 365 insertions(+) create mode 100644 src/Components/Endpoints/src/Binding/Converters/CompiledComplextTypeConverter.cs create mode 100644 src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs create mode 100644 src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs create mode 100644 src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs diff --git a/src/Components/Endpoints/src/Binding/Converters/CompiledComplextTypeConverter.cs b/src/Components/Endpoints/src/Binding/Converters/CompiledComplextTypeConverter.cs new file mode 100644 index 000000000000..2451dfd36e9b --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CompiledComplextTypeConverter.cs @@ -0,0 +1,14 @@ +// 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 CompiledComplextTypeConverter(CompiledComplextTypeConverter.ConverterDelegate body) : FormDataConverter +{ + public delegate bool ConverterDelegate(ref FormDataReader reader, Type type, FormDataSerializerOptions options, out T? result, out bool found); + + internal override bool TryRead(ref FormDataReader context, Type type, FormDataSerializerOptions options, out T? result, out bool found) + { + return body(ref context, type, options, out result, out found); + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs new file mode 100644 index 000000000000..56a8f2e1a279 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs @@ -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, FormDataSerializerOptions options); +} diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs new file mode 100644 index 000000000000..5d340f4f458f --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs @@ -0,0 +1,154 @@ +// 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 class ComplexTypeExpressionConverterFactory : ComplexTypeExpressionConverterFactory +{ + internal override CompiledComplextTypeConverter CreateConverter(Type type, FormDataSerializerOptions options) + { + var body = CreateConverterBody(type, options); + return new CompiledComplextTypeConverter(body); + } + + private CompiledComplextTypeConverter.ConverterDelegate CreateConverterBody(Type type, FormDataSerializerOptions options) + { + var properties = PropertyHelper.GetVisibleProperties(type); + + var (readerParam, typeParam, optionsParam, resultParam, foundValueParam) = CreateFormDataConverterParameters(); + var parameters = new List() { 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() { propertyFoundValue, succeeded, localFoundValueVar }; + var propertyLocals = new List(); + + var body = new List() + { + 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()); + var propertyConverter = Expression.Assign( + propertyConverterVar, + Expression.Call( + optionsParam, + nameof(FormDataSerializerOptions.ResolveConverter), + new[] { property.PropertyType }, + Array.Empty())); + body.Add(propertyConverter); + + // reader.PushPrefix("Property"); + body.Add(Expression.Call( + readerParam, + nameof(FormDataReader.PushPrefix), + Array.Empty(), + 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.TryRead), + Type.EmptyTypes, + readerParam, + typeParam, + optionsParam, + propertyVar, + propertyFoundValue)); + body.Add(callTryRead); + + 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 CreateInstanceAndAssignProperties( + Type model, + ParameterExpression resultParam, + PropertyHelper[] props, + List 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 CompiledComplextTypeConverter.ConverterDelegate CreateConverterFunction( + List parameters, + List variables, + List body) + { + var lambda = Expression.Lambda.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(FormDataSerializerOptions), "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); + +} diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs new file mode 100644 index 000000000000..b5ac980c1ed5 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs @@ -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, FormDataSerializerOptions 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 streetConverter; + // FormDataConveter cityConverter; + // FormDataConveter countryConverter; + // FormDataConveter 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 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, FormDataSerializerOptions 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); + } +} diff --git a/src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs b/src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs index 48ef460c6c6a..4947f4ffaa24 100644 --- a/src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs +++ b/src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs @@ -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 diff --git a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs index 96e181ba603a..d5e17ccdd66a 100644 --- a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs +++ b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs @@ -523,6 +523,58 @@ private void CanDeserialize_Collection(T } } + [Fact] + public void CanDeserialize_ComplexValueType_Address() + { + // Arrange + var expected = new Address() { City = "Redmond", Street = "1 Microsoft Way", Country = "United States", ZipCode = 98052 }; + var data = new Dictionary() + { + ["City"] = "Redmond", + ["Country"] = "United States", + ["Street"] = "1 Microsoft Way", + ["ZipCode"] = "98052", + }; + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + var options = new FormDataSerializerOptions(); + + // Act + var result = FormDataDeserializer.Deserialize
(reader, options); + + // Assert + Assert.Equal(expected.City, result.City); + Assert.Equal(expected.Street, result.Street); + Assert.Equal(expected.Country, result.Country); + Assert.Equal(expected.ZipCode, result.ZipCode); + } + + [Fact] + public void CanDeserialize_ComplexReferenceType_Customer() + { + // Arrange + var expected = new Customer() { Age = 20, Name = "John Doe", Email = "john.doe@example.com", IsPreferred = true }; + var data = new Dictionary() + { + ["Age"] = "20", + ["Name"] = "John Doe", + ["Email"] = "john.doe@example.com", + ["IsPreferred"] = "true", + }; + + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + var options = new FormDataSerializerOptions(); + + // Act + var result = FormDataDeserializer.Deserialize(reader, options); + + // Assert + Assert.NotNull(result); + Assert.Equal(expected.Age, result.Age); + Assert.Equal(expected.Name, result.Name); + Assert.Equal(expected.Email, result.Email); + Assert.Equal(expected.IsPreferred, result.IsPreferred); + } + public static TheoryData NullableBasicTypes { get @@ -805,6 +857,22 @@ public static void Return(int[] array) } } +internal struct Address +{ + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } + public int ZipCode { get; set; } +} + +internal class Customer +{ + public string Name { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public bool IsPreferred { get; set; } +} + // Implements ICollection delegating to List _inner; internal class CustomCollection : ICollection { From c403efe7e9aac96e9533cc8a5b013e264b364f40 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 30 May 2023 22:36:04 +0200 Subject: [PATCH 2/4] Name cleanup --- ...ypeConverter.cs => CompiledComplexTypeConverter.cs} | 2 +- .../ComplexTypeExpressionConverterFactoryOfT.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/Components/Endpoints/src/Binding/Converters/{CompiledComplextTypeConverter.cs => CompiledComplexTypeConverter.cs} (81%) diff --git a/src/Components/Endpoints/src/Binding/Converters/CompiledComplextTypeConverter.cs b/src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs similarity index 81% rename from src/Components/Endpoints/src/Binding/Converters/CompiledComplextTypeConverter.cs rename to src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs index 2451dfd36e9b..2d4540d078e4 100644 --- a/src/Components/Endpoints/src/Binding/Converters/CompiledComplextTypeConverter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; -internal class CompiledComplextTypeConverter(CompiledComplextTypeConverter.ConverterDelegate body) : FormDataConverter +internal class CompiledComplexTypeConverter(CompiledComplexTypeConverter.ConverterDelegate body) : FormDataConverter { public delegate bool ConverterDelegate(ref FormDataReader reader, Type type, FormDataSerializerOptions options, out T? result, out bool found); diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs index 5d340f4f458f..72090c4f6e61 100644 --- a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs @@ -8,13 +8,13 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; internal class ComplexTypeExpressionConverterFactory : ComplexTypeExpressionConverterFactory { - internal override CompiledComplextTypeConverter CreateConverter(Type type, FormDataSerializerOptions options) + internal override CompiledComplexTypeConverter CreateConverter(Type type, FormDataSerializerOptions options) { var body = CreateConverterBody(type, options); - return new CompiledComplextTypeConverter(body); + return new CompiledComplexTypeConverter(body); } - private CompiledComplextTypeConverter.ConverterDelegate CreateConverterBody(Type type, FormDataSerializerOptions options) + private CompiledComplexTypeConverter.ConverterDelegate CreateConverterBody(Type type, FormDataSerializerOptions options) { var properties = PropertyHelper.GetVisibleProperties(type); @@ -122,12 +122,12 @@ static IEnumerable CreateInstanceAndAssignProperties( } } - private CompiledComplextTypeConverter.ConverterDelegate CreateConverterFunction( + private CompiledComplexTypeConverter.ConverterDelegate CreateConverterFunction( List parameters, List variables, List body) { - var lambda = Expression.Lambda.ConverterDelegate>( + var lambda = Expression.Lambda.ConverterDelegate>( Expression.Block(variables, body), parameters); From b930de6cac71ab3fc4ae77d4c56a2180f05fae26 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 1 Jun 2023 16:37:11 +0200 Subject: [PATCH 3/4] Cleanups and fixes --- .../CompiledComplexTypeConverter.cs | 8 ++-- .../ComplexTypeExpressionConverterFactory.cs | 4 +- ...omplexTypeExpressionConverterFactoryOfT.cs | 13 +++-- .../Factories/ComplexTypeConverterFactory.cs | 4 +- .../test/Binding/FormDataMapperTests.cs | 48 +++++++++++++++++-- 5 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs b/src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs index 2d4540d078e4..e78c99d18f09 100644 --- a/src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs @@ -5,10 +5,8 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; internal class CompiledComplexTypeConverter(CompiledComplexTypeConverter.ConverterDelegate body) : FormDataConverter { - public delegate bool ConverterDelegate(ref FormDataReader reader, Type type, FormDataSerializerOptions options, out T? result, out bool found); + 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, FormDataSerializerOptions options, out T? result, out bool found) - { - return body(ref context, type, options, out result, out 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); } diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs index 56a8f2e1a279..68d22f323d09 100644 --- a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs @@ -1,9 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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, FormDataSerializerOptions options); + internal abstract FormDataConverter CreateConverter(Type type, FormDataMapperOptions options); } diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs index 72090c4f6e61..e6d425e7e571 100644 --- a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs @@ -6,15 +6,15 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; -internal class ComplexTypeExpressionConverterFactory : ComplexTypeExpressionConverterFactory +internal sealed class ComplexTypeExpressionConverterFactory : ComplexTypeExpressionConverterFactory { - internal override CompiledComplexTypeConverter CreateConverter(Type type, FormDataSerializerOptions options) + internal override CompiledComplexTypeConverter CreateConverter(Type type, FormDataMapperOptions options) { var body = CreateConverterBody(type, options); return new CompiledComplexTypeConverter(body); } - private CompiledComplexTypeConverter.ConverterDelegate CreateConverterBody(Type type, FormDataSerializerOptions options) + private CompiledComplexTypeConverter.ConverterDelegate CreateConverterBody(Type type, FormDataMapperOptions options) { var properties = PropertyHelper.GetVisibleProperties(type); @@ -62,7 +62,7 @@ private CompiledComplexTypeConverter.ConverterDelegate CreateConverterBody(Ty propertyConverterVar, Expression.Call( optionsParam, - nameof(FormDataSerializerOptions.ResolveConverter), + nameof(FormDataMapperOptions.ResolveConverter), new[] { property.PropertyType }, Array.Empty())); body.Add(propertyConverter); @@ -122,7 +122,7 @@ static IEnumerable CreateInstanceAndAssignProperties( } } - private CompiledComplexTypeConverter.ConverterDelegate CreateConverterFunction( + private static CompiledComplexTypeConverter.ConverterDelegate CreateConverterFunction( List parameters, List variables, List body) @@ -139,7 +139,7 @@ private static FormDataConverterReadParameters CreateFormDataConverterParameters return new( Expression.Parameter(typeof(FormDataReader).MakeByRefType(), "reader"), Expression.Parameter(typeof(Type), "type"), - Expression.Parameter(typeof(FormDataSerializerOptions), "options"), + Expression.Parameter(typeof(FormDataMapperOptions), "options"), Expression.Parameter(typeof(T).MakeByRefType(), "result"), Expression.Parameter(typeof(bool).MakeByRefType(), "foundValue")); } @@ -150,5 +150,4 @@ private record struct FormDataConverterReadParameters( ParameterExpression OptionsParam, ParameterExpression ResultParam, ParameterExpression FoundValueParam); - } diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs index b5ac980c1ed5..f633aca53e1e 100644 --- a/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs @@ -11,7 +11,7 @@ internal class ComplexTypeConverterFactory : IFormDataConverterFactory { internal static readonly ComplexTypeConverterFactory Instance = new(); - public bool CanConvert(Type type, FormDataSerializerOptions options) + public bool CanConvert(Type type, FormDataMapperOptions options) { if (type.GetConstructor(Type.EmptyTypes) == null && !type.IsValueType) { @@ -106,7 +106,7 @@ public bool CanConvert(Type type, FormDataSerializerOptions options) // } // } - public FormDataConverter CreateConverter(Type type, FormDataSerializerOptions options) + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) { if (Activator.CreateInstance(typeof(ComplexTypeExpressionConverterFactory<>).MakeGenericType(type)) is not ComplexTypeExpressionConverterFactory factory) diff --git a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs index d5e17ccdd66a..819244cf19a4 100644 --- a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs +++ b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs @@ -536,10 +536,10 @@ public void CanDeserialize_ComplexValueType_Address() ["ZipCode"] = "98052", }; var reader = new FormDataReader(data, CultureInfo.InvariantCulture); - var options = new FormDataSerializerOptions(); + var options = new FormDataMapperOptions(); // Act - var result = FormDataDeserializer.Deserialize
(reader, options); + var result = FormDataMapper.Map
(reader, options); // Assert Assert.Equal(expected.City, result.City); @@ -562,10 +562,10 @@ public void CanDeserialize_ComplexReferenceType_Customer() }; var reader = new FormDataReader(data, CultureInfo.InvariantCulture); - var options = new FormDataSerializerOptions(); + var options = new FormDataMapperOptions(); // Act - var result = FormDataDeserializer.Deserialize(reader, options); + var result = FormDataMapper.Map(reader, options); // Assert Assert.NotNull(result); @@ -575,6 +575,37 @@ public void CanDeserialize_ComplexReferenceType_Customer() Assert.Equal(expected.IsPreferred, result.IsPreferred); } + [Fact] + public void CanDeserialize_ComplexReferenceType_Inheritance() + { + // Arrange + var expected = new FrequentCustomer() { Age = 20, Name = "John Doe", Email = "john@example.com" , IsPreferred = true, TotalVisits = 10, PreferredStore = "Redmond", MonthlyFrequency = 0.8 }; + var data = new Dictionary() + { + ["Age"] = "20", + ["Name"] = "John Doe", + ["Email"] = "john@example.com", + ["IsPreferred"] = "true", + ["TotalVisits"] = "10", + ["PreferredStore"] = "Redmond", + ["MonthlyFrequency"] = "0.8", + }; + + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map(reader, options); + Assert.NotNull(result); + Assert.Equal(expected.Age, result.Age); + Assert.Equal(expected.Name, result.Name); + Assert.Equal(expected.Email, result.Email); + Assert.Equal(expected.IsPreferred, result.IsPreferred); + Assert.Equal(expected.TotalVisits, result.TotalVisits); + Assert.Equal(expected.PreferredStore, result.PreferredStore); + Assert.Equal(expected.MonthlyFrequency, result.MonthlyFrequency); + } + public static TheoryData NullableBasicTypes { get @@ -873,6 +904,15 @@ internal class Customer public bool IsPreferred { get; set; } } +internal class FrequentCustomer : Customer +{ + public int TotalVisits { get; set; } + + public string PreferredStore { get; set; } + + public double MonthlyFrequency { get; set; } +} + // Implements ICollection delegating to List _inner; internal class CustomCollection : ICollection { From 7e568475803b70a9a143e90bfb82dcd07f7326c8 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 1 Jun 2023 18:24:59 +0200 Subject: [PATCH 4/4] Update src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs --- .../ComplexTypeExpressionConverterFactoryOfT.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs index e6d425e7e571..4b87ce6accff 100644 --- a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs @@ -88,6 +88,13 @@ private CompiledComplexTypeConverter.ConverterDelegate CreateConverterBody(Ty propertyFoundValue)); body.Add(callTryRead); + // reader.PopPrefix("Property"); + body.Add(Expression.Call( + readerParam, + nameof(FormDataReader.PopPrefix), + Array.Empty(), + Expression.Constant(property.Name))); + body.Add(Expression.OrAssign(localFoundValueVar, propertyFoundValue)); }