File: FormMapping\Factories\CollectionConverterFactory.cs
Web Access
Project: src\src\Components\Endpoints\src\Microsoft.AspNetCore.Components.Endpoints.csproj (Microsoft.AspNetCore.Components.Endpoints)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Internal;
 
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
 
internal class CollectionConverterFactory : IFormDataConverterFactory
{
    public static readonly CollectionConverterFactory Instance = new();
 
    [RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
    [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
    public bool CanConvert(Type type, FormDataMapperOptions options)
    {
        var element = ResolveElementType(type);
        if (element == null)
        {
            return false;
        }
 
        if (Activator.CreateInstance(typeof(TypedCollectionConverterFactory<,>)
            .MakeGenericType(type, element!)) is not IFormDataConverterFactory factory)
        {
            return false;
        }
 
        return factory.CanConvert(type, options);
    }
 
    [RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
    [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
    public static Type? ResolveElementType(Type type)
    {
        var enumerable = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>));
        if (enumerable == null && !(type.IsArray && type.GetArrayRank() == 1))
        {
            return null;
        }
 
        return enumerable != null ? enumerable.GetGenericArguments()[0] : type.GetElementType()!;
    }
 
    [RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
    [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
    public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
    {
        ArgumentNullException.ThrowIfNull(type);
        ArgumentNullException.ThrowIfNull(options);
 
        // There is an assumption here that if the type is a bindable collection, it's going to implement
        // ICollection<T> and IEnumerable<T>. There could potentially be a type that implements ICollection<T>
        // multiple times for different T's explicitly, but that is not something we will support (nor something supported today).
        var enumerableType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>));
        var elementType = enumerableType?.GetGenericArguments()[0];
 
        // The collection converter heavily relies on generics to adapt to different collection types.
        // Since reflection gets a bit tricky with generics, we instead close over the generic collection and
        // element types to make it simpler to create the converter.
        var factory = Activator.CreateInstance(typeof(TypedCollectionConverterFactory<,>)
            .MakeGenericType(type, elementType!)) as IFormDataConverterFactory;
 
        if (factory == null)
        {
            throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'.");
        }
 
        return factory.CreateConverter(type, options);
    }
}