// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { public abstract class AssociatedMetadataProvider : IModelMetadataProvider where TModelMetadata : ModelMetadata { private readonly ConcurrentDictionary _typeInfoCache = new ConcurrentDictionary(); private readonly ConcurrentDictionary> _typePropertyInfoCache = new ConcurrentDictionary>(); public IEnumerable GetMetadataForProperties([NotNull] Type containerType) { return GetMetadataForPropertiesCore(containerType); } public ModelMetadata GetMetadataForProperty([NotNull] Type containerType, [NotNull] string propertyName) { if (string.IsNullOrEmpty(propertyName)) { throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(propertyName)); } var typePropertyInfo = GetTypePropertyInformation(containerType); PropertyInformation propertyInfo; if (!typePropertyInfo.TryGetValue(propertyName, out propertyInfo)) { var message = Resources.FormatCommon_PropertyNotFound(containerType, propertyName); throw new ArgumentException(message, nameof(propertyName)); } return CreatePropertyMetadata(propertyInfo); } public ModelMetadata GetMetadataForType([NotNull] Type modelType) { var prototype = GetTypeInformation(modelType); return CreateMetadataFromPrototype(prototype); } public ModelMetadata GetMetadataForParameter( [NotNull] MethodInfo methodInfo, [NotNull] string parameterName) { if (string.IsNullOrEmpty(parameterName)) { throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(parameterName)); } var parameter = methodInfo.GetParameters().FirstOrDefault( param => StringComparer.Ordinal.Equals(param.Name, parameterName)); if (parameter == null) { var message = Resources.FormatCommon_ParameterNotFound(parameterName); throw new ArgumentException(message, nameof(parameterName)); } return GetMetadataForParameterCore(parameterName, parameter); } // Override for creating the prototype metadata (without the model accessor). /// /// Creates a new instance. /// /// The set of attributes relevant for the new instance. /// /// containing this property. null unless this /// describes a property. /// /// this describes. /// /// Name of the property (in ) or parameter this /// describes. null or empty if this /// describes a . /// /// A new instance. protected abstract TModelMetadata CreateMetadataPrototype(IEnumerable attributes, Type containerType, Type modelType, string propertyName); // Override for applying the prototype + model accessor to yield the final metadata. /// /// Creates a new instance based on a . /// /// /// that provides the basis for new instance. /// /// Accessor for model value of new instance. /// /// A new instance based on . /// protected abstract TModelMetadata CreateMetadataFromPrototype(TModelMetadata prototype); private ModelMetadata GetMetadataForParameterCore( string parameterName, ParameterInfo parameter) { var parameterInfo = CreateParameterInfo(parameter.ParameterType, ModelAttributes.GetAttributesForParameter(parameter), parameterName); var metadata = CreateMetadataFromPrototype(parameterInfo.Prototype); return metadata; } private IEnumerable GetMetadataForPropertiesCore(Type containerType) { var typePropertyInfo = GetTypePropertyInformation(containerType); foreach (var kvp in typePropertyInfo) { var propertyInfo = kvp.Value; var propertyMetadata = CreatePropertyMetadata(propertyInfo); yield return propertyMetadata; } } private TModelMetadata CreatePropertyMetadata(PropertyInformation propertyInfo) { var metadata = CreateMetadataFromPrototype(propertyInfo.Prototype); if (propertyInfo.IsReadOnly) { metadata.IsReadOnly = true; } return metadata; } private TModelMetadata GetTypeInformation(Type type, IEnumerable associatedAttributes = null) { // This retrieval is implemented as a TryGetValue/TryAdd instead of a GetOrAdd // to avoid the performance cost of creating instance delegates TModelMetadata typeInfo; if (!_typeInfoCache.TryGetValue(type, out typeInfo)) { typeInfo = CreateTypeInformation(type, associatedAttributes); _typeInfoCache.TryAdd(type, typeInfo); } return typeInfo; } private Dictionary GetTypePropertyInformation(Type type) { // This retrieval is implemented as a TryGetValue/TryAdd instead of a GetOrAdd // to avoid the performance cost of creating instance delegates Dictionary typePropertyInfo; if (!_typePropertyInfoCache.TryGetValue(type, out typePropertyInfo)) { typePropertyInfo = GetPropertiesLookup(type); _typePropertyInfoCache.TryAdd(type, typePropertyInfo); } return typePropertyInfo; } private TModelMetadata CreateTypeInformation(Type type, IEnumerable associatedAttributes) { var attributes = ModelAttributes.GetAttributesForType(type); if (associatedAttributes != null) { attributes = attributes.Concat(associatedAttributes); } return CreateMetadataPrototype(attributes, containerType: null, modelType: type, propertyName: null); } private PropertyInformation CreatePropertyInformation(Type containerType, PropertyHelper helper) { var property = helper.Property; var attributes = ModelAttributes.GetAttributesForProperty(containerType, property); return new PropertyInformation { PropertyHelper = helper, Prototype = CreateMetadataPrototype(attributes, containerType, property.PropertyType, property.Name), IsReadOnly = !property.CanWrite || property.SetMethod.IsPrivate }; } private Dictionary GetPropertiesLookup(Type containerType) { var properties = new Dictionary(StringComparer.Ordinal); foreach (var propertyHelper in PropertyHelper.GetProperties(containerType)) { // Avoid re-generating a property descriptor if one has already been generated for the property name if (!properties.ContainsKey(propertyHelper.Name)) { properties.Add(propertyHelper.Name, CreatePropertyInformation(containerType, propertyHelper)); } } return properties; } private ParameterInformation CreateParameterInfo( Type parameterType, IEnumerable attributes, string parameterName) { var metadataProtoType = CreateMetadataPrototype(attributes: attributes, containerType: null, modelType: parameterType, propertyName: parameterName); return new ParameterInformation { Prototype = metadataProtoType }; } private sealed class ParameterInformation { public TModelMetadata Prototype { get; set; } } private sealed class PropertyInformation { public PropertyHelper PropertyHelper { get; set; } public TModelMetadata Prototype { get; set; } public bool IsReadOnly { get; set; } } } }