From 1f39af7b2167326f34dc2f295eef26cb2ef8deb4 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 30 May 2023 02:01:31 +0200 Subject: [PATCH 1/5] Dictionary binding support --- .../IDictionaryBufferAdapter.cs | 14 + .../Binding/Converters/DictionaryConverter.cs | 81 ++++++ .../Factories/CollectionConverterFactory.cs | 10 +- ...ConcreteTypeCollectionConverterFactory.cs} | 8 +- .../TypedCollectionConverterFactory.cs | 2 +- .../ConcreteTypeDictionaryConverterFactory.cs | 51 ++++ .../TypedDictionaryConverterFactory.cs | 268 ++++++++++++++++++ .../Factories/DictionaryConverterFactory.cs | 118 ++++++++ .../src/Binding/FormDataMapperOptions.cs | 1 + .../Endpoints/src/Binding/FormDataReader.cs | 5 + .../test/Binding/FormDataMapperTests.cs | 209 +++++++++++++- 11 files changed, 759 insertions(+), 8 deletions(-) create mode 100644 src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs create mode 100644 src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs rename src/Components/Endpoints/src/Binding/Factories/Collections/{CollectionLikeTypedCollectionConverterFactory.cs => ConcreteTypeCollectionConverterFactory.cs} (82%) create mode 100644 src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs create mode 100644 src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs create mode 100644 src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs new file mode 100644 index 000000000000..ded44af82c49 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.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 interface IDictionaryBufferAdapter + where TKey : IParsable +{ + public static abstract TBuffer CreateBuffer(); + + public static abstract TBuffer Add(ref TBuffer buffer, TKey key, TValue value); + + public static abstract TDictionary ToResult(TBuffer buffer); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs new file mode 100644 index 000000000000..57bd6b96e45b --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal abstract class DictionaryConverter : FormDataConverter +{ +} + +internal class DictionaryConverter : DictionaryConverter + where TKey : IParsable + where TDictionaryPolicy : IDictionaryBufferAdapter +{ + private readonly FormDataConverter _valueConverter; + + public DictionaryConverter(FormDataConverter elementConverter) + { + ArgumentNullException.ThrowIfNull(elementConverter); + + _valueConverter = elementConverter; + } + + internal override bool TryRead( + ref FormDataReader context, + Type type, + FormDataSerializerOptions options, + [NotNullWhen(true)] out TDictionary? result, + out bool found) + { + TValue currentValue; + TBuffer buffer; + bool foundCurrentValue; + bool currentElementSuccess; + bool succeded = true; + + found = context.GetKeys().GetEnumerator().MoveNext(); + if (!found) + { + result = default!; + return true; + } + + buffer = TDictionaryPolicy.CreateBuffer(); + + // We can't precompute dictionary anyKeys ahead of time, + // so the moment we find a dictionary, we request the list of anyKeys + // for the current location, which will involve parsing the form data anyKeys + // and building a tree of anyKeys. + var keyCount = 0; + var maxCollectionSize = options.MaxCollectionSize; + + foreach (var key in context.GetKeys()) + { + context.PushPrefix(key); + currentElementSuccess = _valueConverter.TryRead(ref context, typeof(TValue), options, out currentValue!, out foundCurrentValue); + context.PopPrefix(key); + + if (!TKey.TryParse(key[1..^1], CultureInfo.InvariantCulture, out var keyValue)) + { + succeded = false; + // Will report an error about unparsable key here. + + // Continue trying to bind the rest of the dictionary. + continue; + } + + TDictionaryPolicy.Add(ref buffer, keyValue!, currentValue); + keyCount++; + if (keyCount > maxCollectionSize) + { + succeded = false; + break; + } + } + + result = TDictionaryPolicy.ToResult(buffer); + return succeded; + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/CollectionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/CollectionConverterFactory.cs index 6b60e38f8e42..0a9c17840d0b 100644 --- a/src/Components/Endpoints/src/Binding/Factories/CollectionConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/CollectionConverterFactory.cs @@ -12,14 +12,20 @@ internal class CollectionConverterFactory : IFormDataConverterFactory public bool CanConvert(Type type, FormDataMapperOptions options) { var enumerable = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>)); - if (enumerable == null && !type.IsArray && type.GetArrayRank() != 1) + if (enumerable == null && !(type.IsArray && type.GetArrayRank() == 1)) { return false; } var element = enumerable != null ? enumerable.GetGenericArguments()[0] : type.GetElementType()!; - return options.HasConverter(element); + if (Activator.CreateInstance(typeof(TypedCollectionConverterFactory<,>) + .MakeGenericType(type, element!)) is not IFormDataConverterFactory factory) + { + return false; + } + + return factory.CanConvert(type, options); } public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) diff --git a/src/Components/Endpoints/src/Binding/Factories/Collections/CollectionLikeTypedCollectionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs similarity index 82% rename from src/Components/Endpoints/src/Binding/Factories/Collections/CollectionLikeTypedCollectionConverterFactory.cs rename to src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs index 20598e872451..ce6e30ba0c48 100644 --- a/src/Components/Endpoints/src/Binding/Factories/Collections/CollectionLikeTypedCollectionConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs @@ -3,15 +3,15 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; -internal sealed class CollectionLikeTypedCollectionConverterFactory +internal class ConcreteTypeCollectionConverterFactory : IFormDataConverterFactory { - public static readonly CollectionLikeTypedCollectionConverterFactory Instance = + public static readonly ConcreteTypeCollectionConverterFactory Instance = new(); - public bool CanConvert(Type _, FormDataMapperOptions options) => true; + public bool CanConvert(Type _, FormDataSerializerOptions options) => true; - public FormDataConverter CreateConverter(Type _, FormDataMapperOptions options) + public FormDataConverter CreateConverter(Type _, FormDataSerializerOptions options) { // Resolve the element type converter var elementTypeConverter = options.ResolveConverter() ?? diff --git a/src/Components/Endpoints/src/Binding/Factories/Collections/TypedCollectionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Collections/TypedCollectionConverterFactory.cs index 0552239b4fd6..e3b922555748 100644 --- a/src/Components/Endpoints/src/Binding/Factories/Collections/TypedCollectionConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/Collections/TypedCollectionConverterFactory.cs @@ -155,7 +155,7 @@ var _ when type.IsAssignableTo(typeof(ImmutableStack)) => // Some of the types above implement ICollection, but do so in a very inneficient way, so we want to // use special converters for them. var _ when type.IsAssignableTo(typeof(ICollection)) - => CollectionLikeTypedCollectionConverterFactory.Instance.CreateConverter(typeof(TCollection), options), + => ConcreteTypeCollectionConverterFactory.Instance.CreateConverter(typeof(TCollection), options), _ => throw new InvalidOperationException($"Unable to create converter for '{typeof(TCollection).FullName}'.") }; } diff --git a/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs new file mode 100644 index 000000000000..6c18de929333 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs @@ -0,0 +1,51 @@ +// 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 ConcreteTypeDictionaryConverterFactory : IFormDataConverterFactory + where TKey : IParsable +{ + public static readonly ConcreteTypeDictionaryConverterFactory Instance = new(); + + public bool CanConvert(Type type, FormDataSerializerOptions options) => true; + + public FormDataConverter CreateConverter(Type type, FormDataSerializerOptions options) + { + // Resolve the element type converter + var keyConverter = options.ResolveConverter() ?? + throw new InvalidOperationException($"Unable to create converter for '{typeof(TDictionary).FullName}'."); + + var valueConverter = options.ResolveConverter() ?? + throw new InvalidOperationException($"Unable to create converter for '{typeof(TDictionary).FullName}'."); + + var customFactory = Activator.CreateInstance(typeof(CustomDictionaryConverterFactory<>) + .MakeGenericType(typeof(TDictionary), typeof(TKey), typeof(TValue), typeof(TDictionary))) as CustomDictionaryConverterFactory; + + if (customFactory == null) + { + throw new InvalidOperationException($"Unable to create converter for type '{typeof(TDictionary).FullName}'."); + } + + return customFactory.CreateConverter(keyConverter, valueConverter); + } + + private abstract class CustomDictionaryConverterFactory + { + public abstract FormDataConverter CreateConverter(FormDataConverter keyConverter, FormDataConverter valueConverter); + } + + private class CustomDictionaryConverterFactory : CustomDictionaryConverterFactory + where TCustomDictionary : TDictionary, IDictionary, new() + { + public override FormDataConverter CreateConverter(FormDataConverter keyConverter, FormDataConverter valueConverter) + { + return new DictionaryConverter< + TCustomDictionary, + DictionaryBufferAdapter, + TCustomDictionary, + TKey, + TValue>(valueConverter); + } + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs new file mode 100644 index 000000000000..7e61400cb592 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs @@ -0,0 +1,268 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Collections.ObjectModel; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal class TypedDictionaryConverterFactory : IFormDataConverterFactory + where TKey : IParsable +{ + public bool CanConvert(Type type, FormDataSerializerOptions options) + { + // Resolve the value type converter + var valueTypeConverter = options.ResolveConverter(); + if (valueTypeConverter == null) + { + return false; + } + + var keyTypeConverter = options.ResolveConverter(); + if (keyTypeConverter == null) + { + return false; + } + + if (type.IsInterface) + { + // At this point we are dealing with an interface. We test from the most specific to the least specific + // to find the best fit for the well-known set of interfaces we support. + return type switch + { + // System.Collections.Immutable + var _ when type == (typeof(IImmutableDictionary)) => true, + + // System.Collections.Generics + var _ when type == (typeof(IReadOnlyDictionary)) => true, + var _ when type == (typeof(IDictionary)) => true, + + _ => throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."), + }; + } + + if (!type.IsAbstract && !type.IsGenericTypeDefinition) + { + return type switch + { + // Immutable collections + var _ when type == (typeof(ImmutableDictionary)) => true, + var _ when type == (typeof(ImmutableSortedDictionary)) => true, + + // Concurrent collections + var _ when type == (typeof(ConcurrentDictionary)) => true, + + // Generic collections + var _ when type == (typeof(SortedList)) => true, + var _ when type == (typeof(SortedDictionary)) => true, + var _ when type == (typeof(Dictionary)) => true, + + var _ when type == (typeof(ReadOnlyDictionary)) => true, + + // Some of the types above implement IDictionary, but do so in a very inneficient way, so we want to + // use special converters for them. + var _ when type.IsAssignableTo(typeof(IDictionary)) && type.GetConstructor(Type.EmptyTypes) != null => true, + _ => false + }; + } + + return false; + } + + public FormDataConverter CreateConverter(Type type, FormDataSerializerOptions options) + { + // Resolve the value type converter + var valueTypeConverter = options.ResolveConverter(); + if (valueTypeConverter == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + if (type.IsInterface) + { + // At this point we are dealing with an interface. We test from the most specific to the least specific + // to find the best fit for the well-known set of interfaces we support. + return type switch + { + // System.Collections.Immutable + var _ when type == (typeof(IImmutableDictionary)) => + ImmutableDictionaryBufferAdapter.CreateInterfaceConverter(valueTypeConverter), + // System.Collections.Generics + var _ when type == (typeof(IReadOnlyDictionary)) => + ReadOnlyDictionaryBufferAdapter.CreateInterfaceConverter(valueTypeConverter), + var _ when type == (typeof(IDictionary)) => + new DictionaryConverter, + DictionaryStaticCastAdapter< + IDictionary, + Dictionary, + DictionaryBufferAdapter, TKey, TValue>, + Dictionary, + TKey, + TValue>, + Dictionary, TKey, TValue>(valueTypeConverter), + + _ => throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."), + }; + } + + if (!type.IsAbstract && !type.IsGenericTypeDefinition) + { + return type switch + { + var _ when type == (typeof(ReadOnlyDictionary)) => + ReadOnlyDictionaryBufferAdapter.CreateConverter(valueTypeConverter), + // Immutable collections + var _ when type == (typeof(ImmutableDictionary)) => + new DictionaryConverter< + ImmutableDictionary, + ImmutableDictionaryBufferAdapter, + ImmutableDictionary.Builder, + TKey, + TValue>(valueTypeConverter), + var _ when type == (typeof(ImmutableSortedDictionary)) => + new DictionaryConverter< + ImmutableSortedDictionary, + ImmutableSortedDictionaryBufferAdapter, + ImmutableSortedDictionary.Builder, + TKey, + TValue>(valueTypeConverter), + + // Concurrent collections + var _ when type == (typeof(ConcurrentDictionary)) => + ConcreteTypeDictionaryConverterFactory, TKey, TValue>.Instance.CreateConverter(type, options), + + // Generic collections + var _ when type == (typeof(SortedList)) => + ConcreteTypeDictionaryConverterFactory, TKey, TValue>.Instance.CreateConverter(type, options), + var _ when type == (typeof(SortedDictionary)) => + ConcreteTypeDictionaryConverterFactory, TKey, TValue>.Instance.CreateConverter(type, options), + var _ when type == (typeof(Dictionary)) => + ConcreteTypeDictionaryConverterFactory, TKey, TValue>.Instance.CreateConverter(type, options), + + // Some of the types above implement IDictionary, but do so in a very inneficient way, so we want to + // use special converters for them. + var _ when type.IsAssignableTo(typeof(IDictionary)) && type.GetConstructor(Type.EmptyTypes) != null => + ConcreteTypeDictionaryConverterFactory.Instance.CreateConverter(type, options), + _ => throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."), + }; + } + + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } +} + +internal class DictionaryBufferAdapter + : IDictionaryBufferAdapter + where TDictionaryType : IDictionary, new() + where TKey : IParsable +{ + public static TDictionaryType Add(ref TDictionaryType buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static TDictionaryType CreateBuffer() => new TDictionaryType(); + + public static TDictionaryType ToResult(TDictionaryType buffer) => buffer; +} + +internal class ReadOnlyDictionaryBufferAdapter + : IDictionaryBufferAdapter, Dictionary, TKey, TValue> + where TKey : IParsable +{ + public static Dictionary Add(ref Dictionary buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static Dictionary CreateBuffer() => + new Dictionary(); + + public static ReadOnlyDictionary ToResult(Dictionary buffer) => + new ReadOnlyDictionary(buffer); + + internal static DictionaryConverter> CreateInterfaceConverter(FormDataConverter valueTypeConverter) + { + return new DictionaryConverter, + DictionaryStaticCastAdapter< + IReadOnlyDictionary, + ReadOnlyDictionary, + ReadOnlyDictionaryBufferAdapter, + Dictionary, + TKey, + TValue>, + Dictionary, + TKey, + TValue>(valueTypeConverter); + } + + internal static DictionaryConverter> CreateConverter(FormDataConverter valueTypeConverter) + { + return new DictionaryConverter, + ReadOnlyDictionaryBufferAdapter, + Dictionary, + TKey, + TValue>(valueTypeConverter); + } +} + +internal class ImmutableDictionaryBufferAdapter + : IDictionaryBufferAdapter, ImmutableDictionary.Builder, TKey, TValue> + where TKey : IParsable +{ + public static ImmutableDictionary.Builder Add(ref ImmutableDictionary.Builder buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static ImmutableDictionary.Builder CreateBuffer() => ImmutableDictionary.CreateBuilder(); + + public static ImmutableDictionary ToResult(ImmutableDictionary.Builder buffer) => buffer.ToImmutable(); + + internal static DictionaryConverter> CreateInterfaceConverter(FormDataConverter valueTypeConverter) + { + return new DictionaryConverter, + DictionaryStaticCastAdapter< + IImmutableDictionary, + ImmutableDictionary, + ImmutableDictionaryBufferAdapter, + ImmutableDictionary.Builder, + TKey, + TValue>, + ImmutableDictionary.Builder, + TKey, + TValue>(valueTypeConverter); + } +} + +internal class ImmutableSortedDictionaryBufferAdapter + : IDictionaryBufferAdapter, ImmutableSortedDictionary.Builder, TKey, TValue> + where TKey : IParsable +{ + public static ImmutableSortedDictionary.Builder Add(ref ImmutableSortedDictionary.Builder buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static ImmutableSortedDictionary.Builder CreateBuffer() => ImmutableSortedDictionary.CreateBuilder(); + + public static ImmutableSortedDictionary ToResult(ImmutableSortedDictionary.Builder buffer) => buffer.ToImmutable(); +} + +internal class DictionaryStaticCastAdapter + : IDictionaryBufferAdapter + where TDictionaryAdapter : IDictionaryBufferAdapter + where TDictionaryImplementation : TDictionaryInterface + where TKey : IParsable +{ + public static TBuffer CreateBuffer() => TDictionaryAdapter.CreateBuffer(); + + public static TBuffer Add(ref TBuffer buffer, TKey key, TValue element) => TDictionaryAdapter.Add(ref buffer, key, element); + + public static TDictionaryInterface ToResult(TBuffer buffer) => TDictionaryAdapter.ToResult(buffer); +} diff --git a/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs new file mode 100644 index 000000000000..caa68c14d2cc --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs @@ -0,0 +1,118 @@ +// 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; + +internal class DictionaryConverterFactory : IFormDataConverterFactory +{ + internal static readonly DictionaryConverterFactory Instance = new(); + + public bool CanConvert(Type type, FormDataSerializerOptions options) + { + // Well-known dictionary types + // IDictionary + // IReadOnlyDictionary + // Dictionary + // SortedDictionary + // SortedList + // ConcurrentDictionary + // ImmutableDictionary + // ImmutableSortedDictionary + + // Type must implement IDictionary IReadOnlyDictionary + // Note that IDictionary doesn't extend IReadOnlyDictionary, hence the need for two checks + var dictionaryType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IDictionary<,>)) ?? + ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IReadOnlyDictionary<,>)); + + if (dictionaryType == null) + { + return false; + } + + // Key type must implement IParsable + var keyType = dictionaryType?.GetGenericArguments()[0]; + if (keyType == null) + { + return false; + } + + var parsableKeyType = ClosedGenericMatcher.ExtractGenericInterface(keyType, typeof(IParsable<>)); + if (parsableKeyType == null) + { + return false; + } + + // Value must have a converter + var valueType = dictionaryType?.GetGenericArguments()[1]; + if (valueType == null) + { + return false; + } + + var converter = options.ResolveConverter(valueType); + if (converter == null) + { + return false; + } + + var factory = Activator.CreateInstance(typeof(TypedDictionaryConverterFactory<,,>) + .MakeGenericType(type, keyType, valueType)) as IFormDataConverterFactory; + + if (factory == null) + { + return false; + } + + return factory.CanConvert(type, options); + } + + public FormDataConverter CreateConverter(Type type, FormDataSerializerOptions options) + { + // Type must implement IDictionary IReadOnlyDictionary + // Note that IDictionary doesn't extend IReadOnlyDictionary, hence the need for two checks + var dictionaryType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IDictionary<,>)) ?? + ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IReadOnlyDictionary<,>)); + if (dictionaryType == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + // Key type must implement IParsable + var keyType = dictionaryType?.GetGenericArguments()[0]; + if (keyType == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + var parsableKeyType = ClosedGenericMatcher.ExtractGenericInterface(keyType, typeof(IParsable<>)); + if (parsableKeyType == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + // Value must have a converter + var valueType = dictionaryType?.GetGenericArguments()[1]; + if (valueType == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + var converter = options.ResolveConverter(valueType); + if (converter == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + var factory = Activator.CreateInstance(typeof(TypedDictionaryConverterFactory<,,>) + .MakeGenericType(type, keyType, valueType)) as IFormDataConverterFactory; + + if (factory == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + 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..32453ce0ed3f 100644 --- a/src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs +++ b/src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs @@ -16,6 +16,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) => DictionaryConverterFactory.Instance.CanConvert(type, options) ? DictionaryConverterFactory.Instance.CreateConverter(type, options) : null); _factories.Add((type, options) => CollectionConverterFactory.Instance.CanConvert(type, options) ? CollectionConverterFactory.Instance.CreateConverter(type, options) : null); } diff --git a/src/Components/Endpoints/src/Binding/FormDataReader.cs b/src/Components/Endpoints/src/Binding/FormDataReader.cs index 3f36ca390df2..e2f656af3736 100644 --- a/src/Components/Endpoints/src/Binding/FormDataReader.cs +++ b/src/Components/Endpoints/src/Binding/FormDataReader.cs @@ -21,6 +21,11 @@ public FormDataReader(IReadOnlyDictionary formCollection, public IFormatProvider Culture { get; internal set; } + internal IEnumerable GetKeys() + { + return _formCollection.Keys; + } + internal void PopPrefix(string _) { // For right now, we don't need to do anything with the prefix diff --git a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs index 96e181ba603a..359542905430 100644 --- a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs +++ b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs @@ -480,6 +480,213 @@ public void CanDeserialize_Collections_IImmutableStack() CanDeserialize_Collection, ImmutableStack, int>(expected); } + [Fact] + public void CanDeserialize_Dictionary_Dictionary() + { + // Arrange + var expected = new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + CanDeserialize_Dictionary, Dictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_ConcurrentDictionary() + { + // Arrange + var expected = new ConcurrentDictionary(new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }); + CanDeserialize_Dictionary, ConcurrentDictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_ImmutableDictionary() + { + // Arrange + var expected = ImmutableDictionary.CreateRange(new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }); + CanDeserialize_Dictionary, ImmutableDictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_ImmutableSortedDictionary() + { + // Arrange + var expected = ImmutableSortedDictionary.CreateRange(new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }); + CanDeserialize_Dictionary, ImmutableSortedDictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_IImmutableDictionary() + { + // Arrange + var expected = ImmutableDictionary.CreateRange(new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }); + // Arrange + var collection = new Dictionary() + { + ["[0]"] = "10", + ["[1]"] = "11", + ["[2]"] = "12", + ["[3]"] = "13", + ["[4]"] = "14", + ["[5]"] = "15", + ["[6]"] = "16", + ["[7]"] = "17", + ["[8]"] = "18", + ["[9]"] = "19", + }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataSerializerOptions(); + + // Act + var result = FormDataDeserializer.Deserialize>(reader, options); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Equal(expected.Count, dictionary.Count); + Assert.Equal(expected.OrderBy(o => o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray()); + } + + [Fact] + public void CanDeserialize_Dictionary_IDictionary() + { + // Arrange + var expected = new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + CanDeserialize_Dictionary, Dictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_SortedList() + { + // Arrange + var expected = new SortedList() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + CanDeserialize_Dictionary, SortedList, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_SortedDictionary() + { + // Arrange + var expected = new SortedDictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + CanDeserialize_Dictionary, SortedDictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_IReadOnlyDictionary() + { + // Arrange + var expected = new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + var collection = new Dictionary() + { + ["[0]"] = "10", + ["[1]"] = "11", + ["[2]"] = "12", + ["[3]"] = "13", + ["[4]"] = "14", + ["[5]"] = "15", + ["[6]"] = "16", + ["[7]"] = "17", + ["[8]"] = "18", + ["[9]"] = "19", + }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataSerializerOptions(); + + // Act + var result = FormDataDeserializer.Deserialize>(reader, options); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Equal(expected.Count, dictionary.Count); + Assert.Equal(expected.OrderBy(o => o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray()); + } + + [Fact] + public void CanDeserialize_Dictionary_ReadOnlyDictionary() + { + // Arrange + var expected = new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + var collection = new Dictionary() + { + ["[0]"] = "10", + ["[1]"] = "11", + ["[2]"] = "12", + ["[3]"] = "13", + ["[4]"] = "14", + ["[5]"] = "15", + ["[6]"] = "16", + ["[7]"] = "17", + ["[8]"] = "18", + ["[9]"] = "19", + }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataSerializerOptions(); + + // Act + var result = FormDataDeserializer.Deserialize>(reader, options); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Equal(expected.Count, dictionary.Count); + Assert.Equal(expected.OrderBy(o => o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray()); + } + + [Fact] + public void Deserialize_EmptyDictionary_ReturnsNull() + { + // Arrange + var collection = new Dictionary() { }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataSerializerOptions(); + + // Act + var result = FormDataDeserializer.Deserialize>(reader, options); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Deserialize_Dictionary_RespectsMaxCollectionSize() + { + // Arrange + var collection = new Dictionary() { }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataSerializerOptions(); + + // Act + var result = FormDataDeserializer.Deserialize>(reader, options); + + // Assert + Assert.Null(result); + } + + private void CanDeserialize_Dictionary(TImplementation expected) + where TDictionary : IDictionary + where TImplementation : TDictionary + { + // Arrange + var collection = new Dictionary() + { + ["[0]"] = "10", + ["[1]"] = "11", + ["[2]"] = "12", + ["[3]"] = "13", + ["[4]"] = "14", + ["[5]"] = "15", + ["[6]"] = "16", + ["[7]"] = "17", + ["[8]"] = "18", + ["[9]"] = "19", + }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataSerializerOptions(); + + // Act + var result = CallDeserialize(reader, options, typeof(TDictionary)); + + // Assert + var dictionary = Assert.IsType(result); + Assert.Equal(expected.Count, dictionary.Count); + Assert.Equal(expected.OrderBy(o => o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray()); + } + [Fact] public void CanDeserialize_Collections_CustomCollection() { @@ -487,7 +694,7 @@ public void CanDeserialize_Collections_CustomCollection() var expected = new CustomCollection { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; CanDeserialize_Collection, CustomCollection, int>(expected); } - + private void CanDeserialize_Collection(TImplementation expected, bool sequenceEquals = false) { // Arrange From 3947c98e0ba61ab789dba40116ab3671d5c4bd88 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 30 May 2023 15:10:24 +0200 Subject: [PATCH 2/5] Cleanups --- .../DictionaryBufferAdapter.cs | 21 ++++ .../DictionaryStaticCastAdapter.cs | 18 +++ .../IDictionaryBufferAdapter.cs | 5 + .../ImmutableDictionaryBufferAdapter.cs | 36 ++++++ .../ImmutableSortedDictionaryBufferAdapter.cs | 21 ++++ .../ReadOnlyDictionaryBufferAdapter.cs | 47 +++++++ .../Binding/Converters/DictionaryConverter.cs | 2 +- .../TypedDictionaryConverterFactory.cs | 115 ------------------ 8 files changed, 149 insertions(+), 116 deletions(-) create mode 100644 src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs create mode 100644 src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs create mode 100644 src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs create mode 100644 src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs create mode 100644 src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs new file mode 100644 index 000000000000..852f669713d9 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs @@ -0,0 +1,21 @@ +// 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; + +// Uses a concrete type that implements IDictionary as a buffer. +internal class DictionaryBufferAdapter + : IDictionaryBufferAdapter + where TDictionaryType : IDictionary, new() + where TKey : IParsable +{ + public static TDictionaryType Add(ref TDictionaryType buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static TDictionaryType CreateBuffer() => new TDictionaryType(); + + public static TDictionaryType ToResult(TDictionaryType buffer) => buffer; +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs new file mode 100644 index 000000000000..f5843f331bca --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs @@ -0,0 +1,18 @@ +// 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; + +// Adapts a concrete dictionary type into an interface. +internal class DictionaryStaticCastAdapter + : IDictionaryBufferAdapter + where TDictionaryAdapter : IDictionaryBufferAdapter + where TDictionaryImplementation : TDictionaryInterface + where TKey : IParsable +{ + public static TBuffer CreateBuffer() => TDictionaryAdapter.CreateBuffer(); + + public static TBuffer Add(ref TBuffer buffer, TKey key, TValue element) => TDictionaryAdapter.Add(ref buffer, key, element); + + public static TDictionaryInterface ToResult(TBuffer buffer) => TDictionaryAdapter.ToResult(buffer); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs index ded44af82c49..e2b4bc6c8b89 100644 --- a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs @@ -3,6 +3,11 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; +// Interface for constructing a dictionary like instance using the +// dictionary converter. +// This interface abstracts over the different ways of constructing a dictionary. +// For example, Immutable types use a builder as a buffer, while other types +// use an instance of the dictionary itself as a buffer. internal interface IDictionaryBufferAdapter where TKey : IParsable { diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs new file mode 100644 index 000000000000..6226e46d5e3f --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal class ImmutableDictionaryBufferAdapter + : IDictionaryBufferAdapter, ImmutableDictionary.Builder, TKey, TValue> + where TKey : IParsable +{ + public static ImmutableDictionary.Builder Add(ref ImmutableDictionary.Builder buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static ImmutableDictionary.Builder CreateBuffer() => ImmutableDictionary.CreateBuilder(); + + public static ImmutableDictionary ToResult(ImmutableDictionary.Builder buffer) => buffer.ToImmutable(); + + internal static DictionaryConverter> CreateInterfaceConverter(FormDataConverter valueTypeConverter) + { + return new DictionaryConverter, + DictionaryStaticCastAdapter< + IImmutableDictionary, + ImmutableDictionary, + ImmutableDictionaryBufferAdapter, + ImmutableDictionary.Builder, + TKey, + TValue>, + ImmutableDictionary.Builder, + TKey, + TValue>(valueTypeConverter); + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs new file mode 100644 index 000000000000..e0818337adde --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal class ImmutableSortedDictionaryBufferAdapter + : IDictionaryBufferAdapter, ImmutableSortedDictionary.Builder, TKey, TValue> + where TKey : IParsable +{ + public static ImmutableSortedDictionary.Builder Add(ref ImmutableSortedDictionary.Builder buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static ImmutableSortedDictionary.Builder CreateBuffer() => ImmutableSortedDictionary.CreateBuilder(); + + public static ImmutableSortedDictionary ToResult(ImmutableSortedDictionary.Builder buffer) => buffer.ToImmutable(); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs new file mode 100644 index 000000000000..0c400442967d --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.ObjectModel; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal class ReadOnlyDictionaryBufferAdapter + : IDictionaryBufferAdapter, Dictionary, TKey, TValue> + where TKey : IParsable +{ + public static Dictionary Add(ref Dictionary buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static Dictionary CreateBuffer() => + new Dictionary(); + + public static ReadOnlyDictionary ToResult(Dictionary buffer) => + new ReadOnlyDictionary(buffer); + + internal static DictionaryConverter> CreateInterfaceConverter(FormDataConverter valueTypeConverter) + { + return new DictionaryConverter, + DictionaryStaticCastAdapter< + IReadOnlyDictionary, + ReadOnlyDictionary, + ReadOnlyDictionaryBufferAdapter, + Dictionary, + TKey, + TValue>, + Dictionary, + TKey, + TValue>(valueTypeConverter); + } + + internal static DictionaryConverter> CreateConverter(FormDataConverter valueTypeConverter) + { + return new DictionaryConverter, + ReadOnlyDictionaryBufferAdapter, + Dictionary, + TKey, + TValue>(valueTypeConverter); + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs index 57bd6b96e45b..774bf6b89bf7 100644 --- a/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs @@ -68,7 +68,7 @@ internal override bool TryRead( TDictionaryPolicy.Add(ref buffer, keyValue!, currentValue); keyCount++; - if (keyCount > maxCollectionSize) + if (keyCount == maxCollectionSize) { succeded = false; break; diff --git a/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs index 7e61400cb592..88d76bac4541 100644 --- a/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs @@ -151,118 +151,3 @@ var _ when type.IsAssignableTo(typeof(IDictionary)) && type.GetCon throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); } } - -internal class DictionaryBufferAdapter - : IDictionaryBufferAdapter - where TDictionaryType : IDictionary, new() - where TKey : IParsable -{ - public static TDictionaryType Add(ref TDictionaryType buffer, TKey key, TValue value) - { - buffer.Add(key, value); - return buffer; - } - - public static TDictionaryType CreateBuffer() => new TDictionaryType(); - - public static TDictionaryType ToResult(TDictionaryType buffer) => buffer; -} - -internal class ReadOnlyDictionaryBufferAdapter - : IDictionaryBufferAdapter, Dictionary, TKey, TValue> - where TKey : IParsable -{ - public static Dictionary Add(ref Dictionary buffer, TKey key, TValue value) - { - buffer.Add(key, value); - return buffer; - } - - public static Dictionary CreateBuffer() => - new Dictionary(); - - public static ReadOnlyDictionary ToResult(Dictionary buffer) => - new ReadOnlyDictionary(buffer); - - internal static DictionaryConverter> CreateInterfaceConverter(FormDataConverter valueTypeConverter) - { - return new DictionaryConverter, - DictionaryStaticCastAdapter< - IReadOnlyDictionary, - ReadOnlyDictionary, - ReadOnlyDictionaryBufferAdapter, - Dictionary, - TKey, - TValue>, - Dictionary, - TKey, - TValue>(valueTypeConverter); - } - - internal static DictionaryConverter> CreateConverter(FormDataConverter valueTypeConverter) - { - return new DictionaryConverter, - ReadOnlyDictionaryBufferAdapter, - Dictionary, - TKey, - TValue>(valueTypeConverter); - } -} - -internal class ImmutableDictionaryBufferAdapter - : IDictionaryBufferAdapter, ImmutableDictionary.Builder, TKey, TValue> - where TKey : IParsable -{ - public static ImmutableDictionary.Builder Add(ref ImmutableDictionary.Builder buffer, TKey key, TValue value) - { - buffer.Add(key, value); - return buffer; - } - - public static ImmutableDictionary.Builder CreateBuffer() => ImmutableDictionary.CreateBuilder(); - - public static ImmutableDictionary ToResult(ImmutableDictionary.Builder buffer) => buffer.ToImmutable(); - - internal static DictionaryConverter> CreateInterfaceConverter(FormDataConverter valueTypeConverter) - { - return new DictionaryConverter, - DictionaryStaticCastAdapter< - IImmutableDictionary, - ImmutableDictionary, - ImmutableDictionaryBufferAdapter, - ImmutableDictionary.Builder, - TKey, - TValue>, - ImmutableDictionary.Builder, - TKey, - TValue>(valueTypeConverter); - } -} - -internal class ImmutableSortedDictionaryBufferAdapter - : IDictionaryBufferAdapter, ImmutableSortedDictionary.Builder, TKey, TValue> - where TKey : IParsable -{ - public static ImmutableSortedDictionary.Builder Add(ref ImmutableSortedDictionary.Builder buffer, TKey key, TValue value) - { - buffer.Add(key, value); - return buffer; - } - - public static ImmutableSortedDictionary.Builder CreateBuffer() => ImmutableSortedDictionary.CreateBuilder(); - - public static ImmutableSortedDictionary ToResult(ImmutableSortedDictionary.Builder buffer) => buffer.ToImmutable(); -} - -internal class DictionaryStaticCastAdapter - : IDictionaryBufferAdapter - where TDictionaryAdapter : IDictionaryBufferAdapter - where TDictionaryImplementation : TDictionaryInterface - where TKey : IParsable -{ - public static TBuffer CreateBuffer() => TDictionaryAdapter.CreateBuffer(); - - public static TBuffer Add(ref TBuffer buffer, TKey key, TValue element) => TDictionaryAdapter.Add(ref buffer, key, element); - - public static TDictionaryInterface ToResult(TBuffer buffer) => TDictionaryAdapter.ToResult(buffer); -} From fcdc1bf193b7b6afef41a751e035d9cee3d85b37 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Tue, 30 May 2023 15:12:18 +0200 Subject: [PATCH 3/5] Tests --- .../Endpoints/test/Binding/FormDataMapperTests.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs index 359542905430..57d8e3f39172 100644 --- a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs +++ b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs @@ -4,11 +4,11 @@ using System.Buffers; using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Xml.Linq; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Components.Endpoints.Binding; @@ -480,6 +480,14 @@ public void CanDeserialize_Collections_IImmutableStack() CanDeserialize_Collection, ImmutableStack, int>(expected); } + [Fact] + public void CanDeserialize_Collections_CustomCollection() + { + // Arrange + var expected = new CustomCollection { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, CustomCollection, int>(expected); + } + [Fact] public void CanDeserialize_Dictionary_Dictionary() { From d1f83623f8fa7f06b595f485c1292837fce9eeed Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 1 Jun 2023 14:39:44 +0200 Subject: [PATCH 4/5] Cleanups --- .../DictionaryBufferAdapter.cs | 2 +- .../DictionaryStaticCastAdapter.cs | 2 +- .../ImmutableDictionaryBufferAdapter.cs | 4 ++-- .../ImmutableSortedDictionaryBufferAdapter.cs | 4 ++-- .../ReadOnlyDictionaryBufferAdapter.cs | 4 ++-- .../Binding/Converters/DictionaryConverter.cs | 4 ++-- .../ConcreteTypeCollectionConverterFactory.cs | 4 ++-- .../ConcreteTypeDictionaryConverterFactory.cs | 6 ++--- .../TypedDictionaryConverterFactory.cs | 6 ++--- .../Factories/DictionaryConverterFactory.cs | 4 ++-- .../test/Binding/FormDataMapperTests.cs | 23 +++++++++---------- 11 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs index 852f669713d9..0e7939018f10 100644 --- a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; // Uses a concrete type that implements IDictionary as a buffer. -internal class DictionaryBufferAdapter +internal sealed class DictionaryBufferAdapter : IDictionaryBufferAdapter where TDictionaryType : IDictionary, new() where TKey : IParsable diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs index f5843f331bca..6e722d2be90d 100644 --- a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; // Adapts a concrete dictionary type into an interface. -internal class DictionaryStaticCastAdapter +internal sealed class DictionaryStaticCastAdapter : IDictionaryBufferAdapter where TDictionaryAdapter : IDictionaryBufferAdapter where TDictionaryImplementation : TDictionaryInterface diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs index 6226e46d5e3f..4e42ad552e0b 100644 --- a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs @@ -1,11 +1,11 @@ -// 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. using System.Collections.Immutable; namespace Microsoft.AspNetCore.Components.Endpoints.Binding; -internal class ImmutableDictionaryBufferAdapter +internal sealed class ImmutableDictionaryBufferAdapter : IDictionaryBufferAdapter, ImmutableDictionary.Builder, TKey, TValue> where TKey : IParsable { diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs index e0818337adde..1ebde40f6b9f 100644 --- a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs @@ -1,11 +1,11 @@ -// 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. using System.Collections.Immutable; namespace Microsoft.AspNetCore.Components.Endpoints.Binding; -internal class ImmutableSortedDictionaryBufferAdapter +internal sealed class ImmutableSortedDictionaryBufferAdapter : IDictionaryBufferAdapter, ImmutableSortedDictionary.Builder, TKey, TValue> where TKey : IParsable { diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs index 0c400442967d..6c5215a7caa9 100644 --- a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs @@ -1,11 +1,11 @@ -// 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. using System.Collections.ObjectModel; namespace Microsoft.AspNetCore.Components.Endpoints.Binding; -internal class ReadOnlyDictionaryBufferAdapter +internal sealed class ReadOnlyDictionaryBufferAdapter : IDictionaryBufferAdapter, Dictionary, TKey, TValue> where TKey : IParsable { diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs index 774bf6b89bf7..cc93af69dbb5 100644 --- a/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs @@ -9,7 +9,7 @@ internal abstract class DictionaryConverter : FormDataConverter : DictionaryConverter +internal sealed class DictionaryConverter : DictionaryConverter where TKey : IParsable where TDictionaryPolicy : IDictionaryBufferAdapter { @@ -25,7 +25,7 @@ public DictionaryConverter(FormDataConverter elementConverter) internal override bool TryRead( ref FormDataReader context, Type type, - FormDataSerializerOptions options, + FormDataMapperOptions options, [NotNullWhen(true)] out TDictionary? result, out bool found) { diff --git a/src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs index ce6e30ba0c48..667a76ffe3c2 100644 --- a/src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs @@ -9,9 +9,9 @@ internal class ConcreteTypeCollectionConverterFactory public static readonly ConcreteTypeCollectionConverterFactory Instance = new(); - public bool CanConvert(Type _, FormDataSerializerOptions options) => true; + public bool CanConvert(Type _, FormDataMapperOptions options) => true; - public FormDataConverter CreateConverter(Type _, FormDataSerializerOptions options) + public FormDataConverter CreateConverter(Type _, FormDataMapperOptions options) { // Resolve the element type converter var elementTypeConverter = options.ResolveConverter() ?? diff --git a/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs index 6c18de929333..a3f77d3600c6 100644 --- a/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs @@ -3,14 +3,14 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; -internal class ConcreteTypeDictionaryConverterFactory : IFormDataConverterFactory +internal sealed class ConcreteTypeDictionaryConverterFactory : IFormDataConverterFactory where TKey : IParsable { public static readonly ConcreteTypeDictionaryConverterFactory Instance = new(); - public bool CanConvert(Type type, FormDataSerializerOptions options) => true; + public bool CanConvert(Type type, FormDataMapperOptions options) => true; - public FormDataConverter CreateConverter(Type type, FormDataSerializerOptions options) + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) { // Resolve the element type converter var keyConverter = options.ResolveConverter() ?? diff --git a/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs index 88d76bac4541..a2822b4b9364 100644 --- a/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs @@ -7,10 +7,10 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding; -internal class TypedDictionaryConverterFactory : IFormDataConverterFactory +internal sealed class TypedDictionaryConverterFactory : IFormDataConverterFactory where TKey : IParsable { - public bool CanConvert(Type type, FormDataSerializerOptions options) + public bool CanConvert(Type type, FormDataMapperOptions options) { // Resolve the value type converter var valueTypeConverter = options.ResolveConverter(); @@ -70,7 +70,7 @@ var _ when type.IsAssignableTo(typeof(IDictionary)) && type.GetCon return false; } - public FormDataConverter CreateConverter(Type type, FormDataSerializerOptions options) + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) { // Resolve the value type converter var valueTypeConverter = options.ResolveConverter(); diff --git a/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs index caa68c14d2cc..698d8182c775 100644 --- a/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs @@ -9,7 +9,7 @@ internal class DictionaryConverterFactory : IFormDataConverterFactory { internal static readonly DictionaryConverterFactory Instance = new(); - public bool CanConvert(Type type, FormDataSerializerOptions options) + public bool CanConvert(Type type, FormDataMapperOptions options) { // Well-known dictionary types // IDictionary @@ -68,7 +68,7 @@ public bool CanConvert(Type type, FormDataSerializerOptions options) return factory.CanConvert(type, options); } - public FormDataConverter CreateConverter(Type type, FormDataSerializerOptions options) + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) { // Type must implement IDictionary IReadOnlyDictionary // Note that IDictionary doesn't extend IReadOnlyDictionary, hence the need for two checks diff --git a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs index 57d8e3f39172..d4fa889cf8b6 100644 --- a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs +++ b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -540,10 +539,10 @@ public void CanDeserialize_Dictionary_IImmutableDictionary() ["[9]"] = "19", }; var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); - var options = new FormDataSerializerOptions(); + var options = new FormDataMapperOptions(); // Act - var result = FormDataDeserializer.Deserialize>(reader, options); + var result = FormDataMapper.Map>(reader, options); // Assert var dictionary = Assert.IsType>(result); @@ -594,10 +593,10 @@ public void CanDeserialize_Dictionary_IReadOnlyDictionary() ["[9]"] = "19", }; var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); - var options = new FormDataSerializerOptions(); + var options = new FormDataMapperOptions(); // Act - var result = FormDataDeserializer.Deserialize>(reader, options); + var result = FormDataMapper.Map>(reader, options); // Assert var dictionary = Assert.IsType>(result); @@ -624,10 +623,10 @@ public void CanDeserialize_Dictionary_ReadOnlyDictionary() ["[9]"] = "19", }; var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); - var options = new FormDataSerializerOptions(); + var options = new FormDataMapperOptions(); // Act - var result = FormDataDeserializer.Deserialize>(reader, options); + var result = FormDataMapper.Map>(reader, options); // Assert var dictionary = Assert.IsType>(result); @@ -641,10 +640,10 @@ public void Deserialize_EmptyDictionary_ReturnsNull() // Arrange var collection = new Dictionary() { }; var reader = new FormDataReader(collection, 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.Null(result); @@ -656,10 +655,10 @@ public void Deserialize_Dictionary_RespectsMaxCollectionSize() // Arrange var collection = new Dictionary() { }; var reader = new FormDataReader(collection, 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.Null(result); @@ -684,7 +683,7 @@ private void CanDeserialize_Dictionary Date: Thu, 1 Jun 2023 16:22:18 +0200 Subject: [PATCH 5/5] Cleanups, fix build --- .../Binding/Factories/DictionaryConverterFactory.cs | 10 ---------- .../Endpoints/test/Binding/FormDataMapperTests.cs | 8 -------- 2 files changed, 18 deletions(-) diff --git a/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs index 698d8182c775..ee92552e357d 100644 --- a/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs +++ b/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs @@ -11,16 +11,6 @@ internal class DictionaryConverterFactory : IFormDataConverterFactory public bool CanConvert(Type type, FormDataMapperOptions options) { - // Well-known dictionary types - // IDictionary - // IReadOnlyDictionary - // Dictionary - // SortedDictionary - // SortedList - // ConcurrentDictionary - // ImmutableDictionary - // ImmutableSortedDictionary - // Type must implement IDictionary IReadOnlyDictionary // Note that IDictionary doesn't extend IReadOnlyDictionary, hence the need for two checks var dictionaryType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IDictionary<,>)) ?? diff --git a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs index d4fa889cf8b6..765e7677045f 100644 --- a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs +++ b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs @@ -694,14 +694,6 @@ private void CanDeserialize_Dictionary o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray()); } - [Fact] - public void CanDeserialize_Collections_CustomCollection() - { - // Arrange - var expected = new CustomCollection { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; - CanDeserialize_Collection, CustomCollection, int>(expected); - } - private void CanDeserialize_Collection(TImplementation expected, bool sequenceEquals = false) { // Arrange