1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
|
namespace System.Web.Mvc.Async {
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web.Mvc.Resources;
internal sealed class AsyncActionMethodSelector {
public AsyncActionMethodSelector(Type controllerType) {
ControllerType = controllerType;
PopulateLookupTables();
}
public Type ControllerType {
get;
private set;
}
public MethodInfo[] AliasedMethods {
get;
private set;
}
public ILookup<string, MethodInfo> NonAliasedMethods {
get;
private set;
}
private AmbiguousMatchException CreateAmbiguousActionMatchException(IEnumerable<MethodInfo> ambiguousMethods, string actionName) {
string ambiguityList = CreateAmbiguousMatchList(ambiguousMethods);
string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatch,
actionName, ControllerType.Name, ambiguityList);
return new AmbiguousMatchException(message);
}
private AmbiguousMatchException CreateAmbiguousMethodMatchException(IEnumerable<MethodInfo> ambiguousMethods, string methodName) {
string ambiguityList = CreateAmbiguousMatchList(ambiguousMethods);
string message = String.Format(CultureInfo.CurrentCulture, MvcResources.AsyncActionMethodSelector_AmbiguousMethodMatch,
methodName, ControllerType.Name, ambiguityList);
return new AmbiguousMatchException(message);
}
private static string CreateAmbiguousMatchList(IEnumerable<MethodInfo> ambiguousMethods) {
StringBuilder exceptionMessageBuilder = new StringBuilder();
foreach (MethodInfo methodInfo in ambiguousMethods) {
exceptionMessageBuilder.AppendLine();
exceptionMessageBuilder.AppendFormat(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatchType, methodInfo, methodInfo.DeclaringType.FullName);
}
return exceptionMessageBuilder.ToString();
}
public ActionDescriptorCreator FindAction(ControllerContext controllerContext, string actionName) {
List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods(controllerContext, actionName);
methodsMatchingName.AddRange(NonAliasedMethods[actionName]);
List<MethodInfo> finalMethods = RunSelectionFilters(controllerContext, methodsMatchingName);
switch (finalMethods.Count) {
case 0:
return null;
case 1:
MethodInfo entryMethod = finalMethods[0];
return GetActionDescriptorDelegate(entryMethod);
default:
throw CreateAmbiguousActionMatchException(finalMethods, actionName);
}
}
private ActionDescriptorCreator GetActionDescriptorDelegate(MethodInfo entryMethod) {
// Is this the FooAsync() / FooCompleted() pattern?
if (IsAsyncSuffixedMethod(entryMethod)) {
string completionMethodName = entryMethod.Name.Substring(0, entryMethod.Name.Length - "Async".Length) + "Completed";
MethodInfo completionMethod = GetMethodByName(completionMethodName);
if (completionMethod != null) {
return (actionName, controllerDescriptor) => new ReflectedAsyncActionDescriptor(entryMethod, completionMethod, actionName, controllerDescriptor);
}
else {
throw Error.AsyncActionMethodSelector_CouldNotFindMethod(completionMethodName, ControllerType);
}
}
// Fallback to synchronous method
return (actionName, controllerDescriptor) => new ReflectedActionDescriptor(entryMethod, actionName, controllerDescriptor);
}
private static string GetCanonicalMethodName(MethodInfo methodInfo) {
string methodName = methodInfo.Name;
return (IsAsyncSuffixedMethod(methodInfo))
? methodName.Substring(0, methodName.Length - "Async".Length)
: methodName;
}
internal List<MethodInfo> GetMatchingAliasedMethods(ControllerContext controllerContext, string actionName) {
// find all aliased methods which are opting in to this request
// to opt in, all attributes defined on the method must return true
var methods = from methodInfo in AliasedMethods
let attrs = ReflectedAttributeCache.GetActionNameSelectorAttributes(methodInfo)
where attrs.All(attr => attr.IsValidName(controllerContext, actionName, methodInfo))
select methodInfo;
return methods.ToList();
}
private static bool IsAsyncSuffixedMethod(MethodInfo methodInfo) {
return methodInfo.Name.EndsWith("Async", StringComparison.OrdinalIgnoreCase);
}
private static bool IsCompletedSuffixedMethod(MethodInfo methodInfo) {
return methodInfo.Name.EndsWith("Completed", StringComparison.OrdinalIgnoreCase);
}
private static bool IsMethodDecoratedWithAliasingAttribute(MethodInfo methodInfo) {
return methodInfo.IsDefined(typeof(ActionNameSelectorAttribute), true /* inherit */);
}
private MethodInfo GetMethodByName(string methodName) {
List<MethodInfo> methods = (from MethodInfo methodInfo in ControllerType.GetMember(methodName, MemberTypes.Method, BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.IgnoreCase)
where IsValidActionMethod(methodInfo, false /* stripInfrastructureMethods */)
select methodInfo).ToList();
switch (methods.Count) {
case 0:
return null;
case 1:
return methods[0];
default:
throw CreateAmbiguousMethodMatchException(methods, methodName);
}
}
private static bool IsValidActionMethod(MethodInfo methodInfo) {
return IsValidActionMethod(methodInfo, true /* stripInfrastructureMethods */);
}
private static bool IsValidActionMethod(MethodInfo methodInfo, bool stripInfrastructureMethods) {
if (methodInfo.IsSpecialName) {
// not a normal method, e.g. a constructor or an event
return false;
}
if (methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom(typeof(AsyncController))) {
// is a method on Object, ControllerBase, Controller, or AsyncController
return false;
};
if (stripInfrastructureMethods) {
if (IsCompletedSuffixedMethod(methodInfo)) {
// do not match FooCompleted() methods, as these are infrastructure methods
return false;
}
}
return true;
}
private void PopulateLookupTables() {
MethodInfo[] allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public);
MethodInfo[] actionMethods = Array.FindAll(allMethods, IsValidActionMethod);
AliasedMethods = Array.FindAll(actionMethods, IsMethodDecoratedWithAliasingAttribute);
NonAliasedMethods = actionMethods.Except(AliasedMethods).ToLookup(GetCanonicalMethodName, StringComparer.OrdinalIgnoreCase);
}
private static List<MethodInfo> RunSelectionFilters(ControllerContext controllerContext, List<MethodInfo> methodInfos) {
// remove all methods which are opting out of this request
// to opt out, at least one attribute defined on the method must return false
List<MethodInfo> matchesWithSelectionAttributes = new List<MethodInfo>();
List<MethodInfo> matchesWithoutSelectionAttributes = new List<MethodInfo>();
foreach (MethodInfo methodInfo in methodInfos) {
ICollection<ActionMethodSelectorAttribute> attrs = ReflectedAttributeCache.GetActionMethodSelectorAttributes(methodInfo);
if (attrs.Count == 0) {
matchesWithoutSelectionAttributes.Add(methodInfo);
}
else if (attrs.All(attr => attr.IsValidForRequest(controllerContext, methodInfo))) {
matchesWithSelectionAttributes.Add(methodInfo);
}
}
// if a matching action method had a selection attribute, consider it more specific than a matching action method
// without a selection attribute
return (matchesWithSelectionAttributes.Count > 0) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes;
}
}
}
|