aspnetcore/src/Microsoft.AspNet.Mvc.ModelB.../Metadata/AssociatedMetadataProvider.cs

242 lines
10 KiB
C#

// 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<TModelMetadata> : IModelMetadataProvider
where TModelMetadata : ModelMetadata
{
private readonly ConcurrentDictionary<Type, TModelMetadata> _typeInfoCache =
new ConcurrentDictionary<Type, TModelMetadata>();
private readonly ConcurrentDictionary<Type, Dictionary<string, PropertyInformation>> _typePropertyInfoCache =
new ConcurrentDictionary<Type, Dictionary<string, PropertyInformation>>();
public IEnumerable<ModelMetadata> 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).
/// <summary>
/// Creates a new <typeparamref name="TModelMetadata"/> instance.
/// </summary>
/// <param name="attributes">The set of attributes relevant for the new instance.</param>
/// <param name="containerType">
/// <see cref="Type"/> containing this property. <c>null</c> unless this <typeparamref name="TModelMetadata"/>
/// describes a property.
/// </param>
/// <param name="modelType"><see cref="Type"/> this <typeparamref name="TModelMetadata"/> describes.</param>
/// <param name="propertyName">
/// Name of the property (in <paramref name="containerType"/>) or parameter this
/// <typeparamref name="TModelMetadata"/> describes. <c>null</c> or empty if this
/// <typeparamref name="TModelMetadata"/> describes a <see cref="Type"/>.
/// </param>
/// <returns>A new <typeparamref name="TModelMetadata"/> instance.</returns>
protected abstract TModelMetadata CreateMetadataPrototype(IEnumerable<object> attributes,
Type containerType,
Type modelType,
string propertyName);
// Override for applying the prototype + model accessor to yield the final metadata.
/// <summary>
/// Creates a new <typeparamref name="TModelMetadata"/> instance based on a <paramref name="prototype"/>.
/// </summary>
/// <param name="prototype">
/// <typeparamref name="TModelMetadata"/> that provides the basis for new instance.
/// </param>
/// <param name="modelAccessor">Accessor for model value of new instance.</param>
/// <returns>
/// A new <typeparamref name="TModelMetadata"/> instance based on <paramref name="prototype"/>.
/// </returns>
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<ModelMetadata> 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<Attribute> 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<string, PropertyInformation> 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<string, PropertyInformation> typePropertyInfo;
if (!_typePropertyInfoCache.TryGetValue(type, out typePropertyInfo))
{
typePropertyInfo = GetPropertiesLookup(type);
_typePropertyInfoCache.TryAdd(type, typePropertyInfo);
}
return typePropertyInfo;
}
private TModelMetadata CreateTypeInformation(Type type, IEnumerable<Attribute> 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<string, PropertyInformation> GetPropertiesLookup(Type containerType)
{
var properties = new Dictionary<string, PropertyInformation>(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<object> 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; }
}
}
}