Skip to content

Commit 6910f3d

Browse files
authored
[Blazor] Remaining model binding work (#49492)
* Mapping recursive types. * Types with constructors * Ignore properties via IgnoreDataMember attribute. * Rename properties via DataMember attribute. * Require properties to be present on the payload via DataMember attribute. * Map enum values. * Expose form mapping options via RazorComponentOptions: * Collection Size, recursion depth, max errors. * Culture. * Key buffer size. * More error handling: * Convert mapping exceptions to errors.
1 parent 9831728 commit 6910f3d

38 files changed

+2772
-163
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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.AspNetCore.Components.Endpoints.FormMapping;
5+
6+
namespace Microsoft.Extensions.DependencyInjection;
7+
8+
/// <summary>
9+
/// Provides options for configuring server-side rendering of Razor Components.
10+
/// </summary>
11+
public class RazorComponentOptions
12+
{
13+
internal readonly FormDataMapperOptions _formMappingOptions = new();
14+
15+
/// <summary>
16+
/// Gets or sets the maximum number of elements allowed in a form collection.
17+
/// </summary>
18+
public int MaxFormMappingCollectionSize
19+
{
20+
get => _formMappingOptions.MaxCollectionSize;
21+
set
22+
{
23+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
24+
_formMappingOptions.MaxCollectionSize = value;
25+
}
26+
}
27+
28+
/// <summary>
29+
/// Gets or sets the maximum depth allowed when recursively mapping form data.
30+
/// </summary>
31+
public int MaxFormMappingRecursionDepth
32+
{
33+
get => _formMappingOptions.MaxRecursionDepth;
34+
set
35+
{
36+
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, 1);
37+
_formMappingOptions.MaxRecursionDepth = value;
38+
}
39+
}
40+
41+
/// <summary>
42+
/// Gets or sets the maximum number of errors allowed when mapping form data.
43+
/// </summary>
44+
public int MaxFormMappingErrorCount
45+
{
46+
get => _formMappingOptions.MaxErrorCount;
47+
set
48+
{
49+
_formMappingOptions.MaxErrorCount = value;
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Gets or sets the maximum size of the buffer used to read form data keys.
55+
/// </summary>
56+
public int MaxFormMappingKeySize
57+
{
58+
get => _formMappingOptions.MaxKeyBufferSize;
59+
set => _formMappingOptions.MaxKeyBufferSize = value;
60+
}
61+
62+
/// <summary>
63+
/// Gets or sets a value that determines whether the current culture should be used when mapping form data.
64+
/// </summary>
65+
public bool FormMappingUseCurrentCulture
66+
{
67+
get => _formMappingOptions.UseCurrentCulture;
68+
set => _formMappingOptions.UseCurrentCulture = value;
69+
}
70+
}

src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ public static class RazorComponentsServiceCollectionExtensions
2727
/// Registers services required for server-side rendering of Razor Components.
2828
/// </summary>
2929
/// <param name="services">The service collection.</param>
30-
/// <returns>A builder for configuring the Razor Components endpoints.</returns>
30+
/// <returns>An <see cref="IRazorComponentsBuilder"/> that can be used to further configure the Razor component services.</returns>
3131
[RequiresUnreferencedCode("Razor Components does not currently support native AOT.", Url = "https://aka.ms/aspnet/nativeaot")]
3232
public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services)
3333
{
34+
ArgumentNullException.ThrowIfNull(services);
35+
3436
// Dependencies
3537
services.AddAntiforgery();
3638

@@ -69,13 +71,28 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
6971
return new DefaultRazorComponentsBuilder(services);
7072
}
7173

72-
private sealed class DefaultRazorComponentsBuilder : IRazorComponentsBuilder
74+
/// <summary>
75+
/// Registers services required for server-side rendering of Razor Components.
76+
/// </summary>
77+
/// <param name="services">The service collection.</param>
78+
/// <param name="setupAction">An <see cref="Action{RazorComponentOptions}"/> to configure the provided <see cref="RazorComponentOptions"/>.</param>
79+
/// <returns>An <see cref="IRazorComponentsBuilder"/> that can be used to further configure the Razor component services.</returns>
80+
public static IRazorComponentsBuilder AddRazorComponents(
81+
this IServiceCollection services,
82+
Action<RazorComponentOptions> setupAction
83+
)
7384
{
74-
public DefaultRazorComponentsBuilder(IServiceCollection services)
75-
{
76-
Services = services;
77-
}
85+
ArgumentNullException.ThrowIfNull(services);
86+
ArgumentNullException.ThrowIfNull(setupAction);
7887

79-
public IServiceCollection Services { get; }
88+
var builder = services.AddRazorComponents();
89+
services.Configure(setupAction);
90+
91+
return builder;
92+
}
93+
94+
private sealed class DefaultRazorComponentsBuilder(IServiceCollection services) : IRazorComponentsBuilder
95+
{
96+
public IServiceCollection Services { get; } = services;
8097
}
8198
}

src/Components/Endpoints/src/FormMapping/Converters/CollectionConverter.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ internal abstract class CollectionConverter<TCollection> : FormDataConverter<TCo
2424
internal class CollectionConverter<TCollection, TCollectionPolicy, TBuffer, TElement> : CollectionConverter<TCollection>
2525
where TCollectionPolicy : ICollectionBufferAdapter<TCollection, TBuffer, TElement>
2626
{
27+
private static readonly Type _elementType = typeof(TElement);
28+
2729
// Indexes up to 100 are pre-allocated to avoid allocations for common cases.
2830
private static readonly string[] Indexes = new string[] {
2931
"[0]", "[1]", "[2]", "[3]", "[4]", "[5]", "[6]", "[7]", "[8]", "[9]",
@@ -46,6 +48,8 @@ public CollectionConverter(FormDataConverter<TElement> elementConverter)
4648
_elementConverter = elementConverter;
4749
}
4850

51+
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
52+
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
4953
internal override bool TryRead(
5054
ref FormDataReader context,
5155
Type type,
@@ -63,7 +67,7 @@ internal override bool TryRead(
6367
try
6468
{
6569
context.PushPrefix("[0]");
66-
succeded = _elementConverter.TryRead(ref context, typeof(TElement), options, out currentElement!, out found);
70+
succeded = _elementConverter.TryRead(ref context, _elementType, options, out currentElement!, out found);
6771
}
6872
finally
6973
{
@@ -85,7 +89,7 @@ internal override bool TryRead(
8589
try
8690
{
8791
context.PushPrefix("[1]");
88-
currentElementSuccess = _elementConverter.TryRead(ref context, typeof(TElement), options, out currentElement!, out foundCurrentElement);
92+
currentElementSuccess = _elementConverter.TryRead(ref context, _elementType, options, out currentElement!, out foundCurrentElement);
8993
succeded = succeded && currentElementSuccess;
9094
}
9195
finally
@@ -113,7 +117,7 @@ internal override bool TryRead(
113117
try
114118
{
115119
context.PushPrefix(prefix);
116-
currentElementSuccess = _elementConverter.TryRead(ref context, typeof(TElement), options, out currentElement!, out foundCurrentElement);
120+
currentElementSuccess = _elementConverter.TryRead(ref context, _elementType, options, out currentElement!, out foundCurrentElement);
117121
succeded = succeded && currentElementSuccess;
118122
}
119123
finally
@@ -156,7 +160,7 @@ internal override bool TryRead(
156160
{
157161
computedPrefix[charsWritten + 1] = ']';
158162
context.PushPrefix(computedPrefix[..(charsWritten + 2)]);
159-
currentElementSuccess = _elementConverter.TryRead(ref context, typeof(TElement), options, out currentElement!, out foundCurrentElement);
163+
currentElementSuccess = _elementConverter.TryRead(ref context, _elementType, options, out currentElement!, out foundCurrentElement);
160164
succeded = succeded && currentElementSuccess;
161165
}
162166
finally
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
5+
46
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
57

68
internal class CompiledComplexTypeConverter<T>(CompiledComplexTypeConverter<T>.ConverterDelegate body) : FormDataConverter<T>
79
{
810
public delegate bool ConverterDelegate(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found);
911

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+
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
13+
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
14+
internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found)
15+
{
16+
result = default;
17+
found = false;
18+
19+
try
20+
{
21+
return body(ref context, type, options, out result, out found);
22+
}
23+
catch (Exception ex)
24+
{
25+
context.AddMappingError(ex, null);
26+
return false;
27+
}
28+
}
1229
}

src/Components/Endpoints/src/FormMapping/Converters/DictionaryConverter.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ internal sealed class DictionaryConverter<TDictionary, TDictionaryPolicy, TBuffe
1616
where TDictionaryPolicy : IDictionaryBufferAdapter<TDictionary, TBuffer, TKey, TValue>
1717
{
1818
private readonly FormDataConverter<TValue> _valueConverter;
19+
private static readonly Type _elementType = typeof(TValue);
1920

2021
public DictionaryConverter(FormDataConverter<TValue> elementConverter)
2122
{
2223
ArgumentNullException.ThrowIfNull(elementConverter);
23-
2424
_valueConverter = elementConverter;
2525
}
2626

27+
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
28+
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
2729
internal override bool TryRead(
2830
ref FormDataReader context,
2931
Type type,
@@ -57,7 +59,7 @@ internal override bool TryRead(
5759
foreach (var key in keys)
5860
{
5961
context.PushPrefix(key.Span);
60-
currentElementSuccess = _valueConverter.TryRead(ref context, typeof(TValue), options, out currentValue!, out foundCurrentValue);
62+
currentElementSuccess = _valueConverter.TryRead(ref context, _elementType, options, out currentValue!, out foundCurrentValue);
6163
succeded &= currentElementSuccess;
6264
context.PopPrefix(key.Span);
6365

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.Diagnostics.CodeAnalysis;
5+
using System.Runtime.CompilerServices;
6+
7+
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
8+
9+
internal class EnumConverter<TEnum> : FormDataConverter<TEnum> where TEnum : struct, Enum
10+
{
11+
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
12+
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
13+
internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out TEnum result, out bool found)
14+
{
15+
found = reader.TryGetValue(out var value);
16+
if (!found)
17+
{
18+
result = default;
19+
return true;
20+
}
21+
if (Enum.TryParse(value, ignoreCase: true, out result))
22+
{
23+
return true;
24+
}
25+
else
26+
{
27+
var segment = reader.GetLastPrefixSegment();
28+
reader.AddMappingError(FormattableStringFactory.Create(FormDataResources.EnumMappingError, value, segment), value);
29+
result = default;
30+
return false;
31+
}
32+
}
33+
}

src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
5+
46
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
57

68
internal sealed class NullableConverter<T> : FormDataConverter<T?> where T : struct
@@ -12,6 +14,8 @@ public NullableConverter(FormDataConverter<T> nonNullableConverter)
1214
_nonNullableConverter = nonNullableConverter;
1315
}
1416

17+
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
18+
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
1519
internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found)
1620
{
1721
if (!(_nonNullableConverter.TryRead(ref context, type, options, out var innerResult, out found) && found))

src/Components/Endpoints/src/FormMapping/Converters/ParsableConverter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Runtime.CompilerServices;
56

67
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
78

89
internal sealed class ParsableConverter<T> : FormDataConverter<T>, ISingleValueConverter where T : IParsable<T>
910
{
11+
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
12+
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
1013
internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found)
1114
{
1215
found = reader.TryGetValue(out var value);

src/Components/Endpoints/src/FormMapping/Factories/CollectionConverterFactory.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ internal class CollectionConverterFactory : IFormDataConverterFactory
1414
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
1515
public bool CanConvert(Type type, FormDataMapperOptions options)
1616
{
17-
var enumerable = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>));
18-
if (enumerable == null && !(type.IsArray && type.GetArrayRank() == 1))
17+
var element = ResolveElementType(type);
18+
if (element == null)
1919
{
2020
return false;
2121
}
2222

23-
var element = enumerable != null ? enumerable.GetGenericArguments()[0] : type.GetElementType()!;
24-
2523
if (Activator.CreateInstance(typeof(TypedCollectionConverterFactory<,>)
2624
.MakeGenericType(type, element!)) is not IFormDataConverterFactory factory)
2725
{
@@ -31,6 +29,19 @@ public bool CanConvert(Type type, FormDataMapperOptions options)
3129
return factory.CanConvert(type, options);
3230
}
3331

32+
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
33+
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
34+
public static Type? ResolveElementType(Type type)
35+
{
36+
var enumerable = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>));
37+
if (enumerable == null && !(type.IsArray && type.GetArrayRank() == 1))
38+
{
39+
return null;
40+
}
41+
42+
return enumerable != null ? enumerable.GetGenericArguments()[0] : type.GetElementType()!;
43+
}
44+
3445
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
3546
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
3647
public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)

src/Components/Endpoints/src/FormMapping/Factories/Collections/TypedCollectionConverterFactory.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ internal sealed class TypedCollectionConverterFactory<TCollection, TElement> : T
2626
public override bool CanConvert(Type _, FormDataMapperOptions options)
2727
{
2828
// Resolve the element type converter
29-
var elementTypeConverter = options.ResolveConverter<TElement>();
30-
31-
if (elementTypeConverter == null)
29+
if (!options.CanConvert(typeof(TElement)))
3230
{
3331
return false;
3432
}

0 commit comments

Comments
 (0)