File: Binding\FormDataMapperTests.cs
Web Access
Project: src\src\Components\Endpoints\test\Microsoft.AspNetCore.Components.Endpoints.Tests.csproj (Microsoft.AspNetCore.Components.Endpoints.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.Serialization;
using System.Text;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
 
public class FormDataMapperTests
{
    [Theory]
    [MemberData(nameof(PrimitiveTypesData))]
    public void CanDeserialize_PrimitiveTypes(string value, Type type, object expected)
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues(value) };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, type);
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Theory]
    [InlineData("Red", Colors.Red)]
    [InlineData("RED", Colors.Red)]
    [InlineData("BlUe", Colors.Blue)]
    [InlineData("green", Colors.Green)]
    public void CanDeserialize_EnumTypes(string value, Colors expected)
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues(value) };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(Colors));
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Theory]
    [InlineData("Red", Colors.Red)]
    [InlineData("RED", Colors.Red)]
    [InlineData("BlUe", Colors.Blue)]
    [InlineData("green", Colors.Green)]
    public void CanDeserialize_NullableEnumTypes(string value, Colors expected)
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues(value) };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(Colors?));
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    private FormDataReader CreateFormDataReader(Dictionary<string, StringValues> collection, CultureInfo invariantCulture, IFormFileCollection formFileCollection = null)
    {
        var dictionary = new Dictionary<FormKey, StringValues>(collection.Count);
        foreach (var kvp in collection)
        {
            dictionary.Add(new FormKey(kvp.Key.AsMemory()), kvp.Value);
        }
        return formFileCollection is null
            ? new FormDataReader(dictionary, CultureInfo.InvariantCulture, new char[2048])
            : new FormDataReader(dictionary, CultureInfo.InvariantCulture, new char[2048], formFileCollection);
    }
 
    [Theory]
    [MemberData(nameof(PrimitiveTypesData))]
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
    public void PrimitiveTypes_MissingValues_DoNotAddErrors(string _, Type type, object __)
#pragma warning restore xUnit1026 // Theory methods should use all of their parameters
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, type);
 
        // Assert
        Assert.Equal(type.IsValueType ? Activator.CreateInstance(type) : null, result);
        Assert.Empty(errors);
    }
 
    [Fact]
    public void Throws_ForInvalidValues()
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues("abc") };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act & Assert
        var exception = Assert.Throws<FormDataMappingException>(() => FormDataMapper.Map<int>(reader, options));
        Assert.NotNull(exception?.Error);
        Assert.Equal("value", exception.Error.Key);
        Assert.Equal("The value 'abc' is not valid for 'value'.", exception.Error.Message.ToString(reader.Culture));
        Assert.Equal("abc", exception.Error.Value);
    }
 
    [Fact]
    public void CanCollectErrors_WithCustomHandler_ForInvalidValues()
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues("abc") };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
 
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act & Assert
        var result = FormDataMapper.Map<int>(reader, options);
        Assert.Equal(default, result);
        var error = Assert.Single(errors);
        Assert.NotNull(error);
        Assert.Equal("value", error.Key);
        Assert.Equal("The value 'abc' is not valid for 'value'.", error.Message.ToString(reader.Culture));
        Assert.Equal("abc", error.Value);
    }
 
    [Theory]
    [MemberData(nameof(NullableBasicTypes))]
    public void CanDeserialize_NullablePrimitiveTypes(string value, Type type, object expected)
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues(value) };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, type);
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Theory]
    [MemberData(nameof(NullNullableBasicTypes))]
    public void CanDeserialize_NullValues(Type type)
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, type);
 
        // Assert
        Assert.Null(result);
    }
 
    [Theory]
    [MemberData(nameof(UriTestData))]
    public void CanDeserialize_Uri(string value, Uri expected)
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues(value) };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<Uri>(reader, options);
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Theory]
    [MemberData(nameof(UriTestData))]
    public void CanDeserialize_ComplexTypes_WithUriProperties(string value, Uri expected)
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { ["value.Slug"] = new StringValues(value) };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<TypeWithUri>(reader, options);
 
        // Assert
        Assert.NotNull(result);
        Assert.Equal(expected, result.Slug);
    }
 
    public static TheoryData<string, Uri> UriTestData => new TheoryData<string, Uri>
    {
        { "http://www.example.com", new Uri("http://www.example.com") },
        { "http://www.example.com/path", new Uri("http://www.example.com/path") },
        { "http://www.example.com/path/", new Uri("http://www.example.com/path/") },
        { "/path", new Uri("/path", UriKind.Relative) },
    };
 
    [Fact]
    public void CanDeserialize_CustomParsableTypes()
    {
        // Arrange
        var expected = new Point { X = 1, Y = 1 };
        var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues("(1,1)") };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<Point>(reader, options);
 
        // Assert
        Assert.Equal(expected, result);
    }
 
#nullable enable
    [Fact]
    public void CanDeserialize_NullableCustomParsableTypes()
    {
        // Arrange
        var expected = new ValuePoint { X = 1, Y = 1 };
        var collection = new Dictionary<string, StringValues>() { ["value"] = new StringValues("(1,1)") };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<ValuePoint?>(reader, options);
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void CanDeserialize_NullableCustomParsableTypes_NullValue()
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>() { };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<ValuePoint?>(reader, options);
 
        // Assert
        Assert.Null(result);
    }
#nullable disable
 
    [Fact]
    public void Deserialize_Collections_NoElements_ReturnsNull()
    {
        // Arrange
        var data = new Dictionary<string, StringValues>() { };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        reader.PushPrefix("value");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<List<int>>(reader, options);
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void Deserialize_Collections_SingleElement_ReturnsCollection()
    {
        // Arrange
        var data = new Dictionary<string, StringValues>() { ["[0]"] = "10" };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<List<int>>(reader, options);
 
        // Assert
        var value = Assert.Single(result);
        Assert.Equal(10, value);
    }
 
    [Fact]
    public void Deserialize_Collections_SupportsMultipleElementsPerKey_ForSingleValueElementTypes_ParsableTypes()
    {
        // Arrange
        var data = new Dictionary<string, StringValues>() { ["values"] = new StringValues(new[] { "10", "11" }) };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        reader.PushPrefix("values");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<List<int>>(reader, options);
 
        // Assert
        Assert.Collection(result,
            v => Assert.Equal(10, v),
            v => Assert.Equal(11, v));
    }
 
    [Fact]
    public void Deserialize_Collections_SupportsMultipleElementsPerKey_ForSingleValueElementTypes_EnumTypes()
    {
        // Arrange
        var data = new Dictionary<string, StringValues>() { ["values"] = new StringValues(new[] { "Red", "Blue" }) };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        reader.PushPrefix("values");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<List<Colors>>(reader, options);
 
        // Assert
        Assert.Collection(result,
            v => Assert.Equal(Colors.Red, v),
            v => Assert.Equal(Colors.Blue, v));
    }
 
    [Fact]
    public void Deserialize_Collections_SupportsMultipleElementsPerKey_ForSingleValueElementTypes_Nullable_WhenUnderlyingElementIsSingleValue()
    {
        // Arrange
        var data = new Dictionary<string, StringValues>() { ["values"] = new StringValues(new[] { "Red", "Blue" }) };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        reader.PushPrefix("values");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<List<Colors?>>(reader, options);
 
        // Assert
        Assert.Collection(result,
            v => Assert.Equal(Colors.Red, v),
            v => Assert.Equal(Colors.Blue, v));
    }
 
    [Fact]
    public void Deserialize_Collections_MultipleElementsPerKey_CanReportErrors()
    {
        // Arrange
        var data = new Dictionary<string, StringValues>() { ["values"] = new StringValues(new[] { "10", "a" }) };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        reader.PushPrefix("values");
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
 
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<List<int>>(reader, options);
 
        // Assert
        Assert.Equal(10, Assert.Single(result));
        var error = Assert.Single(errors);
        Assert.Equal("values", error.Key);
        Assert.Equal("The value 'a' is not valid for 'values'.", error.Message.ToString(CultureInfo.InvariantCulture));
    }
 
    [Fact]
    public void Deserialize_Collections_MultipleElementsPerKey_ContinuesProcessingValuesAfterErrors()
    {
        // Arrange
        var data = new Dictionary<string, StringValues>() { ["values"] = new StringValues(new[] { "10", "a", "11" }) };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        reader.PushPrefix("values");
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
 
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<List<int>>(reader, options);
 
        // Assert
        Assert.Collection(result,
            v => Assert.Equal(10, v),
            v => Assert.Equal(11, v));
        var error = Assert.Single(errors);
        Assert.Equal("values", error.Key);
        Assert.Equal("The value 'a' is not valid for 'values'.", error.Message.ToString(CultureInfo.InvariantCulture));
    }
 
    [Theory]
    [InlineData(99)]
    [InlineData(100)]
    [InlineData(101)]
    public void Deserialize_Collections_HandlesComputedIndexesBoundaryCorrectly(int size)
    {
        // Arrange
        var data = new Dictionary<string, StringValues>(Enumerable.Range(0, size)
            .Select(i => new KeyValuePair<string, StringValues>(
                $"[{i.ToString(CultureInfo.InvariantCulture)}]",
                (i + 10).ToString(CultureInfo.InvariantCulture))));
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions
        {
            MaxCollectionSize = 110
        };
 
        // Act
        var result = FormDataMapper.Map<List<int>>(reader, options);
 
        // Assert
        Assert.Equal(size, result.Count);
    }
 
    [Theory]
    [InlineData(99)]
    [InlineData(100)]
    [InlineData(101)]
    [InlineData(109)]
    [InlineData(110)]
    [InlineData(120)]
    public void Deserialize_Collections_PoolsArraysCorrectly(int size)
    {
        // Arrange
        var rented = new List<int[]>();
        var returned = new List<int[]>();
 
        var data = new Dictionary<string, StringValues>(Enumerable.Range(0, size)
            .Select(i => new KeyValuePair<string, StringValues>(
                $"[{i.ToString(CultureInfo.InvariantCulture)}]",
                (i + 10).ToString(CultureInfo.InvariantCulture))));
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions
        {
            MaxCollectionSize = 140
        };
 
        var converter = new CollectionConverter<
                int[],
                TestArrayPoolBufferAdapter,
                TestArrayPoolBufferAdapter.PooledBuffer,
                int>(new ParsableConverter<int>());
 
        options.AddConverter(converter);
 
        TestArrayPoolBufferAdapter.OnRent += rented.Add;
        TestArrayPoolBufferAdapter.OnReturn += returned.Add;
 
        // Act
        var result = FormDataMapper.Map<int[]>(reader, options);
 
        TestArrayPoolBufferAdapter.OnRent -= rented.Add;
        TestArrayPoolBufferAdapter.OnReturn -= returned.Add;
 
        // Assert
        Assert.Equal(rented.Count, returned.Count);
    }
 
    [Theory]
    [InlineData(2)]
    [InlineData(99)]
    [InlineData(100)]
    [InlineData(101)]
    [InlineData(109)]
    [InlineData(110)]
    [InlineData(120)]
    public void Deserialize_Collections_AlwaysReturnsBuffer(int size)
    {
        // Arrange
        var rented = new List<int[]>();
        var returned = new List<int[]>();
 
        var data = new Dictionary<string, StringValues>(Enumerable.Range(0, size)
            .Select(i => new KeyValuePair<string, StringValues>(
                $"[{i.ToString(CultureInfo.InvariantCulture)}]",
                (i + 10).ToString(CultureInfo.InvariantCulture))));
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions
        {
            MaxCollectionSize = 140
        };
 
        var elementConverter = new ThrowingConverter();
        elementConverter.OnTryReadDelegate = (ref FormDataReader context, Type type, FormDataMapperOptions options, out int result, out bool found) =>
        {
            context.TryGetValue(out var value);
            var index = int.Parse(value, CultureInfo.InvariantCulture) - 10;
            if (index + 1 == size)
            {
                throw new InvalidOperationException("Can't parse this!");
            }
            result = default;
            found = true;
            return false;
        };
 
        var converter = new CollectionConverter<
                int[],
                TestArrayPoolBufferAdapter,
                TestArrayPoolBufferAdapter.PooledBuffer,
                int>(elementConverter);
 
        options.AddConverter(converter);
 
        TestArrayPoolBufferAdapter.OnRent += rented.Add;
        TestArrayPoolBufferAdapter.OnReturn += returned.Add;
 
        // Act
        var result = Assert.Throws<InvalidOperationException>(() => FormDataMapper.Map<int[]>(reader, options));
 
        TestArrayPoolBufferAdapter.OnRent -= rented.Add;
        TestArrayPoolBufferAdapter.OnReturn -= returned.Add;
 
        // Assert
        Assert.Equal(rented.Count, returned.Count);
    }
 
    [Theory]
    [InlineData(1, 2)]
    [InlineData(2, 2)]
    [InlineData(3, 2)]
    [InlineData(49, 50)]
    [InlineData(50, 50)]
    [InlineData(51, 50)]
    [InlineData(60, 50)]
    [InlineData(109, 110)]
    [InlineData(110, 110)]
    [InlineData(111, 110)]
    [InlineData(120, 110)]
    public void Deserialize_Collections_RespectsMaxCollectionSize(int size, int maxCollectionSize)
    {
        // Arrange
        var data = new Dictionary<string, StringValues>(Enumerable.Range(0, size)
            .Select(i => new KeyValuePair<string, StringValues>(
                $"[{i.ToString(CultureInfo.InvariantCulture)}]",
                (i + 10).ToString(CultureInfo.InvariantCulture))));
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
 
        var options = new FormDataMapperOptions
        {
            MaxCollectionSize = maxCollectionSize
        };
 
        // Act
        var result = FormDataMapper.Map<List<int>>(reader, options);
 
        // Assert
        Assert.True(result.Count == maxCollectionSize || result.Count == maxCollectionSize - 1);
        if (size > maxCollectionSize)
        {
            var error = Assert.Single(errors);
            Assert.Equal("", error.Key);
            Assert.Equal($"The number of elements in the collection exceeded the maximum number of '{maxCollectionSize}' elements allowed.", error.Message.ToString(reader.Culture));
            Assert.Null(error.Value);
        }
        else
        {
            Assert.Empty(errors);
        }
    }
 
    [Fact]
    public void Deserialize_Collections_ContinuesParsingAfterErrors()
    {
        // Arrange
        var expected = new List<int> { 0, 11, 12, 13, 0, 15, 16, 17, 18, 19 };
        var collection = new Dictionary<string, StringValues>()
        {
            ["[0]"] = "abc",
            ["[1]"] = "11",
            ["[2]"] = "12",
            ["[3]"] = "13",
            ["[4]"] = "def",
            ["[5]"] = "15",
            ["[6]"] = "16",
            ["[7]"] = "17",
            ["[8]"] = "18",
            ["[9]"] = "19",
        };
 
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<List<int>>(reader, options);
 
        // Assert
        var list = Assert.IsType<List<int>>(result);
        Assert.Equal(expected, list);
        Assert.Equal(2, errors.Count);
        Assert.Collection(errors,
            e =>
            {
                Assert.Equal("[0]", e.Key);
                Assert.Equal("The value 'abc' is not valid for '0'.", e.Message.ToString(reader.Culture));
                Assert.Equal("abc", e.Value);
            },
            e =>
            {
                Assert.Equal("[4]", e.Key);
                Assert.Equal("The value 'def' is not valid for '4'.", e.Message.ToString(reader.Culture));
                Assert.Equal("def", e.Value);
            });
    }
 
    [Fact]
    public void CanDeserialize_Collections_IReadOnlySet()
    {
        // Arrange
        var expected = new HashSet<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<IReadOnlySet<int>, HashSet<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_IReadOnlyListOfT()
    {
        // Arrange
        var expected = new List<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<IReadOnlyList<int>, List<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_IReadOnlyCollection()
    {
        // Arrange
        var expected = new List<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<IReadOnlyCollection<int>, List<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ISet()
    {
        // Arrange
        var expected = new HashSet<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<ISet<int>, HashSet<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_IListOfT()
    {
        // Arrange
        var expected = new List<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<IList<int>, List<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ICollectionOfT()
    {
        // Arrange
        var expected = new List<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<ICollection<int>, List<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_IEnumerableOfT()
    {
        // Arrange
        var expected = new List<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<IEnumerable<int>, List<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ArrayOfT()
    {
        // Arrange
        var expected = new int[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<int[], int[], int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_SortedSet()
    {
        // Arrange
        var expected = new SortedSet<int>(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<SortedSet<int>, SortedSet<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_HashSet()
    {
        // Arrange
        var expected = new HashSet<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<HashSet<int>, HashSet<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ReadOnlyCollectionOfT()
    {
        // Arrange
        var expected = new ReadOnlyCollection<int>(new List<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ReadOnlyCollection<int>, ReadOnlyCollection<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_CollectionOfT()
    {
        // Arrange
        var expected = new Collection<int>(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<Collection<int>, Collection<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ListOfT()
    {
        // Arrange
        var expected = new List<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<List<int>, List<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_LinkedList()
    {
        // Arrange
        var expected = new LinkedList<int>(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<LinkedList<int>, LinkedList<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_Queue()
    {
        // Arrange
        var expected = new Queue<int>(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<Queue<int>, Queue<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_Stack()
    {
        // Arrange
        var expected = new Stack<int>(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<Stack<int>, Stack<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ConcurrentBag()
    {
        // Arrange
        var expected = new ConcurrentBag<int>(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ConcurrentBag<int>, ConcurrentBag<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ConcurrentQueue()
    {
        // Arrange
        var expected = new ConcurrentQueue<int>(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ConcurrentQueue<int>, ConcurrentQueue<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ConcurrentStack()
    {
        // Arrange
        var expected = new ConcurrentStack<int>(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ConcurrentStack<int>, ConcurrentStack<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ImmutableArray()
    {
        // Arrange
        var expected = ImmutableArray.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ImmutableArray<int>, ImmutableArray<int>, int>(expected, sequenceEquals: true);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ImmutableList()
    {
        // Arrange
        var expected = ImmutableList.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ImmutableList<int>, ImmutableList<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ImmutableHashSet()
    {
        // Arrange
        var expected = ImmutableHashSet.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ImmutableHashSet<int>, ImmutableHashSet<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ImmutableSortedSet()
    {
        // Arrange
        var expected = ImmutableSortedSet.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ImmutableSortedSet<int>, ImmutableSortedSet<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ImmutableQueue()
    {
        // Arrange
        var expected = ImmutableQueue.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ImmutableQueue<int>, ImmutableQueue<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_ImmutableStack()
    {
        // Arrange
        var expected = ImmutableStack.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<ImmutableStack<int>, ImmutableStack<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_IImmutableList()
    {
        // Arrange
        var expected = ImmutableList.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<IImmutableList<int>, ImmutableList<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_IImmutableSet()
    {
        // Arrange
        var expected = ImmutableHashSet.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<IImmutableSet<int>, ImmutableHashSet<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_IImmutableQueue()
    {
        // Arrange
        var expected = ImmutableQueue.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<IImmutableQueue<int>, ImmutableQueue<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_IImmutableStack()
    {
        // Arrange
        var expected = ImmutableStack.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
        CanDeserialize_Collection<IImmutableStack<int>, ImmutableStack<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Collections_CustomCollection()
    {
        // Arrange
        var expected = new CustomCollection<int> { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
        CanDeserialize_Collection<CustomCollection<int>, CustomCollection<int>, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Dictionary_Dictionary()
    {
        // Arrange
        var expected = new Dictionary<int, int>() { [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>, Dictionary<int, int>, int, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Dictionary_ConcurrentDictionary()
    {
        // Arrange
        var expected = new ConcurrentDictionary<int, int>(new Dictionary<int, int>() { [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>, ConcurrentDictionary<int, int>, int, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Dictionary_ImmutableDictionary()
    {
        // Arrange
        var expected = ImmutableDictionary.CreateRange(new Dictionary<int, int>() { [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>, ImmutableDictionary<int, int>, int, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Dictionary_ImmutableSortedDictionary()
    {
        // Arrange
        var expected = ImmutableSortedDictionary.CreateRange(new Dictionary<int, int>() { [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>, ImmutableSortedDictionary<int, int>, int, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Dictionary_IImmutableDictionary()
    {
        // Arrange
        var expected = ImmutableDictionary.CreateRange(new Dictionary<int, int>() { [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<string, StringValues>()
        {
            ["[0]"] = "10",
            ["[1]"] = "11",
            ["[2]"] = "12",
            ["[3]"] = "13",
            ["[4]"] = "14",
            ["[5]"] = "15",
            ["[6]"] = "16",
            ["[7]"] = "17",
            ["[8]"] = "18",
            ["[9]"] = "19",
        };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<IImmutableDictionary<int, int>>(reader, options);
 
        // Assert
        var dictionary = Assert.IsType<ImmutableDictionary<int, int>>(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<int, int>() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, };
        CanDeserialize_Dictionary<IDictionary<int, int>, Dictionary<int, int>, int, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Dictionary_SortedList()
    {
        // Arrange
        var expected = new SortedList<int, int>() { [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>, SortedList<int, int>, int, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Dictionary_SortedDictionary()
    {
        // Arrange
        var expected = new SortedDictionary<int, int>() { [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>, SortedDictionary<int, int>, int, int>(expected);
    }
 
    [Fact]
    public void CanDeserialize_Dictionary_IReadOnlyDictionary()
    {
        // Arrange
        var expected = new Dictionary<int, int>() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, };
        var collection = new Dictionary<string, StringValues>()
        {
            ["[0]"] = "10",
            ["[1]"] = "11",
            ["[2]"] = "12",
            ["[3]"] = "13",
            ["[4]"] = "14",
            ["[5]"] = "15",
            ["[6]"] = "16",
            ["[7]"] = "17",
            ["[8]"] = "18",
            ["[9]"] = "19",
        };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<IReadOnlyDictionary<int, int>>(reader, options);
 
        // Assert
        var dictionary = Assert.IsType<ReadOnlyDictionary<int, int>>(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<int, int>() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, };
        var collection = new Dictionary<string, StringValues>()
        {
            ["[0]"] = "10",
            ["[1]"] = "11",
            ["[2]"] = "12",
            ["[3]"] = "13",
            ["[4]"] = "14",
            ["[5]"] = "15",
            ["[6]"] = "16",
            ["[7]"] = "17",
            ["[8]"] = "18",
            ["[9]"] = "19",
        };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<ReadOnlyDictionary<int, int>>(reader, options);
 
        // Assert
        var dictionary = Assert.IsType<ReadOnlyDictionary<int, int>>(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<string, StringValues>() { };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<IReadOnlyDictionary<int, int>>(reader, options);
 
        // Assert
        Assert.Null(result);
    }
 
    [Theory]
    [InlineData(1, 2)]
    [InlineData(2, 2)]
    [InlineData(3, 2)]
    [InlineData(109, 110)]
    [InlineData(110, 110)]
    [InlineData(111, 110)]
    [InlineData(120, 110)]
    public void Deserialize_Dictionary_RespectsMaxCollectionSize(int size, int maxCollectionSize)
    {
        // Arrange
        var data = new Dictionary<string, StringValues>(Enumerable.Range(0, size)
            .Select(i => new KeyValuePair<string, StringValues>(
                $"[{i.ToString(CultureInfo.InvariantCulture)}]",
                (i + 10).ToString(CultureInfo.InvariantCulture))));
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
 
        var options = new FormDataMapperOptions
        {
            MaxCollectionSize = maxCollectionSize
        };
 
        // Act
        var result = FormDataMapper.Map<Dictionary<int, int>>(reader, options);
 
        // Assert
        Assert.True(result.Count == maxCollectionSize || result.Count == maxCollectionSize - 1);
        if (size > maxCollectionSize)
        {
            var error = Assert.Single(errors);
            Assert.Equal("", error.Key);
            Assert.Equal($"The number of elements in the dictionary exceeded the maximum number of '{maxCollectionSize}' elements allowed.", error.Message.ToString(reader.Culture));
            Assert.Null(error.Value);
        }
        else
        {
            Assert.Empty(errors);
        }
    }
 
    [Fact]
    public void Deserialize_Dictionary_ContinuesParsingAfterErrors()
    {
        // Arrange
        var expected = new Dictionary<int, int>
        {
            [0] = 0,
            [1] = 11,
            [2] = 12,
            [3] = 13,
            [4] = 0,
            [5] = 15,
            [6] = 16,
            [7] = 17,
            [8] = 18,
            [9] = 19,
        };
        var collection = new Dictionary<string, StringValues>()
        {
            ["[0]"] = "abc",
            ["[1]"] = "11",
            ["[2]"] = "12",
            ["[3]"] = "13",
            ["[4]"] = "def",
            ["[5]"] = "15",
            ["[6]"] = "16",
            ["[7]"] = "17",
            ["[8]"] = "18",
            ["[9]"] = "19",
        };
 
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<Dictionary<int, int>>(reader, options);
 
        // Assert
        var dictionary = Assert.IsType<Dictionary<int, int>>(result);
        Assert.Equal(expected, dictionary);
        Assert.Equal(2, errors.Count);
        Assert.Collection(errors,
            e =>
            {
                Assert.Equal("[0]", e.Key);
                Assert.Equal("The value 'abc' is not valid for '0'.", e.Message.ToString(reader.Culture));
                Assert.Equal("abc", e.Value);
            },
            e =>
            {
                Assert.Equal("[4]", e.Key);
                Assert.Equal("The value 'def' is not valid for '4'.", e.Message.ToString(reader.Culture));
                Assert.Equal("def", e.Value);
            });
    }
 
    [Fact]
    public void Deserialize_SkipsElement_WhenFailsToParseKey()
    {
        // Arrange
        var expected = new Dictionary<int, int>
        {
            [1] = 11,
            [2] = 12,
            [3] = 13,
            [5] = 15,
            [6] = 16,
            [7] = 17,
            [8] = 18,
            [9] = 19,
        };
        var collection = new Dictionary<string, StringValues>()
        {
            ["[abc]"] = "10",
            ["[1]"] = "11",
            ["[2]"] = "12",
            ["[3]"] = "13",
            ["[def]"] = "14",
            ["[5]"] = "15",
            ["[6]"] = "16",
            ["[7]"] = "17",
            ["[8]"] = "18",
            ["[9]"] = "19",
        };
 
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<Dictionary<int, int>>(reader, options);
 
        // Assert
        var dictionary = Assert.IsType<Dictionary<int, int>>(result);
        Assert.Equal(expected, dictionary);
        Assert.Equal(2, errors.Count);
        Assert.Collection(errors,
            e =>
            {
                Assert.Equal("", e.Key);
                Assert.Equal("The value 'abc' is not a valid key for ''.", e.Message.ToString(reader.Culture));
                Assert.Null(e.Value);
            },
            e =>
            {
                Assert.Equal("", e.Key);
                Assert.Equal("The value 'def' is not a valid key for ''.", e.Message.ToString(reader.Culture));
                Assert.Null(e.Value);
            });
    }
 
    private void CanDeserialize_Dictionary<TDictionary, TImplementation, TKey, TValue>(TImplementation expected)
        where TDictionary : IDictionary<TKey, TValue>
        where TImplementation : TDictionary
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>()
        {
            ["[0]"] = "10",
            ["[1]"] = "11",
            ["[2]"] = "12",
            ["[3]"] = "13",
            ["[4]"] = "14",
            ["[5]"] = "15",
            ["[6]"] = "16",
            ["[7]"] = "17",
            ["[8]"] = "18",
            ["[9]"] = "19",
        };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(TDictionary));
 
        // Assert
        var dictionary = Assert.IsType<TImplementation>(result);
        Assert.Equal(expected.Count, dictionary.Count);
        Assert.Equal(expected.OrderBy(o => o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray());
    }
 
    private void CanDeserialize_Collection<TCollection, TImplementation, TElement>(TImplementation expected, bool sequenceEquals = false)
    {
        // Arrange
        var collection = new Dictionary<string, StringValues>()
        {
            ["[0]"] = "10",
            ["[1]"] = "11",
            ["[2]"] = "12",
            ["[3]"] = "13",
            ["[4]"] = "14",
            ["[5]"] = "15",
            ["[6]"] = "16",
            ["[7]"] = "17",
            ["[8]"] = "18",
            ["[9]"] = "19",
        };
        var reader = CreateFormDataReader(collection, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(TCollection));
 
        // Assert
        var list = Assert.IsType<TImplementation>(result);
        if (!sequenceEquals)
        {
            Assert.Equal(expected, list);
        }
        else
        {
            Assert.True(((IEnumerable<TElement>)expected).SequenceEqual((IEnumerable<TElement>)list));
        }
    }
 
    [Fact]
    public void CanDeserialize_ComplexValueType_Address()
    {
        // Arrange
        var expected = new Address() { City = "Redmond", Street = "1 Microsoft Way", Country = "United States", ZipCode = 98052 };
        var data = new Dictionary<string, StringValues>()
        {
            ["City"] = "Redmond",
            ["Country"] = "United States",
            ["Street"] = "1 Microsoft Way",
            ["ZipCode"] = "98052",
        };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<Address>(reader, options);
 
        // Assert
        Assert.Equal(expected.City, result.City);
        Assert.Equal(expected.Street, result.Street);
        Assert.Equal(expected.Country, result.Country);
        Assert.Equal(expected.ZipCode, result.ZipCode);
    }
 
    [Fact]
    public void CanDeserialize_ComplexReferenceType_Customer()
    {
        // Arrange
        var expected = new Customer() { Age = 20, Name = "John Doe", Email = "john.doe@example.com", IsPreferred = true };
        var data = new Dictionary<string, StringValues>()
        {
            ["Age"] = "20",
            ["Name"] = "John Doe",
            ["Email"] = "john.doe@example.com",
            ["IsPreferred"] = "true",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<Customer>(reader, options);
 
        // Assert
        Assert.NotNull(result);
        Assert.Equal(expected.Age, result.Age);
        Assert.Equal(expected.Name, result.Name);
        Assert.Equal(expected.Email, result.Email);
        Assert.Equal(expected.IsPreferred, result.IsPreferred);
    }
 
    [Fact]
    public void CanDeserialize_ComplexRecursiveTypes_RecursiveList()
    {
        // Arrange
        var expected = new RecursiveList()
        {
            Head = 10,
            Tail = null
        };
 
        for (var i = 10 - 1; i >= 0; i--)
        {
            expected = new RecursiveList()
            {
                Head = i,
                Tail = expected
            };
        }
 
        var data = new Dictionary<string, StringValues>()
        {
            ["Head"] = "0",
            ["Tail.Head"] = "1",
            ["Tail.Tail.Head"] = "2",
            ["Tail.Tail.Tail.Head"] = "3",
            ["Tail.Tail.Tail.Tail.Head"] = "4",
            ["Tail.Tail.Tail.Tail.Tail.Head"] = "5",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "6",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "7",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "8",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "9",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "10",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<RecursiveList>(reader, options);
 
        // Assert
        Assert.NotNull(result);
        Assert.Multiple(() =>
        {
            Assert.Equal(expected.Head, result.Head);
            Assert.Equal(expected.Tail.Head, result.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Head, result.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Tail.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head);
            Assert.Null(result.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail);
        });
    }
 
    [Fact]
    public void CanDeserialize_ComplexRecursiveTypes_ThrowsWhenMaxRecursionDepthExceeded()
    {
        // Arrange
        var expected = new RecursiveList()
        {
            Head = 5,
            Tail = null
        };
 
        for (var i = 5 - 1; i >= 0; i--)
        {
            expected = new RecursiveList()
            {
                Head = i,
                Tail = expected
            };
        }
 
        var data = new Dictionary<string, StringValues>()
        {
            ["Head"] = "0",
            ["Tail.Head"] = "1",
            ["Tail.Tail.Head"] = "2",
            ["Tail.Tail.Tail.Head"] = "3",
            ["Tail.Tail.Tail.Tail.Head"] = "4",
            ["Tail.Tail.Tail.Tail.Tail.Head"] = "5",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "6",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "7",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "8",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "9",
            ["Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Tail.Head"] = "10",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        reader.MaxRecursionDepth = 5;
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, exception) =>
        {
            errors.Add(new FormDataMappingError(key, message, exception));
        };
 
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<RecursiveList>(reader, options);
 
        // Assert
        Assert.NotNull(result);
        Assert.Multiple(() =>
        {
            Assert.Equal(expected.Head, result.Head);
            Assert.Equal(expected.Tail.Head, result.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Head, result.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Head);
            Assert.Equal(expected.Tail.Tail.Tail.Tail.Head, result.Tail.Tail.Tail.Tail.Head);
            Assert.Null(result.Tail.Tail.Tail.Tail.Tail);
        });
        Assert.Collection(errors,
            e =>
            {
                Assert.Equal("Tail.Tail.Tail.Tail.Tail", e.Key);
                Assert.Equal("The maximum recursion depth of '5' was exceeded for 'Tail.Tail.Tail.Tail.Tail.Head'.", e.Message.ToString(CultureInfo.InvariantCulture));
            },
            e =>
            {
                Assert.Equal("Tail.Tail.Tail.Tail.Tail", e.Key);
                Assert.Equal("The maximum recursion depth of '5' was exceeded for 'Tail.Tail.Tail.Tail.Tail.Tail'.", e.Message.ToString(CultureInfo.InvariantCulture));
            });
    }
 
    [Fact]
    public void CanDeserialize_ComplexRecursiveCollectionTypes_RecursiveTree()
    {
        // Arrange
        var expected = new RecursiveTree()
        {
            Value = 10,
            Children = null
        };
 
        for (var i = 10 - 1; i >= 0; i--)
        {
            expected = new RecursiveTree()
            {
                Value = i,
                Children = new List<RecursiveTree>() { expected }
            };
        }
 
        var data = new Dictionary<string, StringValues>()
        {
            ["Value"] = "0",
            ["Children[0].Value"] = "1",
            ["Children[0].Children[0].Value"] = "2",
            ["Children[0].Children[0].Children[0].Value"] = "3",
            ["Children[0].Children[0].Children[0].Children[0].Value"] = "4",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "5",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "6",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "7",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "8",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "9",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "10",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<RecursiveTree>(reader, options);
 
        // Assert
        Assert.NotNull(result);
        Assert.Multiple(() =>
        {
            Assert.Equal(expected.Value, result.Value);
            Assert.Equal(expected.Children[0].Value, result.Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Value, result.Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Null(result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children);
        });
    }
 
    [Fact]
    public void CanDeserialize_ComplexRecursiveCollectionTypes_RecursiveDictionaryTree()
    {
        // Arrange
        var expected = new RecursiveDictionaryTree()
        {
            Value = 10,
            Children = null
        };
 
        for (var i = 10 - 1; i >= 0; i--)
        {
            expected = new RecursiveDictionaryTree()
            {
                Value = i,
                Children = new Dictionary<int, RecursiveDictionaryTree>() { [0] = expected }
            };
        }
 
        var data = new Dictionary<string, StringValues>()
        {
            ["Value"] = "0",
            ["Children[0].Value"] = "1",
            ["Children[0].Children[0].Value"] = "2",
            ["Children[0].Children[0].Children[0].Value"] = "3",
            ["Children[0].Children[0].Children[0].Children[0].Value"] = "4",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "5",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "6",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "7",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "8",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "9",
            ["Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value"] = "10",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<RecursiveTree>(reader, options);
 
        // Assert
        Assert.NotNull(result);
        Assert.Multiple(() =>
        {
            Assert.Equal(expected.Value, result.Value);
            Assert.Equal(expected.Children[0].Value, result.Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Value, result.Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Equal(expected.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value, result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Value);
            Assert.Null(result.Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children[0].Children);
        });
    }
 
    [Fact]
    public void Deserialize_ComplexType_ContinuesMappingAfterPropertyError()
    {
        // Arrange
        var expected = new Customer() { Age = 0, Name = "John Doe", Email = "john.doe@example.com", IsPreferred = true };
        var data = new Dictionary<string, StringValues>()
        {
            ["Age"] = "abc",
            ["Name"] = "John Doe",
            ["Email"] = "john.doe@example.com",
            ["IsPreferred"] = "true",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, exception) =>
        {
            errors.Add(new FormDataMappingError(key, message, exception));
        };
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<Customer>(reader, options);
 
        // Assert
        Assert.NotNull(result);
        Assert.Equal(expected.Age, result.Age);
        Assert.Equal(expected.Name, result.Name);
        Assert.Equal(expected.Email, result.Email);
        Assert.Equal(expected.IsPreferred, result.IsPreferred);
 
        var error = Assert.Single(errors);
        Assert.Equal("Age", error.Key);
        var expectedMessage = "The value 'abc' is not valid for 'Age'.";
        var actualMessage = error.Message.ToString(reader.Culture);
        Assert.Equal(expectedMessage, actualMessage);
        Assert.Equal("abc", error.Value);
    }
 
    [Fact]
    public void CanDeserialize_ComplexReferenceType_Inheritance()
    {
        // Arrange
        var expected = new FrequentCustomer() { Age = 20, Name = "John Doe", Email = "john@example.com", IsPreferred = true, TotalVisits = 10, PreferredStore = "Redmond", MonthlyFrequency = 0.8 };
        var data = new Dictionary<string, StringValues>()
        {
            ["Age"] = "20",
            ["Name"] = "John Doe",
            ["Email"] = "john@example.com",
            ["IsPreferred"] = "true",
            ["TotalVisits"] = "10",
            ["PreferredStore"] = "Redmond",
            ["MonthlyFrequency"] = "0.8",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<FrequentCustomer>(reader, options);
        Assert.NotNull(result);
        Assert.Equal(expected.Age, result.Age);
        Assert.Equal(expected.Name, result.Name);
        Assert.Equal(expected.Email, result.Email);
        Assert.Equal(expected.IsPreferred, result.IsPreferred);
        Assert.Equal(expected.TotalVisits, result.TotalVisits);
        Assert.Equal(expected.PreferredStore, result.PreferredStore);
        Assert.Equal(expected.MonthlyFrequency, result.MonthlyFrequency);
    }
 
    [Fact]
    public void CanDeserialize_ComplexTypeWithConstructorParameters_KeyValuePair()
    {
        // Arrange
        var expected = new KeyValuePair<string, int>("Age", 20);
        var data = new Dictionary<string, StringValues>()
        {
            ["Key"] = "Age",
            ["Value"] = "20",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<KeyValuePair<string, int>>(reader, options);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_RecordType()
    {
        // Arrange
        var expected = new ClassRecordType("Age", 20);
        var data = new Dictionary<string, StringValues>()
        {
            ["Key"] = "Age",
            ["Value"] = "20",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<ClassRecordType>(reader, options);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_StructRecordType()
    {
        // Arrange
        var expected = new StructRecordType("Age", 20);
        var data = new Dictionary<string, StringValues>()
        {
            ["Key"] = "Age",
            ["Value"] = "20",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<StructRecordType>(reader, options);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_AppliesDataMemberRelatedAttributes()
    {
        // Arrange
        var expected = new DataMemberAttributesType { Key = "Age", Value = 20 };
        var data = new Dictionary<string, StringValues>()
        {
            ["mycustomkey"] = "Age",
            ["mycustomvalue"] = "20",
            ["Ignored"] = "This should be ignored",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<DataMemberAttributesType>(reader, options);
        Assert.Equal(expected.Key, result.Key);
        Assert.Equal(expected.Value, result.Value);
        Assert.Null(result.Ignored);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_AppliesDataMemberRelatedAttributes_FromMatchingConstructorParameters()
    {
        // Arrange
        var expected = new DataMemberAttributesConstructorType("Age", 20);
        var data = new Dictionary<string, StringValues>()
        {
            ["mycustomkey"] = "Age",
            ["mycustomvalue"] = "20",
            ["Ignored"] = "This should be ignored",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<DataMemberAttributesConstructorType>(reader, options);
        Assert.Equal(expected.Key, result.Key);
        Assert.Equal(expected.Value, result.Value);
        Assert.Null(result.Ignored);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_IgnoresPropertiesWithoutPublicSetters()
    {
        // Arrange
        var expected = new TypeIgnoresReadOnlyProperties() { Name = "John" };
        var data = new Dictionary<string, StringValues>()
        {
            ["Id"] = "1",
            ["Name"] = "John",
            ["Age"] = "20",
            ["Email"] = "john@doe.com",
            ["IsPreferred"] = "true",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<TypeIgnoresReadOnlyProperties>(reader, options);
        Assert.Equal(0, result.Id);
        Assert.Equal(expected.Name, result.Name);
        Assert.Equal(expected.Age, result.Age);
        Assert.Equal(expected.Email, result.Email);
        Assert.Equal(expected.IsPreferred, result.IsPreferred);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_RequiredProperties()
    {
        // Arrange
        var expected = new TypeRequiredProperties() { Name = null, Age = 20 };
        var data = new Dictionary<string, StringValues>()
        {
            ["Age"] = "20",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<TypeRequiredProperties>(reader, options);
        Assert.Equal(expected.Name, result.Name);
        Assert.Equal(expected.Age, result.Age);
        Assert.Single(errors);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_CanDeserializeTuples()
    {
        // Arrange
        var expected = new Tuple<int, string>(1, "John");
        var data = new Dictionary<string, StringValues>()
        {
            ["Item1"] = "1",
            ["Item2"] = "John",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<Tuple<int, string>>(reader, options);
        Assert.Equal(expected.Item1, result.Item1);
        Assert.Equal(expected.Item2, result.Item2);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_CanDeserializeValueTuples()
    {
        // Arrange
        var expected = new ValueTuple<int, string>(1, "John");
        var data = new Dictionary<string, StringValues>()
        {
            ["Item1"] = "1",
            ["Item2"] = "John",
        };
 
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<ValueTuple<int, string>>(reader, options);
        Assert.Equal(expected.Item1, result.Item1);
        Assert.Equal(expected.Item2, result.Item2);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_DoesNotRegisterMissingRequiredParametersIfNoValueFound()
    {
        // Arrange
        var expected = new ThrowsWithMissingParameterValue("Age");
        var data = new Dictionary<string, StringValues>() { };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
 
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<ThrowsWithMissingParameterValue>(reader, options);
 
        // Assert
        Assert.Null(result);
        Assert.Empty(errors);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_ThrowsFromConstructor()
    {
        // Arrange
        var expected = new ThrowsWithMissingParameterValue("Age");
        var data = new Dictionary<string, StringValues>() { ["value"] = "20" };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
 
        var options = new FormDataMapperOptions();
 
        // Act
        var result = FormDataMapper.Map<ThrowsWithMissingParameterValue>(reader, options);
 
        // Assert
        Assert.Null(result);
        Assert.Equal(2, errors.Count);
        var error = errors[0];
        Assert.Equal("key", error.Key);
        Assert.Equal("Missing required value for constructor parameter 'key'.", error.Message.ToString(CultureInfo.InvariantCulture));
 
        var constructorError = errors[1];
        Assert.Equal("", constructorError.Key);
        Assert.Equal("Value cannot be null. (Parameter 'key')", constructorError.Message.ToString(CultureInfo.InvariantCulture));
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_CanSerializerFormFile()
    {
        // Arrange
        var expected = new FormFile(Stream.Null, 0, 10, "file", "file.txt");
        var formFileCollection = new FormFileCollection { expected };
        var data = new Dictionary<string, StringValues>();
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture, formFileCollection);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        reader.PushPrefix("file");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(IFormFile));
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_CanSerializerIReadOnlyListFormFile()
    {
        // Arrange
        var formFileCollection = new FormFileCollection
        {
            new FormFile(Stream.Null, 0, 10, "file", "file-1.txt"),
            new FormFile(Stream.Null, 0, 20, "file", "file-2.txt"),
            new FormFile(Stream.Null, 0, 30, "file", "file-3.txt"),
            new FormFile(Stream.Null, 0, 40, "oddOneOutFile", "file-4.txt"),
        };
        var data = new Dictionary<string, StringValues>();
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture, formFileCollection);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        reader.PushPrefix("file");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(IReadOnlyList<IFormFile>));
 
        // Assert
        var formFileResult = Assert.IsAssignableFrom<IReadOnlyList<IFormFile>>(result);
        Assert.Collection(formFileResult,
            element => Assert.Equal("file-1.txt", element.FileName),
            element => Assert.Equal("file-2.txt", element.FileName),
            element => Assert.Equal("file-3.txt", element.FileName)
        );
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_ReturnsFirstFileForMultiples()
    {
        // Arrange
        var formFileCollection = new FormFileCollection
        {
            new FormFile(Stream.Null, 0, 10, "file", "file-1.txt"),
            new FormFile(Stream.Null, 0, 20, "file", "file-2.txt"),
            new FormFile(Stream.Null, 0, 30, "file", "file-3.txt"),
            new FormFile(Stream.Null, 0, 40, "oddOneOutFile", "file-4.txt"),
        };
        var data = new Dictionary<string, StringValues>();
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture, formFileCollection);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        reader.PushPrefix("file");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(IFormFile));
 
        // Assert
        Assert.Equal(formFileCollection[0], result);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_CanSerializerFormFileCollection()
    {
        // Arrange
        var expected = new FormFileCollection { new FormFile(Stream.Null, 0, 10, "file", "file.txt") };
        var data = new Dictionary<string, StringValues>();
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture, expected);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        reader.PushPrefix("file");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(IFormFileCollection));
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_CanSerializerBrowserFile()
    {
        // Arrange
        var expectedString = "This is the contents of my text file.";
        var expected = new FormFileCollection { new FormFile(new MemoryStream(Encoding.UTF8.GetBytes(expectedString)), 0, expectedString.Length, "file", "file.txt") };
        var data = new Dictionary<string, StringValues>();
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture, expected);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        reader.PushPrefix("file");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(IBrowserFile));
 
        // Assert
        var browserFile = Assert.IsAssignableFrom<IBrowserFile>(result);
        Assert.Equal("file", browserFile.Name);
        Assert.Equal(expectedString.Length, browserFile.Size);
        var buffer = new byte[browserFile.Size];
        browserFile.OpenReadStream().Read(buffer);
        Assert.Equal(expectedString, Encoding.UTF8.GetString(buffer, 0, buffer.Length));
    }
 
    [Fact]
    public void CanDeserialize_ComplexType_CanSerializerIReadOnlyListBrowserFile()
    {
        // Arrange
        var expectedString1 = "This is the contents of my first text file.";
        var expectedString2 = "This is the contents of my second text file.";
        var expected = new FormFileCollection
        {
            new FormFile(new MemoryStream(Encoding.UTF8.GetBytes(expectedString1)), 0, expectedString1.Length, "file", "file1.txt"),
            new FormFile(new MemoryStream(Encoding.UTF8.GetBytes(expectedString2)), 0, expectedString2.Length, "file", "file2.txt")
        };
        var data = new Dictionary<string, StringValues>();
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture, expected);
        var errors = new List<FormDataMappingError>();
        reader.ErrorHandler = (key, message, attemptedValue) =>
        {
            errors.Add(new FormDataMappingError(key, message, attemptedValue));
        };
        reader.PushPrefix("file");
        var options = new FormDataMapperOptions();
 
        // Act
        var result = CallDeserialize(reader, options, typeof(IReadOnlyList<IBrowserFile>));
 
        // Assert
        var browserFiles = Assert.IsAssignableFrom<IReadOnlyList<IBrowserFile>>(result);
        // First file
        var browserFile1 = browserFiles[0];
        Assert.Equal("file", browserFile1.Name);
        Assert.Equal(expectedString1.Length, browserFile1.Size);
        var buffer1 = new byte[browserFile1.Size];
        browserFile1.OpenReadStream().Read(buffer1);
        Assert.Equal(expectedString1, Encoding.UTF8.GetString(buffer1, 0, buffer1.Length));
        // Second files
        var browserFile2 = browserFiles[0];
        Assert.Equal("file", browserFile2.Name);
        Assert.Equal(expectedString1.Length, browserFile2.Size);
        var buffer2 = new byte[browserFile2.Size];
        browserFile1.OpenReadStream().Read(buffer2);
        Assert.Equal(expectedString1, Encoding.UTF8.GetString(buffer2, 0, buffer2.Length));
    }
 
    [Fact]
    public void RecursiveTypes_Comparer_SortsValues_Correctly()
    {
        // Arrange
        var data = new Dictionary<string, StringValues>()
        {
            ["customerId"] = "20",
            ["customer[Id]"] = "20",
            ["customer.Id"] = "20"
        };
        var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
        reader.PushPrefix("customer");
 
        // Act
        var result = reader.CurrentPrefixExists();
 
        // Assert
        Assert.True(result);
    }
 
    public static TheoryData<string, Type, object> NullableBasicTypes
    {
        get
        {
            var result = new TheoryData<string, Type, object>
            {
                // strings
                { "C", typeof(char?), new char?('C')},
                // bool
                { "true", typeof(bool?), new bool?(true)},
                // bytes
                { "63", typeof(byte?), new byte?((byte)0b_0011_1111)},
                { "-63", typeof(sbyte?), new sbyte?((sbyte)-0b_0011_1111)},
                // numeric types
                { "123", typeof(ushort?), new ushort?((ushort)123u)},
                { "456", typeof(uint?), new uint?(456u)},
                { "789", typeof(ulong?), new ulong?(789uL)},
                { "-101112", typeof(Int128?), new Int128?(-(Int128)101112)},
                { "-123", typeof(short?), new short?((short)-123)},
                { "-456", typeof(int?), new int?(-456)},
                { "-789", typeof(long?), new long?(-789L)},
                { "101112", typeof(UInt128?), new UInt128?((UInt128)101112)},
                // floating point types
                { "12.56", typeof(Half?), new Half?((Half)12.56f)},
                { "6.28", typeof(float?), new float?(6.28f)},
                { "3.14", typeof(double?), new double?(3.14)},
                { "1.23", typeof(decimal?), new decimal?(1.23m)},
                // dates and times
                { "04/20/2023", typeof(DateOnly?), new DateOnly?(new DateOnly(2023, 04, 20))},
                { "4/20/2023 12:56:34", typeof(DateTime?), new DateTime?(new DateTime(2023, 04, 20, 12, 56, 34))},
                { "4/20/2023 12:56:34 PM +02:00", typeof(DateTimeOffset?), new DateTimeOffset?(new DateTimeOffset(2023, 04, 20, 12, 56, 34, TimeSpan.FromHours(2)))},
                { "02:01:03", typeof(TimeSpan?), new TimeSpan?(new TimeSpan(02, 01, 03))},
                { "12:56:34", typeof(TimeOnly?), new TimeOnly?(new TimeOnly(12, 56, 34))},
 
                // other types
                { "a55eb3df-e984-42b5-85ca-4f68da8567d1", typeof(Guid?), new Guid?(new Guid("a55eb3df-e984-42b5-85ca-4f68da8567d1")) },
            };
 
            return result;
        }
    }
 
    public static TheoryData<Type> NullNullableBasicTypes
    {
        get
        {
            var result = new TheoryData<Type>
            {
                // strings
                { typeof(char?) },
                // bool
                { typeof(bool?) },
                // bytes
                { typeof(byte?) },
                { typeof(sbyte?) },
                // numeric types
                { typeof(ushort?) },
                { typeof(uint?) },
                { typeof(ulong?) },
                { typeof(Int128?) },
                { typeof(short?) },
                { typeof(int?) },
                { typeof(long?) },
                { typeof(UInt128?) },
                // floating point types
                { typeof(Half?) },
                { typeof(float?) },
                { typeof(double?) },
                { typeof(decimal?) },
                // dates and times
                { typeof(DateOnly?) },
                { typeof(DateTime?) },
                { typeof(DateTimeOffset?) },
                { typeof(TimeSpan?) },
                { typeof(TimeOnly?) },
 
                // other types
                { typeof(Guid?) },
            };
 
            return result;
        }
    }
 
    public static TheoryData<string, Type, object> PrimitiveTypesData
    {
        get
        {
            var result = new TheoryData<string, Type, object>
            {
                // strings
                { "C", typeof(char), 'C' },
                { "hello", typeof(string), "hello" },
                // bool
                { "true", typeof(bool), true },
                // bytes
                { "63", typeof(byte), (byte)0b_0011_1111 },
                { "-63", typeof(sbyte), (sbyte)-0b_0011_1111 },
                // numeric types
                { "123", typeof(ushort), (ushort)123u },
                { "456", typeof(uint), 456u },
                { "789", typeof(ulong), 789uL },
                { "-101112", typeof(Int128), -(Int128)101112 },
                { "-123", typeof(short), (short)-123 },
                { "-456", typeof(int), -456 },
                { "-789", typeof(long), -789L },
                { "101112", typeof(UInt128), (UInt128)101112 },
                // floating point types
                { "12.56", typeof(Half), (Half)12.56f },
                { "6.28", typeof(float), 6.28f },
                { "3.14", typeof(double), 3.14 },
                { "1.23", typeof(decimal), 1.23m },
                // dates and times
                { "04/20/2023", typeof(DateOnly), new DateOnly(2023, 04, 20) },
                { "4/20/2023 12:56:34", typeof(DateTime), new DateTime(2023, 04, 20, 12, 56, 34) },
                { "4/20/2023 12:56:34 PM +02:00", typeof(DateTimeOffset), new DateTimeOffset(2023, 04, 20, 12, 56, 34, TimeSpan.FromHours(2)) },
                { "02:01:03", typeof(TimeSpan), new TimeSpan(02, 01, 03) },
                { "12:56:34", typeof(TimeOnly), new TimeOnly(12, 56, 34) },
 
                // other types
                { "a55eb3df-e984-42b5-85ca-4f68da8567d1", typeof(Guid), new Guid("a55eb3df-e984-42b5-85ca-4f68da8567d1") }
            };
 
            return result;
        }
    }
 
    private object CallDeserialize(FormDataReader reader, FormDataMapperOptions options, Type type)
    {
        var method = typeof(FormDataMapper)
            .GetMethod("Map", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public) ??
            throw new InvalidOperationException("Unable to find method 'Map'.");
 
        return method.MakeGenericMethod(type).Invoke(null, new object[] { reader, options })!;
    }
}
 
internal class TypeWithUri
{
    public Uri Slug { get; set; }
}
 
internal class Point : IParsable<Point>, IEquatable<Point>
{
    public int X { get; set; }
    public int Y { get; set; }
 
    public static Point Parse(string s, IFormatProvider provider)
    {
        // Parses points. Points start with ( and end with ).
        // Points define two components, X and Y, separated by a comma.
        var components = s.Trim('(', ')').Split(',');
        if (components.Length != 2)
        {
            throw new FormatException("Invalid point format.");
        }
        var result = new Point();
        result.X = int.Parse(components[0], provider);
        result.Y = int.Parse(components[1], provider);
        return result;
    }
 
    public static bool TryParse([NotNullWhen(true)] string s, IFormatProvider provider, [MaybeNullWhen(false)] out Point result)
    {
        // Try parse points is similar to Parse, but returns a bool to indicate success.
        // It also uses the out parameter to return the result.
        try
        {
            result = Parse(s, provider);
            return true;
        }
        catch (FormatException)
        {
            result = null;
            return false;
        }
    }
 
    public override bool Equals(object obj) => Equals(obj as Point);
 
    public bool Equals(Point other) => other is not null && X == other.X && Y == other.Y;
 
    public override int GetHashCode() => HashCode.Combine(X, Y);
 
    public static bool operator ==(Point left, Point right) => EqualityComparer<Point>.Default.Equals(left, right);
 
    public static bool operator !=(Point left, Point right) => !(left == right);
}
 
internal struct ValuePoint : IParsable<ValuePoint>, IEquatable<ValuePoint>
{
    public int X { get; set; }
 
    public int Y { get; set; }
 
    public static ValuePoint Parse(string s, IFormatProvider provider)
    {
        // Parses points. Points start with ( and end with ).
        // Points define two components, X and Y, separated by a comma.
        var components = s.Trim('(', ')').Split(',');
        if (components.Length != 2)
        {
            throw new FormatException("Invalid point format.");
        }
        var result = new ValuePoint();
        result.X = int.Parse(components[0], provider);
        result.Y = int.Parse(components[1], provider);
        return result;
    }
 
    public static bool TryParse([NotNullWhen(true)] string s, IFormatProvider provider, [MaybeNullWhen(false)] out ValuePoint result)
    {
        // Try parse points is similar to Parse, but returns a bool to indicate success.
        // It also uses the out parameter to return the result.
        try
        {
            result = Parse(s, provider);
            return true;
        }
        catch (FormatException)
        {
            result = default;
            return false;
        }
    }
 
    public override bool Equals(object obj) => Equals((ValuePoint)obj);
 
    public bool Equals(ValuePoint other) => X == other.X && Y == other.Y;
 
    public override int GetHashCode() => HashCode.Combine(X, Y);
 
    public static bool operator ==(ValuePoint left, ValuePoint right) => EqualityComparer<ValuePoint>.Default.Equals(left, right);
 
    public static bool operator !=(ValuePoint left, ValuePoint right) => !(left == right);
}
 
internal abstract class TestArrayPoolBufferAdapter
    : ICollectionBufferAdapter<int[], TestArrayPoolBufferAdapter.PooledBuffer, int>
{
    public static event Action<int[]> OnRent;
    public static event Action<int[]> OnReturn;
 
    public static PooledBuffer CreateBuffer() => new() { Data = Rent(16), Count = 0 };
 
    public static PooledBuffer Add(ref PooledBuffer buffer, int element)
    {
        if (buffer.Count >= buffer.Data.Length)
        {
            var newBuffer = Rent(buffer.Data.Length * 2);
            Array.Copy(buffer.Data, newBuffer, buffer.Data.Length);
            Return(buffer.Data);
            buffer.Data = newBuffer;
        }
 
        buffer.Data[buffer.Count++] = element;
        return buffer;
    }
 
    public static int[] ToResult(PooledBuffer buffer)
    {
        var result = new int[buffer.Count];
        Array.Copy(buffer.Data, result, buffer.Count);
        Return(buffer.Data);
        return result;
    }
 
    public struct PooledBuffer
    {
        public int[] Data { get; set; }
        public int Count { get; set; }
    }
 
    public static int[] Rent(int size)
    {
        var result = ArrayPool<int>.Shared.Rent(size);
        OnRent?.Invoke(result);
        return result;
    }
 
    public static void Return(int[] array)
    {
        OnReturn?.Invoke(array);
        ArrayPool<int>.Shared.Return(array);
    }
}
 
public enum Colors
{
    Red,
    Green,
    Blue
}
 
internal struct Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    public int ZipCode { get; set; }
}
 
internal class Customer
{
    public string Name { get; set; }
    public string Email { get; set; }
    public int Age { get; set; }
    public bool IsPreferred { get; set; }
}
 
internal class FrequentCustomer : Customer
{
    public int TotalVisits { get; set; }
 
    public string PreferredStore { get; set; }
 
    public double MonthlyFrequency { get; set; }
}
 
// Implements ICollection<TEnum> delegating to List<TEnum> _inner;
internal class CustomCollection<T> : ICollection<T>
{
    private readonly List<T> _inner = new();
 
    public int Count => _inner.Count;
    public bool IsReadOnly => false;
    public void Add(T item) => _inner.Add(item);
    public void Clear() => _inner.Clear();
    public bool Contains(T item) => _inner.Contains(item);
    public bool Remove(T item) => _inner.Remove(item);
    public IEnumerator<T> GetEnumerator() => _inner.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
    public void CopyTo(T[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
}
 
internal class RecursiveList
{
    public int Head { get; set; }
    public RecursiveList Tail { get; set; }
}
 
internal class RecursiveTree
{
    public int Value { get; set; }
    public List<RecursiveTree> Children { get; set; }
}
 
internal class RecursiveDictionaryTree
{
    public int Value { get; set; }
 
    public Dictionary<int, RecursiveDictionaryTree> Children { get; set; }
}
 
internal record ClassRecordType(string Key, int Value);
 
internal record struct StructRecordType(string Key, int Value);
 
internal class DataMemberAttributesType
{
    [DataMember(Name = "mycustomkey")]
    public string Key { get; set; }
 
    [DataMember(Name = "mycustomvalue")]
    public int Value { get; set; }
 
    [IgnoreDataMember]
    public string Ignored { get; set; }
}
 
internal class DataMemberAttributesConstructorType
{
    public DataMemberAttributesConstructorType(string key, int value)
    {
        Key = key;
        Value = value;
    }
 
    [DataMember(Name = "mycustomkey")]
    public string Key { get; set; }
 
    [DataMember(Name = "mycustomvalue")]
    public int Value { get; set; }
 
    [IgnoreDataMember]
    public string Ignored { get; set; }
}
 
internal class TypeIgnoresReadOnlyProperties
{
    public int Id { get; }
 
    public int Age { get; internal set; }
 
    public string Email { get; private set; }
 
    public bool IsPreferred { get; protected set; }
 
    public string Name { get; set; }
}
 
internal class TypeRequiredProperties
{
    public int Age { get; set; }
 
    public required string Name { get; set; }
}
 
public class ThrowsWithMissingParameterValue
{
    public ThrowsWithMissingParameterValue(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
        Key = key;
    }
 
    public string Key { get; set; }
 
    public int Value { get; set; }
}
 
public class Throwing { }
 
internal class ThrowingConverter : FormDataConverter<int>
{
    internal delegate bool OnTryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out int result, out bool found);
 
    internal OnTryRead OnTryReadDelegate { get; set; } =
        (ref FormDataReader context, Type type, FormDataMapperOptions options, out int result, out bool found) =>
            throw new InvalidOperationException("Could not read value.");
 
    internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out int result, out bool found) =>
        OnTryReadDelegate.Invoke(ref context, type, options, out result, out found);
}