Skip to content

Commit 110522a

Browse files
authored
[Blazor] SupplyParameterFromForm dictionary support (#48565)
* Adds support for binding dictionaries of primitive types.
1 parent 07e42c1 commit 110522a

16 files changed

+780
-7
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
// Uses a concrete type that implements IDictionary<TKey, TValue> as a buffer.
7+
internal sealed class DictionaryBufferAdapter<TDictionaryType, TKey, TValue>
8+
: IDictionaryBufferAdapter<TDictionaryType, TDictionaryType, TKey, TValue>
9+
where TDictionaryType : IDictionary<TKey, TValue>, new()
10+
where TKey : IParsable<TKey>
11+
{
12+
public static TDictionaryType Add(ref TDictionaryType buffer, TKey key, TValue value)
13+
{
14+
buffer.Add(key, value);
15+
return buffer;
16+
}
17+
18+
public static TDictionaryType CreateBuffer() => new TDictionaryType();
19+
20+
public static TDictionaryType ToResult(TDictionaryType buffer) => buffer;
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
// Adapts a concrete dictionary type into an interface.
7+
internal sealed class DictionaryStaticCastAdapter<TDictionaryInterface, TDictionaryImplementation, TDictionaryAdapter, TBuffer, TKey, TValue>
8+
: IDictionaryBufferAdapter<TDictionaryInterface, TBuffer, TKey, TValue>
9+
where TDictionaryAdapter : IDictionaryBufferAdapter<TDictionaryImplementation, TBuffer, TKey, TValue>
10+
where TDictionaryImplementation : TDictionaryInterface
11+
where TKey : IParsable<TKey>
12+
{
13+
public static TBuffer CreateBuffer() => TDictionaryAdapter.CreateBuffer();
14+
15+
public static TBuffer Add(ref TBuffer buffer, TKey key, TValue element) => TDictionaryAdapter.Add(ref buffer, key, element);
16+
17+
public static TDictionaryInterface ToResult(TBuffer buffer) => TDictionaryAdapter.ToResult(buffer);
18+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
// Interface for constructing a dictionary like instance using the
7+
// dictionary converter.
8+
// This interface abstracts over the different ways of constructing a dictionary.
9+
// For example, Immutable types use a builder as a buffer, while other types
10+
// use an instance of the dictionary itself as a buffer.
11+
internal interface IDictionaryBufferAdapter<TDictionary, TBuffer, TKey, TValue>
12+
where TKey : IParsable<TKey>
13+
{
14+
public static abstract TBuffer CreateBuffer();
15+
16+
public static abstract TBuffer Add(ref TBuffer buffer, TKey key, TValue value);
17+
18+
public static abstract TDictionary ToResult(TBuffer buffer);
19+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.Collections.Immutable;
5+
6+
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
7+
8+
internal sealed class ImmutableDictionaryBufferAdapter<TKey, TValue>
9+
: IDictionaryBufferAdapter<ImmutableDictionary<TKey, TValue>, ImmutableDictionary<TKey, TValue>.Builder, TKey, TValue>
10+
where TKey : IParsable<TKey>
11+
{
12+
public static ImmutableDictionary<TKey, TValue>.Builder Add(ref ImmutableDictionary<TKey, TValue>.Builder buffer, TKey key, TValue value)
13+
{
14+
buffer.Add(key, value);
15+
return buffer;
16+
}
17+
18+
public static ImmutableDictionary<TKey, TValue>.Builder CreateBuffer() => ImmutableDictionary.CreateBuilder<TKey, TValue>();
19+
20+
public static ImmutableDictionary<TKey, TValue> ToResult(ImmutableDictionary<TKey, TValue>.Builder buffer) => buffer.ToImmutable();
21+
22+
internal static DictionaryConverter<IImmutableDictionary<TKey, TValue>> CreateInterfaceConverter(FormDataConverter<TValue> valueTypeConverter)
23+
{
24+
return new DictionaryConverter<IImmutableDictionary<TKey, TValue>,
25+
DictionaryStaticCastAdapter<
26+
IImmutableDictionary<TKey, TValue>,
27+
ImmutableDictionary<TKey, TValue>,
28+
ImmutableDictionaryBufferAdapter<TKey, TValue>,
29+
ImmutableDictionary<TKey, TValue>.Builder,
30+
TKey,
31+
TValue>,
32+
ImmutableDictionary<TKey, TValue>.Builder,
33+
TKey,
34+
TValue>(valueTypeConverter);
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.Collections.Immutable;
5+
6+
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
7+
8+
internal sealed class ImmutableSortedDictionaryBufferAdapter<TKey, TValue>
9+
: IDictionaryBufferAdapter<ImmutableSortedDictionary<TKey, TValue>, ImmutableSortedDictionary<TKey, TValue>.Builder, TKey, TValue>
10+
where TKey : IParsable<TKey>
11+
{
12+
public static ImmutableSortedDictionary<TKey, TValue>.Builder Add(ref ImmutableSortedDictionary<TKey, TValue>.Builder buffer, TKey key, TValue value)
13+
{
14+
buffer.Add(key, value);
15+
return buffer;
16+
}
17+
18+
public static ImmutableSortedDictionary<TKey, TValue>.Builder CreateBuffer() => ImmutableSortedDictionary.CreateBuilder<TKey, TValue>();
19+
20+
public static ImmutableSortedDictionary<TKey, TValue> ToResult(ImmutableSortedDictionary<TKey, TValue>.Builder buffer) => buffer.ToImmutable();
21+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.Collections.ObjectModel;
5+
6+
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
7+
8+
internal sealed class ReadOnlyDictionaryBufferAdapter<TKey, TValue>
9+
: IDictionaryBufferAdapter<ReadOnlyDictionary<TKey, TValue>, Dictionary<TKey, TValue>, TKey, TValue>
10+
where TKey : IParsable<TKey>
11+
{
12+
public static Dictionary<TKey, TValue> Add(ref Dictionary<TKey, TValue> buffer, TKey key, TValue value)
13+
{
14+
buffer.Add(key, value);
15+
return buffer;
16+
}
17+
18+
public static Dictionary<TKey, TValue> CreateBuffer() =>
19+
new Dictionary<TKey, TValue>();
20+
21+
public static ReadOnlyDictionary<TKey, TValue> ToResult(Dictionary<TKey, TValue> buffer) =>
22+
new ReadOnlyDictionary<TKey, TValue>(buffer);
23+
24+
internal static DictionaryConverter<IReadOnlyDictionary<TKey, TValue>> CreateInterfaceConverter(FormDataConverter<TValue> valueTypeConverter)
25+
{
26+
return new DictionaryConverter<IReadOnlyDictionary<TKey, TValue>,
27+
DictionaryStaticCastAdapter<
28+
IReadOnlyDictionary<TKey, TValue>,
29+
ReadOnlyDictionary<TKey, TValue>,
30+
ReadOnlyDictionaryBufferAdapter<TKey, TValue>,
31+
Dictionary<TKey, TValue>,
32+
TKey,
33+
TValue>,
34+
Dictionary<TKey, TValue>,
35+
TKey,
36+
TValue>(valueTypeConverter);
37+
}
38+
39+
internal static DictionaryConverter<ReadOnlyDictionary<TKey, TValue>> CreateConverter(FormDataConverter<TValue> valueTypeConverter)
40+
{
41+
return new DictionaryConverter<ReadOnlyDictionary<TKey, TValue>,
42+
ReadOnlyDictionaryBufferAdapter<TKey, TValue>,
43+
Dictionary<TKey, TValue>,
44+
TKey,
45+
TValue>(valueTypeConverter);
46+
}
47+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.Globalization;
6+
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
7+
8+
internal abstract class DictionaryConverter<TDictionary> : FormDataConverter<TDictionary>
9+
{
10+
}
11+
12+
internal sealed class DictionaryConverter<TDictionary, TDictionaryPolicy, TBuffer, TKey, TValue> : DictionaryConverter<TDictionary>
13+
where TKey : IParsable<TKey>
14+
where TDictionaryPolicy : IDictionaryBufferAdapter<TDictionary, TBuffer, TKey, TValue>
15+
{
16+
private readonly FormDataConverter<TValue> _valueConverter;
17+
18+
public DictionaryConverter(FormDataConverter<TValue> elementConverter)
19+
{
20+
ArgumentNullException.ThrowIfNull(elementConverter);
21+
22+
_valueConverter = elementConverter;
23+
}
24+
25+
internal override bool TryRead(
26+
ref FormDataReader context,
27+
Type type,
28+
FormDataMapperOptions options,
29+
[NotNullWhen(true)] out TDictionary? result,
30+
out bool found)
31+
{
32+
TValue currentValue;
33+
TBuffer buffer;
34+
bool foundCurrentValue;
35+
bool currentElementSuccess;
36+
bool succeded = true;
37+
38+
found = context.GetKeys().GetEnumerator().MoveNext();
39+
if (!found)
40+
{
41+
result = default!;
42+
return true;
43+
}
44+
45+
buffer = TDictionaryPolicy.CreateBuffer();
46+
47+
// We can't precompute dictionary anyKeys ahead of time,
48+
// so the moment we find a dictionary, we request the list of anyKeys
49+
// for the current location, which will involve parsing the form data anyKeys
50+
// and building a tree of anyKeys.
51+
var keyCount = 0;
52+
var maxCollectionSize = options.MaxCollectionSize;
53+
54+
foreach (var key in context.GetKeys())
55+
{
56+
context.PushPrefix(key);
57+
currentElementSuccess = _valueConverter.TryRead(ref context, typeof(TValue), options, out currentValue!, out foundCurrentValue);
58+
context.PopPrefix(key);
59+
60+
if (!TKey.TryParse(key[1..^1], CultureInfo.InvariantCulture, out var keyValue))
61+
{
62+
succeded = false;
63+
// Will report an error about unparsable key here.
64+
65+
// Continue trying to bind the rest of the dictionary.
66+
continue;
67+
}
68+
69+
TDictionaryPolicy.Add(ref buffer, keyValue!, currentValue);
70+
keyCount++;
71+
if (keyCount == maxCollectionSize)
72+
{
73+
succeded = false;
74+
break;
75+
}
76+
}
77+
78+
result = TDictionaryPolicy.ToResult(buffer);
79+
return succeded;
80+
}
81+
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@ internal class CollectionConverterFactory : IFormDataConverterFactory
1212
public bool CanConvert(Type type, FormDataMapperOptions options)
1313
{
1414
var enumerable = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>));
15-
if (enumerable == null && !type.IsArray && type.GetArrayRank() != 1)
15+
if (enumerable == null && !(type.IsArray && type.GetArrayRank() == 1))
1616
{
1717
return false;
1818
}
1919

2020
var element = enumerable != null ? enumerable.GetGenericArguments()[0] : type.GetElementType()!;
2121

22-
return options.HasConverter(element);
22+
if (Activator.CreateInstance(typeof(TypedCollectionConverterFactory<,>)
23+
.MakeGenericType(type, element!)) is not IFormDataConverterFactory factory)
24+
{
25+
return false;
26+
}
27+
28+
return factory.CanConvert(type, options);
2329
}
2430

2531
public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
55

6-
internal sealed class CollectionLikeTypedCollectionConverterFactory<TCollection, TElement>
6+
internal class ConcreteTypeCollectionConverterFactory<TCollection, TElement>
77
: IFormDataConverterFactory
88
{
9-
public static readonly CollectionLikeTypedCollectionConverterFactory<TCollection, TElement> Instance =
9+
public static readonly ConcreteTypeCollectionConverterFactory<TCollection, TElement> Instance =
1010
new();
1111

1212
public bool CanConvert(Type _, FormDataMapperOptions options) => true;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ var _ when type.IsAssignableTo(typeof(ImmutableStack<TElement>)) =>
155155
// Some of the types above implement ICollection<T>, but do so in a very inneficient way, so we want to
156156
// use special converters for them.
157157
var _ when type.IsAssignableTo(typeof(ICollection<TElement>))
158-
=> CollectionLikeTypedCollectionConverterFactory<TCollection, TElement>.Instance.CreateConverter(typeof(TCollection), options),
158+
=> ConcreteTypeCollectionConverterFactory<TCollection, TElement>.Instance.CreateConverter(typeof(TCollection), options),
159159
_ => throw new InvalidOperationException($"Unable to create converter for '{typeof(TCollection).FullName}'.")
160160
};
161161
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 sealed class ConcreteTypeDictionaryConverterFactory<TDictionary, TKey, TValue> : IFormDataConverterFactory
7+
where TKey : IParsable<TKey>
8+
{
9+
public static readonly ConcreteTypeDictionaryConverterFactory<TDictionary, TKey, TValue> Instance = new();
10+
11+
public bool CanConvert(Type type, FormDataMapperOptions options) => true;
12+
13+
public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
14+
{
15+
// Resolve the element type converter
16+
var keyConverter = options.ResolveConverter<TKey>() ??
17+
throw new InvalidOperationException($"Unable to create converter for '{typeof(TDictionary).FullName}'.");
18+
19+
var valueConverter = options.ResolveConverter<TValue>() ??
20+
throw new InvalidOperationException($"Unable to create converter for '{typeof(TDictionary).FullName}'.");
21+
22+
var customFactory = Activator.CreateInstance(typeof(CustomDictionaryConverterFactory<>)
23+
.MakeGenericType(typeof(TDictionary), typeof(TKey), typeof(TValue), typeof(TDictionary))) as CustomDictionaryConverterFactory;
24+
25+
if (customFactory == null)
26+
{
27+
throw new InvalidOperationException($"Unable to create converter for type '{typeof(TDictionary).FullName}'.");
28+
}
29+
30+
return customFactory.CreateConverter(keyConverter, valueConverter);
31+
}
32+
33+
private abstract class CustomDictionaryConverterFactory
34+
{
35+
public abstract FormDataConverter CreateConverter(FormDataConverter<TKey> keyConverter, FormDataConverter<TValue> valueConverter);
36+
}
37+
38+
private class CustomDictionaryConverterFactory<TCustomDictionary> : CustomDictionaryConverterFactory
39+
where TCustomDictionary : TDictionary, IDictionary<TKey, TValue>, new()
40+
{
41+
public override FormDataConverter CreateConverter(FormDataConverter<TKey> keyConverter, FormDataConverter<TValue> valueConverter)
42+
{
43+
return new DictionaryConverter<
44+
TCustomDictionary,
45+
DictionaryBufferAdapter<TCustomDictionary, TKey, TValue>,
46+
TCustomDictionary,
47+
TKey,
48+
TValue>(valueConverter);
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)