File: FormMapping\Metadata\FormDataMetadataFactory.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 System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping.Metadata;
 
internal partial class FormDataMetadataFactory(List<IFormDataConverterFactory> factories, ILoggerFactory loggerFactory)
{
    private readonly object _lock = new object();
    private readonly FormMetadataContext _context = new();
    private readonly ParsableConverterFactory _parsableFactory = factories.OfType<ParsableConverterFactory>().Single();
    private readonly DictionaryConverterFactory _dictionaryFactory = factories.OfType<DictionaryConverterFactory>().Single();
    private readonly FileConverterFactory _fileConverterFactory = factories.OfType<FileConverterFactory>().Single();
    private readonly CollectionConverterFactory _collectionFactory = factories.OfType<CollectionConverterFactory>().Single();
    private readonly ILogger<FormDataMetadataFactory> _logger = loggerFactory.CreateLogger<FormDataMetadataFactory>();
 
    [RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
    [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
    public FormDataTypeMetadata? GetOrCreateMetadataFor(Type type, FormDataMapperOptions options)
    {
        var shouldClearContext = !_context.ResolutionInProgress;
        lock (_lock)
        {
            try
            {
                // We are walking the graph in order to detect recursive types.
                // We evaluate whether a type is:
                // 1. Primitive
                // 2. Dictionary
                // 3. Collection
                // 4. Complex
                // Only complex types can be recursive.
                // We only compute metadata when we are dealing with objects, other classes of
                // types are handled directly by the appropriate converters.
                // We keep track of the metadata for the types because it is useful when we generate
                // the specific object converter for a type.
                // The code generation varies depending on whether there is recursion or not within
                // the type graph.
                if (shouldClearContext)
                {
                    Log.StartResolveMetadataGraph(_logger, type);
                    _context.BeginResolveGraph();
                }
 
                // Try to get the metadata for the type or create and add a new instance.
                var result = _context.TypeMetadata.TryGetValue(type, out var value) ? value : new FormDataTypeMetadata(type);
                if (value == null)
                {
                    Log.NoMetadataFound(_logger, type);
                    _context.TypeMetadata[type] = result;
                }
                else
                {
                    Log.MetadataFound(_logger, type);
                }
 
                if (type.IsGenericTypeDefinition)
                {
                    Log.GenericTypeDefinitionNotSupported(_logger, type);
                    _context.TypeMetadata.Remove(type);
                    return null;
                }
 
                // Check for cycles and mark any type as recursive if needed.
                DetectCyclesAndMarkMetadataTypesAsRecursive(type, result);
 
                // We found the value on the existing metadata, we can return it.
                if (value != null)
                {
                    return result;
                }
 
                // These blocks are evaluated in a specific order.
                if (_parsableFactory.CanConvert(type, options) || type.IsEnum ||
                    (Nullable.GetUnderlyingType(type) is { } underlyingType &&
                        _parsableFactory.CanConvert(underlyingType, options)))
                {
                    Log.PrimitiveType(_logger, type);
                    result.Kind = FormDataTypeKind.Primitive;
                    return result;
                }
 
                if (_fileConverterFactory.CanConvert(type, options))
                {
                    result.Kind = FormDataTypeKind.File;
                    return result;
                }
 
                if (WellKnownConverters.Converters.TryGetValue(type, out var converter))
                {
                    result.Kind = FormDataTypeKind.Primitive;
                    return result;
                }
 
                if (_dictionaryFactory.CanConvert(type, options))
                {
                    Log.DictionaryType(_logger, type);
                    result.Kind = FormDataTypeKind.Dictionary;
                    var (keyType, valueType) = DictionaryConverterFactory.ResolveDictionaryTypes(type)!;
                    result.KeyType = GetOrCreateMetadataFor(keyType, options);
                    result.ValueType = GetOrCreateMetadataFor(valueType, options);
                    return result;
                }
 
                if (_collectionFactory.CanConvert(type, options))
                {
                    Log.CollectionType(_logger, type);
                    result.Kind = FormDataTypeKind.Collection;
                    result.ElementType = GetOrCreateMetadataFor(CollectionConverterFactory.ResolveElementType(type)!, options);
                    return result;
                }
 
                Log.ObjectType(_logger, type);
                result.Kind = FormDataTypeKind.Object;
                _context.Track(type);
                var constructors = type.GetConstructors();
 
                if (constructors.Length == 1)
                {
                    result.Constructor = constructors[0];
                    if (type.IsAbstract)
                    {
                        Log.AbstractClassesNotSupported(_logger, type);
                        _context.TypeMetadata.Remove(type);
                        return null;
                    }
                }
                else if (constructors.Length > 1)
                {
                    // We can't select the constructor when there are multiple of them.
                    Log.MultiplePublicConstructorsFound(_logger, type);
                    return null;
                }
                else if (!type.IsValueType)
                {
                    if (type.IsInterface)
                    {
                        Log.InterfacesNotSupported(_logger, type);
                    }
                    else if (type.IsAbstract)
                    {
                        Log.AbstractClassesNotSupported(_logger, type);
                    }
                    else
                    {
                        Log.NoPublicConstructorFound(_logger, type);
                    }
 
                    _context.TypeMetadata.Remove(type);
                    // We can't bind to reference types without constructors.
                    return null;
                }
 
                if (result.Constructor != null)
                {
                    if (_logger.IsEnabled(LogLevel.Debug))
                    {
                        var parameters = $"({string.Join(", ", result.Constructor.GetParameters().Select(p => p.ParameterType.Name))})";
                        Log.ConstructorFound(_logger, type, parameters);
                    }
 
                    var values = result.Constructor.GetParameters();
 
                    foreach (var parameter in values)
                    {
                        Log.ConstructorParameter(_logger, type, parameter.Name!, parameter.ParameterType);
                        var parameterTypeInfo = GetOrCreateMetadataFor(parameter.ParameterType, options);
                        if (parameterTypeInfo == null)
                        {
                            Log.ConstructorParameterTypeNotSupported(_logger, type, parameter.Name!, parameter.ParameterType);
                            _context.TypeMetadata.Remove(type);
                            return null;
                        }
 
                        result.ConstructorParameters.Add(new FormDataParameterMetadata(parameter, parameterTypeInfo));
                    }
                }
 
                var candidateProperty = PropertyHelper.GetVisibleProperties(type);
                foreach (var propertyHelper in candidateProperty)
                {
                    var property = propertyHelper.Property;
                    Log.CandidateProperty(_logger, propertyHelper.Name, property.PropertyType);
                    var matchingConstructorParameter = result
                        .ConstructorParameters
                        .FirstOrDefault(p => string.Equals(p.Name, property.Name, StringComparison.OrdinalIgnoreCase));
 
                    if (matchingConstructorParameter != null)
                    {
                        Log.MatchingConstructorParameterFound(_logger, matchingConstructorParameter.Name, property.Name);
                        var dataMember = property.GetCustomAttribute<DataMemberAttribute>();
                        if (dataMember != null && dataMember.IsNameSetExplicitly && dataMember.Name != null)
                        {
                            Log.CustomParameterNameMetadata(_logger, dataMember.Name, property.Name);
                            matchingConstructorParameter.Name = dataMember.Name;
                        }
 
                        // The propertyHelper is already present in the constructor, we don't need to add it again.
                        continue;
                    }
 
                    var ignoreDataMember = property.GetCustomAttribute<IgnoreDataMemberAttribute>();
                    if (ignoreDataMember != null)
                    {
                        Log.IgnoredProperty(_logger, property.Name);
                        // The propertyHelper is marked as ignored, we don't need to add it.
                        continue;
                    }
 
                    if (property.SetMethod == null || !property.SetMethod.IsPublic)
                    {
                        Log.NonPublicSetter(_logger, property.Name);
                        // The property is readonly, we don't need to add it.
                        continue;
                    }
 
                    var propertyTypeInfo = GetOrCreateMetadataFor(property.PropertyType, options);
                    if (propertyTypeInfo == null)
                    {
                        Log.PropertyTypeNotSupported(_logger, type, property.Name, property.PropertyType);
                        _context.TypeMetadata.Remove(type);
                        return null;
                    }
                    var propertyInfo = new FormDataPropertyMetadata(property, propertyTypeInfo);
 
                    var dataMemberAttribute = property.GetCustomAttribute<DataMemberAttribute>();
                    if (dataMemberAttribute != null && dataMemberAttribute.IsNameSetExplicitly && dataMemberAttribute.Name != null)
                    {
                        Log.CustomParameterNameMetadata(_logger, dataMemberAttribute.Name, property.Name);
                        propertyInfo.Name = dataMemberAttribute.Name;
                        propertyInfo.Required = dataMemberAttribute.IsRequired;
                        Log.PropertyRequired(_logger, propertyInfo.Name);
                    }
 
                    var requiredAttribute = property.GetCustomAttribute<RequiredMemberAttribute>();
                    if (requiredAttribute != null)
                    {
                        propertyInfo.Required = true;
                        Log.PropertyRequired(_logger, propertyInfo.Name);
                    }
 
                    result.Properties.Add(propertyInfo);
                }
 
                Log.MetadataComputed(_logger, type);
                return result;
            }
            finally
            {
                _context.Untrack(type);
                if (shouldClearContext)
                {
                    Log.EndResolveMetadataGraph(_logger, type);
                    _context.EndResolveGraph();
                }
            }
        }
    }
 
    internal bool HasMetadataFor(Type type) => _context.TypeMetadata.ContainsKey(type);
 
    private void DetectCyclesAndMarkMetadataTypesAsRecursive(Type type, FormDataTypeMetadata result)
    {
        // Mark any type as recursive if its already present in the current resolution graph or
        // if there is a base type for it on the resolution graph.
        // For example, given A and B : A. With A having a propertyHelper of type B.
        // when we inspect B, we can tell that A is recursive because you can have A -> B -> B -> B -> ...
        // The opposite scenario is not something we need to worry about because we don't support polymorphism,
        // meaning that we will never create an instance of B if the declared type is A.
        // In that scenario, A and B : A. With B having a propertyHelper of type A.
        // The recursion stops at A, because A doesn't define the propertyHelper and we will never bind B to A.
        // If in the future we support polymorphism, it's a matter or updating the logic below to account for it.
        for (var i = 0; i < _context.CurrentTypes.Count; i++)
        {
            if (_context.CurrentTypes[i] == type)
            {
                // We found an exact match in the current resolution graph.
                // This means that the type is recursive.
                result.IsRecursive = true;
                ReportRecursiveChain(type);
 
            }
            else if (type.IsSubclassOf(_context.CurrentTypes[i]))
            {
                // We found a type that is assignable from the current type.
                // This means that the type is recursive.
                var existingType = _context.TypeMetadata[_context.CurrentTypes[i]];
                ReportRecursiveChain(type);
                existingType.IsRecursive = true;
            }
 
            // We don't need to check for interfaces here because we never map to an interface, so if we encounter one, we will fail,
            // as we can't construct the converter to map the interface to a concrete instance.
        }
 
        void ReportRecursiveChain(Type type)
        {
            if (_logger.IsEnabled(LogLevel.Debug))
            {
                var chain = string.Join(" -> ", _context.CurrentTypes.Append(type).Select(t => t.Name));
                Log.RecursiveTypeFound(_logger, type, chain);
            }
        }
    }
 
    private class FormMetadataContext
    {
        public Dictionary<Type, FormDataTypeMetadata> TypeMetadata { get; set; } = new();
 
        public List<Type> CurrentTypes { get; set; } = new();
 
        public bool ResolutionInProgress { get; internal set; }
 
        internal void BeginResolveGraph()
        {
            if (ResolutionInProgress)
            {
                throw new InvalidOperationException("Cannot begin a new resolution graph while one is already in progress.");
            }
            ResolutionInProgress = true;
        }
 
        internal void EndResolveGraph()
        {
            if (!ResolutionInProgress)
            {
                throw new InvalidOperationException("Cannot end a resolution graph while one is not in progress.");
            }
            ResolutionInProgress = false;
            CurrentTypes.Clear();
        }
 
        internal void Track(Type type)
        {
            CurrentTypes.Add(type);
        }
 
        internal void Untrack(Type type)
        {
            CurrentTypes.Remove(type);
        }
    }
 
    private static partial class Log
    {
        [LoggerMessage(1, LogLevel.Debug, "Begin resolve metadata graph for type '{Type}'.", EventName = nameof(StartResolveMetadataGraph))]
        public static partial void StartResolveMetadataGraph(ILogger logger, Type type);
 
        [LoggerMessage(2, LogLevel.Debug, "End resolve metadata graph for type '{Type}'.", EventName = nameof(EndResolveMetadataGraph))]
        public static partial void EndResolveMetadataGraph(ILogger logger, Type type);
 
        [LoggerMessage(3, LogLevel.Debug, "Cached metadata found for type '{Type}'.", EventName = nameof(Metadata))]
        public static partial void MetadataFound(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(4, LogLevel.Debug, "No cached metadata graph for type '{Type}'.", EventName = nameof(NoMetadataFound))]
        public static partial void NoMetadataFound(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(5, LogLevel.Debug, "Recursive type '{Type}' found in the resolution graph '{Chain}'.", EventName = nameof(RecursiveTypeFound))]
        public static partial void RecursiveTypeFound(ILogger<FormDataMetadataFactory> logger, Type type, string chain);
 
        [LoggerMessage(6, LogLevel.Debug, "'{Type}' identified as primitive.", EventName = nameof(PrimitiveType))]
        public static partial void PrimitiveType(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(7, LogLevel.Debug, "'{Type}' identified as dictionary.", EventName = nameof(DictionaryType))]
        public static partial void DictionaryType(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(8, LogLevel.Debug, "'{Type}' identified as collection.", EventName = nameof(CollectionType))]
        public static partial void CollectionType(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(9, LogLevel.Debug, "'{Type}' identified as object.", EventName = nameof(ObjectType))]
        public static partial void ObjectType(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(10, LogLevel.Debug, "Constructor found for type '{Type}' with parameters '{Parameters}'.", EventName = nameof(ConstructorFound))]
        public static partial void ConstructorFound(ILogger<FormDataMetadataFactory> logger, Type type, string parameters);
 
        [LoggerMessage(11, LogLevel.Debug, "Constructor parameter '{Name}' of type '{ParameterType}' found for type '{Type}'.", EventName = nameof(ConstructorParameter))]
        public static partial void ConstructorParameter(ILogger<FormDataMetadataFactory> logger, Type type, string name, Type parameterType);
 
        [LoggerMessage(12, LogLevel.Debug, "Candidate property '{Name}' of type '{PropertyType}'.", EventName = nameof(CandidateProperty))]
        public static partial void CandidateProperty(ILogger<FormDataMetadataFactory> logger, string name, Type propertyType);
 
        [LoggerMessage(13, LogLevel.Debug, "Candidate property {PropertyName} has a matching constructor parameter '{ConstructorParameterName}'.", EventName = nameof(MatchingConstructorParameterFound))]
        public static partial void MatchingConstructorParameterFound(ILogger<FormDataMetadataFactory> logger, string constructorParameterName, string propertyName);
 
        [LoggerMessage(14, LogLevel.Debug, "Candidate property or constructor parameter {PropertyName} defines a custom name '{CustomName}'.", EventName = nameof(CustomParameterNameMetadata))]
        public static partial void CustomParameterNameMetadata(ILogger<FormDataMetadataFactory> logger, string customName, string propertyName);
 
        [LoggerMessage(15, LogLevel.Debug, "Candidate property {Name} will not be mapped. It has been explicitly ignored.", EventName = nameof(IgnoredProperty))]
        public static partial void IgnoredProperty(ILogger<FormDataMetadataFactory> logger, string name);
 
        [LoggerMessage(16, LogLevel.Debug, "Candidate property {Name} will not be mapped. It has no public setter.", EventName = nameof(NonPublicSetter))]
        public static partial void NonPublicSetter(ILogger<FormDataMetadataFactory> logger, string name);
 
        [LoggerMessage(17, LogLevel.Debug, "Candidate property {Name} is marked as required.", EventName = nameof(PropertyRequired))]
        public static partial void PropertyRequired(ILogger<FormDataMetadataFactory> logger, string name);
 
        [LoggerMessage(18, LogLevel.Debug, "Metadata created for {Type}.", EventName = nameof(MetadataComputed))]
        public static partial void MetadataComputed(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(19, LogLevel.Debug, "Can not map type generic type definition '{Type}'.", EventName = nameof(GenericTypeDefinitionNotSupported))]
        public static partial void GenericTypeDefinitionNotSupported(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(20, LogLevel.Debug, "Unable to select a constructor. Multiple public constructors found for type '{Type}'.", EventName = nameof(MultiplePublicConstructorsFound))]
        public static partial void MultiplePublicConstructorsFound(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(21, LogLevel.Debug, "Can not map interface type '{Type}'.", EventName = nameof(InterfacesNotSupported))]
        public static partial void InterfacesNotSupported(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(22, LogLevel.Debug, "Can not map abstract type '{Type}'.", EventName = nameof(AbstractClassesNotSupported))]
        public static partial void AbstractClassesNotSupported(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(23, LogLevel.Debug, "Unable to select a constructor. No public constructors found for type '{Type}'.", EventName = nameof(NoPublicConstructorFound))]
        public static partial void NoPublicConstructorFound(ILogger<FormDataMetadataFactory> logger, Type type);
 
        [LoggerMessage(24, LogLevel.Debug, "Can not map type '{Type}'. Constructor parameter {Name} of type {ParameterType} is not supported.", EventName = nameof(ConstructorParameterTypeNotSupported))]
        public static partial void ConstructorParameterTypeNotSupported(ILogger<FormDataMetadataFactory> logger, Type type, string name, Type parameterType);
 
        [LoggerMessage(25, LogLevel.Debug, "Can not map type '{Type}'. Property {Name} of type {PropertyType} is not supported.", EventName = nameof(PropertyTypeNotSupported))]
        public static partial void PropertyTypeNotSupported(ILogger<FormDataMetadataFactory> logger, Type type, string name, Type propertyType);
    }
}