// 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.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc.ApplicationModels
{
///
/// A default implementation of .
///
public class DefaultActionModelBuilder : IActionModelBuilder
{
///
public IEnumerable BuildActionModels([NotNull] TypeInfo typeInfo, [NotNull] MethodInfo methodInfo)
{
if (!IsAction(typeInfo, methodInfo))
{
return Enumerable.Empty();
}
// CoreCLR returns IEnumerable from GetCustomAttributes - the OfType
// is needed to so that the result of ToArray() is object
var attributes = methodInfo.GetCustomAttributes(inherit: true).OfType().ToArray();
// Route attributes create multiple actions, we want to split the set of
// attributes based on these so each action only has the attributes that affect it.
//
// The set of route attributes are split into those that 'define' a route versus those that are
// 'silent'.
//
// We need to define from action for each attribute that 'defines' a route, and a single action
// for all of the ones that don't (if any exist).
//
// Ex:
// [HttpGet]
// [AcceptVerbs("POST", "PUT")]
// [Route("Api/Things")]
// public void DoThing()
//
// This will generate 2 actions:
// 1. [Route("Api/Things")]
// 2. [HttpGet], [AcceptVerbs("POST", "PUT")]
//
// Note that having a route attribute that doesn't define a route template _might_ be an error. We
// don't have enough context to really know at this point so we just pass it on.
var splitAttributes = new List();
var hasSilentRouteAttribute = false;
foreach (var attribute in attributes)
{
var routeTemplateProvider = attribute as IRouteTemplateProvider;
if (routeTemplateProvider != null)
{
if (IsSilentRouteAttribute(routeTemplateProvider))
{
hasSilentRouteAttribute = true;
}
else
{
splitAttributes.Add(attribute);
}
}
}
var actionModels = new List();
if (splitAttributes.Count == 0 && !hasSilentRouteAttribute)
{
actionModels.Add(CreateActionModel(methodInfo, attributes));
}
else
{
foreach (var splitAttribute in splitAttributes)
{
var filteredAttributes = new List();
foreach (var attribute in attributes)
{
if (attribute == splitAttribute)
{
filteredAttributes.Add(attribute);
}
else if (attribute is IRouteTemplateProvider)
{
// Exclude other route template providers
}
else
{
filteredAttributes.Add(attribute);
}
}
actionModels.Add(CreateActionModel(methodInfo, filteredAttributes));
}
if (hasSilentRouteAttribute)
{
var filteredAttributes = new List();
foreach (var attribute in attributes)
{
if (!splitAttributes.Contains(attribute))
{
filteredAttributes.Add(attribute);
}
}
actionModels.Add(CreateActionModel(methodInfo, filteredAttributes));
}
}
foreach (var actionModel in actionModels)
{
foreach (var parameterInfo in actionModel.ActionMethod.GetParameters())
{
var parameterModel = CreateParameterModel(parameterInfo);
if (parameterModel != null)
{
parameterModel.Action = actionModel;
actionModel.Parameters.Add(parameterModel);
}
}
}
return actionModels;
}
///
/// Returns true if the is an action. Otherwise false .
///
/// The .
/// The .
/// true if the is an action. Otherwise false .
///
/// Override this method to provide custom logic to determine which methods are considered actions.
///
protected virtual bool IsAction([NotNull] TypeInfo typeInfo, [NotNull] MethodInfo methodInfo)
{
// The SpecialName bit is set to flag members that are treated in a special way by some compilers
// (such as property accessors and operator overloading methods).
if (methodInfo.IsSpecialName)
{
return false;
}
if (methodInfo.IsDefined(typeof(NonActionAttribute)))
{
return false;
}
// Overriden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid.
if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object))
{
return false;
}
// Dispose method implemented from IDisposable is not valid
if (IsIDisposableMethod(methodInfo, typeInfo))
{
return false;
}
if (methodInfo.IsStatic)
{
return false;
}
if (methodInfo.IsAbstract)
{
return false;
}
if (methodInfo.IsConstructor)
{
return false;
}
if (methodInfo.IsGenericMethod)
{
return false;
}
return
methodInfo.IsPublic;
}
private bool IsIDisposableMethod(MethodInfo methodInfo, TypeInfo typeInfo)
{
return
(typeof(IDisposable).GetTypeInfo().IsAssignableFrom(typeInfo) &&
typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0] == methodInfo);
}
///
/// Creates an for the given .
///
/// The .
/// The set of attributes to use as metadata.
/// An for the given .
///
/// An action-method in code may expand into multiple instances depending on how
/// the action is routed. In the case of multiple routing attributes, this method will invoked be once for
/// each action that can be created.
///
/// If overriding this method, use the provided list to find metadata related to
/// the action being created.
///
protected virtual ActionModel CreateActionModel(
[NotNull] MethodInfo methodInfo,
[NotNull] IReadOnlyList attributes)
{
var actionModel = new ActionModel(methodInfo, attributes)
{
IsActionNameMatchRequired = true,
};
actionModel.ActionConstraints.AddRange(attributes.OfType());
actionModel.Filters.AddRange(attributes.OfType());
var actionName = attributes.OfType().FirstOrDefault();
if (actionName?.Name != null)
{
actionModel.ActionName = actionName.Name;
}
else
{
actionModel.ActionName = methodInfo.Name;
}
var apiVisibility = attributes.OfType().FirstOrDefault();
if (apiVisibility != null)
{
actionModel.ApiExplorer.IsVisible = !apiVisibility.IgnoreApi;
}
var apiGroupName = attributes.OfType().FirstOrDefault();
if (apiGroupName != null)
{
actionModel.ApiExplorer.GroupName = apiGroupName.GroupName;
}
var httpMethods = attributes.OfType();
actionModel.HttpMethods.AddRange(
httpMethods
.Where(a => a.HttpMethods != null)
.SelectMany(a => a.HttpMethods)
.Distinct());
var routeTemplateProvider = attributes.OfType().FirstOrDefault();
if (routeTemplateProvider != null && !IsSilentRouteAttribute(routeTemplateProvider))
{
actionModel.AttributeRouteModel = new AttributeRouteModel(routeTemplateProvider);
}
return actionModel;
}
///
/// Creates a for the given .
///
/// The .
/// A for the given .
protected virtual ParameterModel CreateParameterModel([NotNull] ParameterInfo parameterInfo)
{
// CoreCLR returns IEnumerable from GetCustomAttributes - the OfType
// is needed to so that the result of ToArray() is object
var attributes = parameterInfo.GetCustomAttributes(inherit: true).OfType().ToArray();
var parameterModel = new ParameterModel(parameterInfo, attributes);
parameterModel.BinderMetadata = attributes.OfType().FirstOrDefault();
parameterModel.ParameterName = parameterInfo.Name;
parameterModel.IsOptional = parameterInfo.HasDefaultValue;
return parameterModel;
}
private bool IsSilentRouteAttribute(IRouteTemplateProvider routeTemplateProvider)
{
return
routeTemplateProvider.Template == null &&
routeTemplateProvider.Order == null &&
routeTemplateProvider.Name == null;
}
}
}