| File: ModelBinding\Metadata\DefaultModelMetadataProvider.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable enable using System.Collections.Concurrent; using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; /// <summary> /// A default implementation of <see cref="IModelMetadataProvider"/> based on reflection. /// </summary> public class DefaultModelMetadataProvider : ModelMetadataProvider { private readonly ConcurrentDictionary<ModelMetadataIdentity, ModelMetadataCacheEntry> _modelMetadataCache = new(); private readonly Func<ModelMetadataIdentity, ModelMetadataCacheEntry> _cacheEntryFactory; private readonly ModelMetadataCacheEntry _metadataCacheEntryForObjectType; /// <summary> /// Creates a new <see cref="DefaultModelMetadataProvider"/>. /// </summary> /// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param> public DefaultModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider) : this(detailsProvider, new DefaultModelBindingMessageProvider()) { } /// <summary> /// Creates a new <see cref="DefaultModelMetadataProvider"/>. /// </summary> /// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param> /// <param name="optionsAccessor">The accessor for <see cref="MvcOptions"/>.</param> public DefaultModelMetadataProvider( ICompositeMetadataDetailsProvider detailsProvider, IOptions<MvcOptions> optionsAccessor) : this(detailsProvider, GetMessageProvider(optionsAccessor)) { } private DefaultModelMetadataProvider( ICompositeMetadataDetailsProvider detailsProvider, DefaultModelBindingMessageProvider modelBindingMessageProvider) { ArgumentNullException.ThrowIfNull(detailsProvider); DetailsProvider = detailsProvider; ModelBindingMessageProvider = modelBindingMessageProvider; _cacheEntryFactory = CreateCacheEntry; _metadataCacheEntryForObjectType = GetMetadataCacheEntryForObjectType(); } /// <summary> /// Gets the <see cref="ICompositeMetadataDetailsProvider"/>. /// </summary> protected ICompositeMetadataDetailsProvider DetailsProvider { get; } /// <summary> /// Gets the <see cref="Metadata.DefaultModelBindingMessageProvider"/>. /// </summary> /// <value>Same as <see cref="MvcOptions.ModelBindingMessageProvider"/> in all production scenarios.</value> protected DefaultModelBindingMessageProvider ModelBindingMessageProvider { get; } internal void ClearCache() => _modelMetadataCache.Clear(); /// <inheritdoc /> public override IEnumerable<ModelMetadata> GetMetadataForProperties(Type modelType) { ArgumentNullException.ThrowIfNull(modelType); var cacheEntry = GetCacheEntry(modelType); // We're relying on a safe race-condition for Properties - take care only // to set the value once the properties are fully-initialized. if (cacheEntry.Details.Properties == null) { var key = ModelMetadataIdentity.ForType(modelType); var propertyDetails = CreatePropertyDetails(key); var properties = new ModelMetadata[propertyDetails.Length]; for (var i = 0; i < properties.Length; i++) { propertyDetails[i].ContainerMetadata = cacheEntry.Metadata; properties[i] = CreateModelMetadata(propertyDetails[i]); } cacheEntry.Details.Properties = properties; } return cacheEntry.Details.Properties; } /// <inheritdoc /> public override ModelMetadata GetMetadataForParameter(ParameterInfo parameter) => GetMetadataForParameter(parameter, parameter.ParameterType); /// <inheritdoc /> public override ModelMetadata GetMetadataForParameter(ParameterInfo parameter, Type modelType) { ArgumentNullException.ThrowIfNull(parameter); ArgumentNullException.ThrowIfNull(modelType); var cacheEntry = GetCacheEntry(parameter, modelType); return cacheEntry.Metadata; } /// <inheritdoc /> public override ModelMetadata GetMetadataForType(Type modelType) { ArgumentNullException.ThrowIfNull(modelType); var cacheEntry = GetCacheEntry(modelType); return cacheEntry.Metadata; } /// <inheritdoc /> public override ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, Type modelType) { ArgumentNullException.ThrowIfNull(propertyInfo); ArgumentNullException.ThrowIfNull(modelType); var cacheEntry = GetCacheEntry(propertyInfo, modelType); return cacheEntry.Metadata; } /// <inheritdoc /> public override ModelMetadata GetMetadataForConstructor(ConstructorInfo constructorInfo, Type modelType) { ArgumentNullException.ThrowIfNull(constructorInfo); var cacheEntry = GetCacheEntry(constructorInfo, modelType); return cacheEntry.Metadata; } private static DefaultModelBindingMessageProvider GetMessageProvider(IOptions<MvcOptions> optionsAccessor) { ArgumentNullException.ThrowIfNull(optionsAccessor); return optionsAccessor.Value.ModelBindingMessageProvider; } private ModelMetadataCacheEntry GetCacheEntry(Type modelType) { ModelMetadataCacheEntry cacheEntry; // Perf: We cached model metadata cache entry for "object" type to save ConcurrentDictionary lookups. if (modelType == typeof(object)) { cacheEntry = _metadataCacheEntryForObjectType; } else { var key = ModelMetadataIdentity.ForType(modelType); cacheEntry = _modelMetadataCache.GetOrAdd(key, _cacheEntryFactory); } return cacheEntry; } private ModelMetadataCacheEntry GetCacheEntry(ParameterInfo parameter, Type modelType) { return _modelMetadataCache.GetOrAdd( ModelMetadataIdentity.ForParameter(parameter, modelType), _cacheEntryFactory); } private ModelMetadataCacheEntry GetCacheEntry(PropertyInfo property, Type modelType) { return _modelMetadataCache.GetOrAdd( ModelMetadataIdentity.ForProperty(property, modelType, property.DeclaringType!), _cacheEntryFactory); } private ModelMetadataCacheEntry GetCacheEntry(ConstructorInfo constructor, Type modelType) { return _modelMetadataCache.GetOrAdd( ModelMetadataIdentity.ForConstructor(constructor, modelType), _cacheEntryFactory); } private ModelMetadataCacheEntry CreateCacheEntry(ModelMetadataIdentity key) { DefaultMetadataDetails details; if (key.MetadataKind == ModelMetadataKind.Constructor) { details = CreateConstructorDetails(key); } else if (key.MetadataKind == ModelMetadataKind.Parameter) { details = CreateParameterDetails(key); } else if (key.MetadataKind == ModelMetadataKind.Property) { details = CreateSinglePropertyDetails(key); } else { details = CreateTypeDetails(key); } var metadata = CreateModelMetadata(details); return new ModelMetadataCacheEntry(metadata, details); } private static DefaultMetadataDetails CreateSinglePropertyDetails(ModelMetadataIdentity propertyKey) { var propertyHelpers = PropertyHelper.GetVisibleProperties(propertyKey.ContainerType!); for (var i = 0; i < propertyHelpers.Length; i++) { var propertyHelper = propertyHelpers[i]; if (propertyHelper.Name == propertyKey.Name) { return CreateSinglePropertyDetails(propertyKey, propertyHelper); } } Debug.Fail($"Unable to find property '{propertyKey.Name}' on type '{propertyKey.ContainerType}."); return null; } private DefaultMetadataDetails CreateConstructorDetails(ModelMetadataIdentity constructorKey) { var constructor = constructorKey.ConstructorInfo; var parameters = constructor!.GetParameters(); var parameterMetadata = new ModelMetadata[parameters.Length]; var parameterTypes = new Type[parameters.Length]; for (var i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; var parameterDetails = CreateParameterDetails(ModelMetadataIdentity.ForParameter(parameter)); parameterMetadata[i] = CreateModelMetadata(parameterDetails); parameterTypes[i] = parameter.ParameterType; } var constructorDetails = new DefaultMetadataDetails(constructorKey, ModelAttributes.Empty); constructorDetails.BoundConstructorParameters = parameterMetadata; constructorDetails.BoundConstructorInvoker = CreateObjectFactory(constructor); return constructorDetails; static Func<object?[], object> CreateObjectFactory(ConstructorInfo constructor) { var args = Expression.Parameter(typeof(object?[]), "args"); var factoryExpressionBody = BuildFactoryExpression(constructor, args); var factoryLamda = Expression.Lambda<Func<object?[], object>>(factoryExpressionBody, args); return factoryLamda.Compile(); } } private static Expression BuildFactoryExpression( ConstructorInfo constructor, Expression factoryArgumentArray) { var constructorParameters = constructor.GetParameters(); var constructorArguments = new Expression[constructorParameters.Length]; for (var i = 0; i < constructorParameters.Length; i++) { var constructorParameter = constructorParameters[i]; var parameterType = constructorParameter.ParameterType; constructorArguments[i] = Expression.ArrayAccess(factoryArgumentArray, Expression.Constant(i)); if (ParameterDefaultValue.TryGetDefaultValue(constructorParameter, out var defaultValue)) { // We have a default value; } else if (parameterType.IsValueType) { defaultValue = Activator.CreateInstance(parameterType); } if (defaultValue != null) { var defaultValueExpression = Expression.Constant(defaultValue); constructorArguments[i] = Expression.Coalesce(constructorArguments[i], defaultValueExpression); } constructorArguments[i] = Expression.Convert(constructorArguments[i], parameterType); } return Expression.New(constructor, constructorArguments); } private ModelMetadataCacheEntry GetMetadataCacheEntryForObjectType() { var key = ModelMetadataIdentity.ForType(typeof(object)); var entry = CreateCacheEntry(key); return entry; } /// <summary> /// Creates a new <see cref="ModelMetadata"/> from a <see cref="DefaultMetadataDetails"/>. /// </summary> /// <param name="entry">The <see cref="DefaultMetadataDetails"/> entry with cached data.</param> /// <returns>A new <see cref="ModelMetadata"/> instance.</returns> /// <remarks> /// <see cref="DefaultModelMetadataProvider"/> will always create instances of /// <see cref="DefaultModelMetadata"/> .Override this method to create a <see cref="ModelMetadata"/> /// of a different concrete type. /// </remarks> protected virtual ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry) { return new DefaultModelMetadata(this, DetailsProvider, entry, ModelBindingMessageProvider); } /// <summary> /// Creates the <see cref="DefaultMetadataDetails"/> entries for the properties of a model /// <see cref="Type"/>. /// </summary> /// <param name="key"> /// The <see cref="ModelMetadataIdentity"/> identifying the model <see cref="Type"/>. /// </param> /// <returns>A details object for each property of the model <see cref="Type"/>.</returns> /// <remarks> /// The results of this method will be cached and used to satisfy calls to /// <see cref="GetMetadataForProperties(Type)"/>. Override this method to provide a different /// set of property data. /// </remarks> protected virtual DefaultMetadataDetails[] CreatePropertyDetails(ModelMetadataIdentity key) { var propertyHelpers = PropertyHelper.GetVisibleProperties(key.ModelType); var propertyEntries = new List<DefaultMetadataDetails>(propertyHelpers.Length); for (var i = 0; i < propertyHelpers.Length; i++) { var propertyHelper = propertyHelpers[i]; var propertyKey = ModelMetadataIdentity.ForProperty( propertyHelper.Property, propertyHelper.Property.PropertyType, key.ModelType); var propertyEntry = CreateSinglePropertyDetails(propertyKey, propertyHelper); propertyEntries.Add(propertyEntry); } return propertyEntries.ToArray(); } private static DefaultMetadataDetails CreateSinglePropertyDetails( ModelMetadataIdentity propertyKey, PropertyHelper propertyHelper) { Debug.Assert(propertyKey.MetadataKind == ModelMetadataKind.Property); var containerType = propertyKey.ContainerType!; var attributes = ModelAttributes.GetAttributesForProperty( containerType, propertyHelper.Property, propertyKey.ModelType); var propertyEntry = new DefaultMetadataDetails(propertyKey, attributes); if (propertyHelper.Property.CanRead && propertyHelper.Property.GetMethod?.IsPublic == true) { var getter = PropertyHelper.MakeNullSafeFastPropertyGetter(propertyHelper.Property); propertyEntry.PropertyGetter = getter; } if (propertyHelper.Property.CanWrite && propertyHelper.Property.SetMethod?.IsPublic == true && !containerType.IsValueType) { propertyEntry.PropertySetter = propertyHelper.ValueSetter; } return propertyEntry; } /// <summary> /// Creates the <see cref="DefaultMetadataDetails"/> entry for a model <see cref="Type"/>. /// </summary> /// <param name="key"> /// The <see cref="ModelMetadataIdentity"/> identifying the model <see cref="Type"/>. /// </param> /// <returns>A details object for the model <see cref="Type"/>.</returns> /// <remarks> /// The results of this method will be cached and used to satisfy calls to /// <see cref="GetMetadataForType(Type)"/>. Override this method to provide a different /// set of attributes. /// </remarks> protected virtual DefaultMetadataDetails CreateTypeDetails(ModelMetadataIdentity key) { return new DefaultMetadataDetails( key, ModelAttributes.GetAttributesForType(key.ModelType)); } /// <summary> /// Creates the <see cref="DefaultMetadataDetails"/> entry for a parameter <see cref="Type"/>. /// </summary> /// <param name="key"> /// The <see cref="ModelMetadataIdentity"/> identifying the parameter <see cref="Type"/>. /// </param> /// <returns>A details object for the parameter.</returns> protected virtual DefaultMetadataDetails CreateParameterDetails(ModelMetadataIdentity key) { return new DefaultMetadataDetails( key, ModelAttributes.GetAttributesForParameter(key.ParameterInfo!, key.ModelType)); } private readonly struct ModelMetadataCacheEntry { public ModelMetadataCacheEntry(ModelMetadata metadata, DefaultMetadataDetails details) { Metadata = metadata; Details = details; } public ModelMetadata Metadata { get; } public DefaultMetadataDetails Details { get; } } }