using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Reflection; namespace Microsoft.AspNet.Mvc { public class DefaultActionDiscoveryConventions : IActionDiscoveryConventions { private static readonly string[] _supportedHttpMethodsByConvention = { "GET", "POST", "PUT", "DELETE", "PATCH", }; private static readonly string[] _supportedHttpMethodsForDefaultMethod = { "GET", "POST" }; public virtual string DefaultMethodName { get { return "Index"; } } public virtual bool IsController([NotNull] TypeInfo typeInfo) { if (!typeInfo.IsClass || typeInfo.IsAbstract || typeInfo.ContainsGenericParameters) { return false; } if (typeInfo.Name.Equals("Controller", StringComparison.OrdinalIgnoreCase)) { return false; } return typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo); } // If the convention is All methods starting with Get do not have an action name, // for a input GetXYZ methodInfo, the return value will be // { { HttpMethods = "GET", ActionName = "GetXYZ", RequireActionNameMatch = false }} public virtual IEnumerable GetActions( [NotNull] MethodInfo methodInfo, [NotNull] TypeInfo controllerTypeInfo) { if (!IsValidActionMethod(methodInfo)) { return null; } var actionInfos = GetActionsForMethodsWithCustomAttributes(methodInfo); if (actionInfos.Any()) { return actionInfos; } else { actionInfos = GetActionsForMethodsWithoutCustomAttributes(methodInfo, controllerTypeInfo); } return actionInfos; } protected virtual bool IsDefaultActionMethod([NotNull] MethodInfo methodInfo) { return String.Equals(methodInfo.Name, DefaultMethodName, StringComparison.OrdinalIgnoreCase); } protected virtual bool IsValidActionMethod(MethodInfo method) { return method.IsPublic && !method.IsAbstract && !method.IsConstructor && !method.IsGenericMethod && // 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). !method.IsSpecialName; } public virtual IEnumerable GetSupportedHttpMethods(MethodInfo methodInfo) { var supportedHttpMethods = _supportedHttpMethodsByConvention.FirstOrDefault( httpMethod => methodInfo.Name.Equals(httpMethod, StringComparison.OrdinalIgnoreCase)); if (supportedHttpMethods != null) { yield return supportedHttpMethods; } } private bool HasCustomAttributes(MethodInfo methodInfo) { var actionAttributes = GetActionCustomAttributes(methodInfo); return actionAttributes.Any(); } private ActionAttributes GetActionCustomAttributes(MethodInfo methodInfo) { var attributes = methodInfo.GetCustomAttributes(); var actionNameAttribute = attributes.OfType().FirstOrDefault(); var httpMethodConstraints = attributes.OfType(); return new ActionAttributes() { HttpMethodProviderAttributes = httpMethodConstraints, ActionNameAttribute = actionNameAttribute }; } private IEnumerable GetActionsForMethodsWithCustomAttributes(MethodInfo methodInfo) { var actionAttributes = GetActionCustomAttributes(methodInfo); if (!actionAttributes.Any()) { // If the action is not decorated with any of the attributes, // it would be handled by convention. yield break; } var actionNameAttribute = actionAttributes.ActionNameAttribute; var actionName = actionNameAttribute != null ? actionNameAttribute.Name : methodInfo.Name; var httpMethodProviders = actionAttributes.HttpMethodProviderAttributes; var httpMethods = httpMethodProviders.SelectMany(x => x.HttpMethods).Distinct().ToArray(); yield return new ActionInfo() { HttpMethods = httpMethods, ActionName = actionName, RequireActionNameMatch = true }; } private IEnumerable GetActionsForMethodsWithoutCustomAttributes(MethodInfo methodInfo, TypeInfo controllerTypeInfo) { var actionInfos = new List(); var httpMethods = GetSupportedHttpMethods(methodInfo); if (httpMethods != null && httpMethods.Any()) { return new[] { new ActionInfo() { HttpMethods = httpMethods.ToArray(), ActionName = methodInfo.Name, RequireActionNameMatch = false, } }; } // For Default Method add an action Info with GET, POST Http Method constraints. // Only constraints (out of GET and POST) for which there are no convention based actions available are added. // If there are existing action infos with http constraints for GET and POST, this action info is not added for default method. if (IsDefaultActionMethod(methodInfo)) { var existingHttpMethods = new HashSet(); foreach (var declaredMethodInfo in controllerTypeInfo.DeclaredMethods) { if (!IsValidActionMethod(declaredMethodInfo) || HasCustomAttributes(declaredMethodInfo)) { continue; } httpMethods = GetSupportedHttpMethods(declaredMethodInfo); if (httpMethods != null) { existingHttpMethods.UnionWith(httpMethods); } } var undefinedHttpMethods = _supportedHttpMethodsForDefaultMethod.Except( existingHttpMethods, StringComparer.Ordinal) .ToArray(); if (undefinedHttpMethods.Any()) { actionInfos.Add(new ActionInfo() { HttpMethods = undefinedHttpMethods, ActionName = methodInfo.Name, RequireActionNameMatch = false, }); } } actionInfos.Add( new ActionInfo() { ActionName = methodInfo.Name, RequireActionNameMatch = true, }); return actionInfos; } private class ActionAttributes { public IEnumerable HttpMethodProviderAttributes { get; set; } public ActionNameAttribute ActionNameAttribute { get; set; } public bool Any() { return ActionNameAttribute != null || HttpMethodProviderAttributes.Any(); } } } }