// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using Microsoft.Web.Http.Data.Metadata; namespace Microsoft.Web.Http.Data { public class DataControllerDescription { private static readonly ConcurrentDictionary _descriptionMap = new ConcurrentDictionary(); private static ConcurrentDictionary> _typeDescriptionProviderMap = new ConcurrentDictionary>(); private static readonly string[] _deletePrefixes = { "Delete", "Remove" }; private static readonly string[] _insertPrefixes = { "Insert", "Add", "Create" }; private static readonly string[] _updatePrefixes = { "Update", "Change", "Modify" }; private Type _dataControllerType; private ReadOnlyCollection _entityTypes; private List _updateActions; internal DataControllerDescription(Type dataControllerType, IEnumerable entityTypes, List actions) { _dataControllerType = dataControllerType; _entityTypes = entityTypes.ToList().AsReadOnly(); _updateActions = actions; } /// /// Gets the Type of the /// public Type ControllerType { get { return _dataControllerType; } } /// /// Gets the entity types exposed by the /// public IEnumerable EntityTypes { get { return _entityTypes; } } public static DataControllerDescription GetDescription(HttpControllerDescriptor controllerDescriptor) { return _descriptionMap.GetOrAdd(controllerDescriptor.ControllerType, type => { return CreateDescription(controllerDescriptor); }); } /// /// Creates and returns the metadata provider for the specified DataController Type. /// /// The DataController Type. /// The metadata provider. internal static MetadataProvider CreateMetadataProvider(Type dataControllerType) { // construct a list of all types in the inheritance hierarchy for the controller List baseTypes = new List(); Type currType = dataControllerType; while (currType != typeof(DataController)) { baseTypes.Add(currType); currType = currType.BaseType; } // create our base reflection provider List providerList = new List(); ReflectionMetadataProvider reflectionProvider = new ReflectionMetadataProvider(); // Set the IsEntity function which consults the chain of providers. Func isEntityTypeFunc = (t) => providerList.Any(p => p.LookUpIsEntityType(t)); reflectionProvider.SetIsEntityTypeFunc(isEntityTypeFunc); // Now from most derived to base, create any declared metadata providers, // chaining the instances as we progress. Note that ordering from derived to // base is important - we want to ensure that any providers the user has placed on // their DataController directly come before any DAL providers. MetadataProvider currProvider = reflectionProvider; providerList.Add(currProvider); for (int i = 0; i < baseTypes.Count; i++) { currType = baseTypes[i]; // Reflection rather than TD is used here so we only get explicit // Type attributes. TD inherits attributes by default, even if the // attributes aren't inheritable. foreach (MetadataProviderAttribute providerAttribute in currType.GetCustomAttributes(typeof(MetadataProviderAttribute), false)) { currProvider = providerAttribute.CreateProvider(dataControllerType, currProvider); currProvider.SetIsEntityTypeFunc(isEntityTypeFunc); providerList.Add(currProvider); } } return currProvider; } private static DataControllerDescription CreateDescription(HttpControllerDescriptor controllerDescriptor) { Type dataControllerType = controllerDescriptor.ControllerType; MetadataProvider metadataProvider = CreateMetadataProvider(dataControllerType); // get all public candidate methods and create the operations HashSet entityTypes = new HashSet(); List actions = new List(); IEnumerable methodsToInspect = dataControllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public) .Where(p => (p.DeclaringType != typeof(DataController) && (p.DeclaringType != typeof(object))) && !p.IsSpecialName); foreach (MethodInfo method in methodsToInspect) { if (method.GetCustomAttributes(typeof(NonActionAttribute), false).Length > 0) { continue; } if (method.IsVirtual && method.GetBaseDefinition().DeclaringType == typeof(DataController)) { // don't want to infer overrides of DataController virtual methods as // operations continue; } // We need to ensure the buddy metadata provider is registered BEFORE we // attempt to do convention, since we rely on IsEntity which relies on // KeyAttributes being present (possibly from "buddy" classes) RegisterAssociatedMetadataProvider(method); ChangeOperation operationType = ClassifyUpdateOperation(method, metadataProvider); if (operationType != ChangeOperation.None) { Type entityType = method.GetParameters()[0].ParameterType; UpdateActionDescriptor actionDescriptor = new UpdateActionDescriptor(controllerDescriptor, method, entityType, operationType); ValidateAction(actionDescriptor); actions.Add(actionDescriptor); // TODO : currently considering entity types w/o any query methods // exposing them. Should we? if (metadataProvider.IsEntityType(entityType)) { AddEntityType(entityType, entityTypes, metadataProvider); } } else { // if the method is a "query" operation returning an entity, // add to entity types if (method.ReturnType != typeof(void)) { Type returnType = TypeUtility.UnwrapTaskInnerType(method.ReturnType); Type elementType = TypeUtility.GetElementType(returnType); if (metadataProvider.IsEntityType(elementType)) { AddEntityType(elementType, entityTypes, metadataProvider); } } } } return new DataControllerDescription(dataControllerType, entityTypes, actions); } /// /// Adds the specified entity type and any associated entity types recursively to the specified set. /// /// The entity Type to add. /// The types set to accumulate in. /// The metadata provider. private static void AddEntityType(Type entityType, HashSet entityTypes, MetadataProvider metadataProvider) { if (entityTypes.Contains(entityType)) { // already added this type return; } entityTypes.Add(entityType); RegisterDataControllerTypeDescriptionProvider(entityType, metadataProvider); foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(entityType)) { // for any "exposed" association members, recursively add the associated // entity type if (pd.Attributes[typeof(AssociationAttribute)] != null && TypeUtility.IsDataMember(pd)) { Type includedEntityType = TypeUtility.GetElementType(pd.PropertyType); if (metadataProvider.IsEntityType(entityType)) { AddEntityType(includedEntityType, entityTypes, metadataProvider); } } } // Recursively add any derived entity types specified by [KnownType] // attributes IEnumerable knownTypes = TypeUtility.GetKnownTypes(entityType, true); foreach (Type knownType in knownTypes) { if (entityType.IsAssignableFrom(knownType)) { AddEntityType(knownType, entityTypes, metadataProvider); } } } private static void ValidateAction(UpdateActionDescriptor updateAction) { // Only authorization filters are supported on CUD actions. This will capture 99% of user errors. // There is the chance that someone might attempt to implement an attribute that implements both // IAuthorizationFilter AND another filter type, but we don't want to have a black-list of filter // types here. if (updateAction.GetFilters().Any(p => !typeof(AuthorizationFilterAttribute).IsAssignableFrom(p.GetType()))) { throw Error.NotSupported(Resource.InvalidAction_UnsupportedFilterType, updateAction.ControllerDescriptor.ControllerType.Name, updateAction.ActionName); } } private static ChangeOperation ClassifyUpdateOperation(MethodInfo method, MetadataProvider metadataProvider) { ChangeOperation operationType; AttributeCollection methodAttributes = new AttributeCollection(method.GetCustomAttributes(false).Cast().ToArray()); // Check if explicit attributes exist. if (methodAttributes[typeof(InsertAttribute)] != null) { operationType = ChangeOperation.Insert; } else if (methodAttributes[typeof(UpdateAttribute)] != null) { UpdateAttribute updateAttribute = (UpdateAttribute)methodAttributes[typeof(UpdateAttribute)]; if (updateAttribute.UsingCustomMethod) { operationType = ChangeOperation.Custom; } else { operationType = ChangeOperation.Update; } } else if (methodAttributes[typeof(DeleteAttribute)] != null) { operationType = ChangeOperation.Delete; } else { return TryClassifyUpdateOperationImplicit(method, metadataProvider); } return operationType; } private static ChangeOperation TryClassifyUpdateOperationImplicit(MethodInfo method, MetadataProvider metadataProvider) { ChangeOperation operationType = ChangeOperation.None; if (method.ReturnType == typeof(void)) { // Check if this looks like an insert, update or delete method. if (_insertPrefixes.Any(p => method.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase))) { operationType = ChangeOperation.Insert; } else if (_updatePrefixes.Any(p => method.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase))) { operationType = ChangeOperation.Update; } else if (_deletePrefixes.Any(p => method.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase))) { operationType = ChangeOperation.Delete; } else if (IsCustomUpdateMethod(method, metadataProvider)) { operationType = ChangeOperation.Custom; } } return operationType; } private static bool IsCustomUpdateMethod(MethodInfo method, MetadataProvider metadataProvider) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 0) { return false; } if (method.ReturnType != typeof(void)) { return false; } return metadataProvider.IsEntityType(parameters[0].ParameterType); } /// /// Register the associated metadata provider for Types in the signature /// of the specified method as required. /// /// The method to register for. private static void RegisterAssociatedMetadataProvider(MethodInfo methodInfo) { Type type = TypeUtility.GetElementType(methodInfo.ReturnType); if (type != typeof(void) && type.GetCustomAttributes(typeof(MetadataTypeAttribute), true).Length != 0) { RegisterAssociatedMetadataTypeTypeDescriptor(type); } foreach (ParameterInfo parameter in methodInfo.GetParameters()) { type = parameter.ParameterType; if (type != typeof(void) && type.GetCustomAttributes(typeof(MetadataTypeAttribute), true).Length != 0) { RegisterAssociatedMetadataTypeTypeDescriptor(type); } } } /// /// Verifies that the reference does not contain a cyclic reference and /// registers the AssociatedMetadataTypeTypeDescriptionProvider in that case. /// /// The entity type with the MetadataType attribute. private static void RegisterAssociatedMetadataTypeTypeDescriptor(Type type) { Type currentType = type; HashSet metadataTypeReferences = new HashSet(); metadataTypeReferences.Add(currentType); while (true) { MetadataTypeAttribute attribute = (MetadataTypeAttribute)Attribute.GetCustomAttribute(currentType, typeof(MetadataTypeAttribute)); if (attribute == null) { break; } else { currentType = attribute.MetadataClassType; // If we find a cyclic reference, throw an error. if (metadataTypeReferences.Contains(currentType)) { throw Error.InvalidOperation(Resource.CyclicMetadataTypeAttributesFound, type.FullName); } else { metadataTypeReferences.Add(currentType); } } } // If the MetadataType reference chain doesn't contain a cycle, register the use of the AssociatedMetadataTypeTypeDescriptionProvider. RegisterCustomTypeDescriptor(new AssociatedMetadataTypeTypeDescriptionProvider(type), type); } // The JITer enforces CAS. By creating a separate method we can avoid getting SecurityExceptions // when we weren't going to really call TypeDescriptor.AddProvider. internal static void RegisterCustomTypeDescriptor(TypeDescriptionProvider tdp, Type type) { // Check if we already registered provider with the specified type. HashSet existingProviders = _typeDescriptionProviderMap.GetOrAdd(type, t => { return new HashSet(); }); if (!existingProviders.Contains(tdp.GetType())) { TypeDescriptor.AddProviderTransparent(tdp, type); existingProviders.Add(tdp.GetType()); } } /// /// Register our DataControllerTypeDescriptionProvider for the specified Type. This provider is responsible for surfacing the /// custom TDs returned by metadata providers. /// /// The Type that we should register for. /// The metadata provider. private static void RegisterDataControllerTypeDescriptionProvider(Type type, MetadataProvider metadataProvider) { DataControllerTypeDescriptionProvider tdp = new DataControllerTypeDescriptionProvider(type, metadataProvider); RegisterCustomTypeDescriptor(tdp, type); } public UpdateActionDescriptor GetUpdateAction(string name) { return _updateActions.FirstOrDefault(p => p.ActionName == name); } public UpdateActionDescriptor GetUpdateAction(Type entityType, ChangeOperation operationType) { return _updateActions.FirstOrDefault(p => (p.EntityType == entityType) && (p.ChangeOperation == operationType)); } public UpdateActionDescriptor GetCustomMethod(Type entityType, string methodName) { if (entityType == null) { throw Error.ArgumentNull("entityType"); } if (methodName == null) { throw Error.ArgumentNull("methodName"); } return _updateActions.FirstOrDefault(p => (p.EntityType == entityType) && (p.ChangeOperation == ChangeOperation.Custom) && (p.ActionName == methodName)); } /// /// This is the default provider in the metadata provider chain. It is based solely on /// attributes applied directly to types (either via CLR attributes, or via "buddy" metadata class). /// private class ReflectionMetadataProvider : MetadataProvider { public ReflectionMetadataProvider() : base(parent: null) { } /// /// Returns true if the Type has at least one member marked with KeyAttribute. /// /// The Type to check. /// True if the Type is an entity, false otherwise. public override bool LookUpIsEntityType(Type type) { return TypeDescriptor.GetProperties(type).Cast().Any(p => p.Attributes[typeof(KeyAttribute)] != null); } } } }