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