using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; using System.Reflection.Emit; using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { public abstract class AssociatedMetadataProvider : IModelMetadataProvider where TModelMetadata : ModelMetadata { private readonly ConcurrentDictionary _typeInfoCache = new ConcurrentDictionary(); public IEnumerable GetMetadataForProperties(object container, [NotNull] Type containerType) { return GetMetadataForPropertiesCore(container, containerType); } public ModelMetadata GetMetadataForProperty(Func modelAccessor, [NotNull] Type containerType, [NotNull] string propertyName) { if (string.IsNullOrEmpty(propertyName)) { throw Error.ArgumentNullOrEmpty("propertyName"); } var typeInfo = GetTypeInformation(containerType); PropertyInformation propertyInfo; if (!typeInfo.Properties.TryGetValue(propertyName, out propertyInfo)) { throw Error.Argument("propertyName", Resources.FormatCommon_PropertyNotFound(containerType, propertyName)); } return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor); } public ModelMetadata GetMetadataForType(Func modelAccessor, [NotNull] Type modelType) { TModelMetadata prototype = GetTypeInformation(modelType).Prototype; return CreateMetadataFromPrototype(prototype, modelAccessor); } // Override for creating the prototype metadata (without the accessor) protected abstract TModelMetadata CreateMetadataPrototype(IEnumerable attributes, Type containerType, Type modelType, string propertyName); // Override for applying the prototype + modelAccess to yield the final metadata protected abstract TModelMetadata CreateMetadataFromPrototype(TModelMetadata prototype, Func modelAccessor); private IEnumerable GetMetadataForPropertiesCore(object container, Type containerType) { var typeInfo = GetTypeInformation(containerType); foreach (var kvp in typeInfo.Properties) { var propertyInfo = kvp.Value; Func modelAccessor = null; if (container != null) { Func propertyGetter = propertyInfo.ValueAccessor; modelAccessor = () => propertyGetter(container); } yield return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor); } } private TypeInformation 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 TypeInformation typeInfo; if (!_typeInfoCache.TryGetValue(type, out typeInfo)) { typeInfo = CreateTypeInformation(type, associatedAttributes); _typeInfoCache.TryAdd(type, typeInfo); } return typeInfo; } private TypeInformation CreateTypeInformation(Type type, IEnumerable associatedAttributes) { var typeInfo = type.GetTypeInfo(); var attributes = typeInfo.GetCustomAttributes(); if (associatedAttributes != null) { attributes = attributes.Concat(associatedAttributes); } var info = new TypeInformation { Prototype = CreateMetadataPrototype(attributes, containerType: null, modelType: type, propertyName: null) }; var properties = new Dictionary(); foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { // Avoid re-generating a property descriptor if one has already been generated for the property name if (!properties.ContainsKey(property.Name)) { properties.Add(property.Name, CreatePropertyInformation(type, property)); } } info.Properties = properties; return info; } private PropertyInformation CreatePropertyInformation(Type containerType, PropertyInfo property) { var info = new PropertyInformation(); info.ValueAccessor = CreatePropertyValueAccessor(property); info.Prototype = CreateMetadataPrototype(property.GetCustomAttributes(), containerType, property.PropertyType, property.Name); return info; } private static Func CreatePropertyValueAccessor(PropertyInfo property) { var declaringType = property.DeclaringType; var declaringTypeInfo = declaringType.GetTypeInfo(); if (declaringTypeInfo.IsVisible) { if (property.CanRead) { var getMethodInfo = property.GetMethod; if (getMethodInfo != null) { return CreateDynamicValueAccessor(getMethodInfo, declaringType, property.Name); } } } // If either the type isn't public or we can't find a public getter, use the slow Reflection path return container => property.GetValue(container); } // Uses Lightweight Code Gen to generate a tiny delegate that gets the property value // This is an optimization to avoid having to go through the much slower System.Reflection APIs // e.g. generates (object o) => (Person)o.Id private static Func CreateDynamicValueAccessor(MethodInfo getMethodInfo, Type declaringType, string propertyName) { Contract.Assert(getMethodInfo != null && getMethodInfo.IsPublic && !getMethodInfo.IsStatic); var declaringTypeInfo = declaringType.GetTypeInfo(); var propertyType = getMethodInfo.ReturnType; var dynamicMethod = new DynamicMethod("Get" + propertyName + "From" + declaringType.Name, typeof(object), new [] { typeof(object) }); var ilg = dynamicMethod.GetILGenerator(); // Load the container onto the stack, convert from object => declaring type for the property ilg.Emit(OpCodes.Ldarg_0); if (declaringTypeInfo.IsValueType) { ilg.Emit(OpCodes.Unbox, declaringType); } else { ilg.Emit(OpCodes.Castclass, declaringType); } // if declaring type is value type, we use Call : structs don't have inheritance // if get method is sealed or isn't virtual, we use Call : it can't be overridden if (declaringTypeInfo.IsValueType || !getMethodInfo.IsVirtual || getMethodInfo.IsFinal) { ilg.Emit(OpCodes.Call, getMethodInfo); } else { ilg.Emit(OpCodes.Callvirt, getMethodInfo); } // Box if the property type is a value type, so it can be returned as an object if (propertyType.GetTypeInfo().IsValueType) { ilg.Emit(OpCodes.Box, propertyType); } // Return property value ilg.Emit(OpCodes.Ret); return (Func)dynamicMethod.CreateDelegate(typeof(Func)); } private sealed class TypeInformation { public TModelMetadata Prototype { get; set; } public Dictionary Properties { get; set; } } private sealed class PropertyInformation { public Func ValueAccessor { get; set; } public TModelMetadata Prototype { get; set; } } } }