From 414c009b8063c889f9a5c88b2b14c502c935e7b4 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 23 Sep 2014 16:09:32 -0700 Subject: [PATCH] Removing Overloading and Automatic verb-mapping This change removes WebAPI-style method parameter overloading and the automatic mapping of 'unnamed' actions based on method names. For all practicaly purposes, this change restores the MVC5 behavior for action selection. WebAPI-style overloading will be brought back in the future via a set of opt-in constructs. --- .../AmbiguousActionException.cs | 29 +++ .../DefaultActionDiscoveryConventions.cs | 112 +----------- .../DefaultActionSelector.cs | 139 +++++--------- .../DefaultActionSelectorSelectAsyncValues.cs | 10 +- .../Properties/Resources.Designer.cs | 16 ++ src/Microsoft.AspNet.Mvc.Core/Resources.resx | 4 + .../ActionAttributeTests.cs | 32 +--- ...iscoveryConventionsActionSelectionTests.cs | 173 ++---------------- .../DefaultActionSelectorTests.cs | 100 +++++++++- 9 files changed, 219 insertions(+), 396 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/AmbiguousActionException.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/AmbiguousActionException.cs b/src/Microsoft.AspNet.Mvc.Core/AmbiguousActionException.cs new file mode 100644 index 0000000000..c9e7f7f0fc --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AmbiguousActionException.cs @@ -0,0 +1,29 @@ +// 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.Runtime.Serialization; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// An exception which indicates multiple matches in action selection. + /// + #if ASPNET50 + [Serializable] + #endif + public class AmbiguousActionException : InvalidOperationException + { + public AmbiguousActionException(string message) + : base(message) + { + } + + #if ASPNET50 + protected AmbiguousActionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + #endif + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs index 0713d78cf8..eaa181046a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Reflection; using Microsoft.AspNet.Mvc.Routing; @@ -12,26 +11,6 @@ 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 || @@ -73,17 +52,20 @@ namespace Microsoft.AspNet.Mvc } else { - actionInfos = GetActionsForMethodsWithoutCustomAttributes(methodInfo, controllerTypeInfo); + // By default the action is just matched by name. + actionInfos = new ActionInfo[] + { + new ActionInfo() + { + ActionName = methodInfo.Name, + RequireActionNameMatch = true, + } + }; } return actionInfos; } - protected virtual bool IsDefaultActionMethod([NotNull] MethodInfo methodInfo) - { - return String.Equals(methodInfo.Name, DefaultMethodName, StringComparison.OrdinalIgnoreCase); - } - /// /// Determines whether the method is a valid action. /// @@ -107,18 +89,6 @@ namespace Microsoft.AspNet.Mvc method.GetBaseDefinition().DeclaringType != typeof(object); } - 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); @@ -256,70 +226,6 @@ namespace Microsoft.AspNet.Mvc return null; } - 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 ActionNameAttribute ActionNameAttribute { get; set; } diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs index f762dcacdf..b018b24e20 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Logging; -using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Routing; using Microsoft.Framework.Logging; @@ -18,22 +17,19 @@ namespace Microsoft.AspNet.Mvc { private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider; private readonly IActionSelectorDecisionTreeProvider _decisionTreeProvider; - private readonly IActionBindingContextProvider _bindingProvider; private ILogger _logger; public DefaultActionSelector( [NotNull] IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider, - [NotNull] IActionSelectorDecisionTreeProvider decisionTreeProvider, - [NotNull] IActionBindingContextProvider bindingProvider, + [NotNull] IActionSelectorDecisionTreeProvider decisionTreeProvider, [NotNull] ILoggerFactory loggerFactory) { _actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider; _decisionTreeProvider = decisionTreeProvider; - _bindingProvider = bindingProvider; _logger = loggerFactory.Create(); } - public async Task SelectAsync([NotNull] RouteContext context) + public Task SelectAsync([NotNull] RouteContext context) { using (_logger.BeginScope("DefaultActionSelector.SelectAsync")) { @@ -67,7 +63,9 @@ namespace Microsoft.AspNet.Mvc matching = matchesWithConstraints; } - if (matching.Count == 0) + var finalMatches = SelectBestActions(matching); + + if (finalMatches.Count == 0) { if (_logger.IsEnabled(TraceType.Information)) { @@ -75,17 +73,17 @@ namespace Microsoft.AspNet.Mvc { ActionsMatchingRouteConstraints = matchingRouteConstraints, ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints, - ActionsMatchingRouteAndMethodAndDynamicConstraints = + ActionsMatchingRouteAndMethodAndDynamicConstraints = matchingRouteAndMethodAndDynamicConstraints, - ActionsMatchingWithConstraints = matchesWithConstraints + FinalMatches = finalMatches, }); } - return null; + return Task.FromResult(null); } - else + else if (finalMatches.Count == 1) { - var selectedAction = await SelectBestCandidate(context, matching); + var selectedAction = finalMatches[0]; if (_logger.IsEnabled(TraceType.Information)) { @@ -95,16 +93,50 @@ namespace Microsoft.AspNet.Mvc ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints, ActionsMatchingRouteAndMethodAndDynamicConstraints = matchingRouteAndMethodAndDynamicConstraints, - ActionsMatchingWithConstraints = matchesWithConstraints, + FinalMatches = finalMatches, SelectedAction = selectedAction }); } - return selectedAction; + return Task.FromResult(selectedAction); + } + else + { + if (_logger.IsEnabled(TraceType.Information)) + { + _logger.WriteValues(new DefaultActionSelectorSelectAsyncValues() + { + ActionsMatchingRouteConstraints = matchingRouteConstraints, + ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints, + ActionsMatchingRouteAndMethodAndDynamicConstraints = + matchingRouteAndMethodAndDynamicConstraints, + FinalMatches = finalMatches, + }); + } + + var actionNames = string.Join( + Environment.NewLine, + finalMatches.Select(a => a.DisplayName)); + + var message = Resources.FormatDefaultActionSelector_AmbiguousActions( + Environment.NewLine, + actionNames); + + throw new AmbiguousActionException(message); } } } + /// + /// Returns the set of best matching actions. + /// + /// The set of actions that satisfy all constraints. + /// A list of the best matching actions. + protected virtual IReadOnlyList SelectBestActions(IReadOnlyList actions) + { + return actions; + } + private bool MatchMethodConstraints(ActionDescriptor descriptor, RouteContext context) { return descriptor.MethodConstraints == null || @@ -117,76 +149,6 @@ namespace Microsoft.AspNet.Mvc descriptor.DynamicConstraints.All(c => c.Accept(context)); } - protected virtual async Task SelectBestCandidate( - RouteContext context, - List candidates) - { - var applicableCandiates = new List(); - foreach (var action in candidates) - { - var isApplicable = true; - var candidate = new ActionDescriptorCandidate() - { - Action = action, - }; - - var actionContext = new ActionContext(context, action); - var actionBindingContext = await _bindingProvider.GetActionBindingContextAsync(actionContext); - - foreach (var parameter in action.Parameters.Where(p => p.ParameterBindingInfo != null)) - { - if (!ValueProviderResult.CanConvertFromString(parameter.ParameterBindingInfo.ParameterType)) - { - continue; - } - - if (await actionBindingContext.ValueProvider.ContainsPrefixAsync( - parameter.ParameterBindingInfo.Prefix)) - { - candidate.FoundParameters++; - if (parameter.IsOptional) - { - candidate.FoundOptionalParameters++; - } - } - else if (!parameter.IsOptional) - { - isApplicable = false; - break; - } - } - - if (isApplicable) - { - applicableCandiates.Add(candidate); - } - } - - if (applicableCandiates.Count == 0) - { - return null; - } - - var mostParametersSatisfied = - applicableCandiates - .GroupBy(c => c.FoundParameters) - .OrderByDescending(g => g.Key) - .First(); - - var fewestOptionalParameters = - mostParametersSatisfied - .GroupBy(c => c.FoundOptionalParameters) - .OrderBy(g => g.Key).First() - .ToArray(); - - if (fewestOptionalParameters.Length > 1) - { - throw new InvalidOperationException("The actions are ambiguious."); - } - - return fewestOptionalParameters[0].Action; - } - // This method attempts to ensure that the route that's about to generate a link will generate a link // to an existing action. This method is called by a route (through MvcApplication) prior to generating // any link - this gives WebFX a chance to 'veto' the values provided by a route. @@ -222,14 +184,5 @@ namespace Microsoft.AspNet.Mvc return descriptors.Items; } - - private class ActionDescriptorCandidate - { - public ActionDescriptor Action { get; set; } - - public int FoundParameters { get; set; } - - public int FoundOptionalParameters { get; set; } - } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Logging/DefaultActionSelectorSelectAsyncValues.cs b/src/Microsoft.AspNet.Mvc.Core/Logging/DefaultActionSelectorSelectAsyncValues.cs index 4699f7c459..a41fca2cb6 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Logging/DefaultActionSelectorSelectAsyncValues.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Logging/DefaultActionSelectorSelectAsyncValues.cs @@ -38,12 +38,12 @@ namespace Microsoft.AspNet.Mvc.Logging public IReadOnlyList ActionsMatchingRouteAndMethodAndDynamicConstraints { get; set; } /// - /// The actions that matched with at least one constraint. + /// The list of actions that are the best matches. These match all constraints. /// - public IReadOnlyList ActionsMatchingWithConstraints { get; set; } + public IReadOnlyList FinalMatches { get; set; } /// - /// The selected action. + /// The selected action. Will be null if no matches are found or more than one match is found. /// public ActionDescriptor SelectedAction { get; set; } @@ -65,8 +65,8 @@ namespace Microsoft.AspNet.Mvc.Logging builder.Append("\tActions matching route, method, and dynamic constraints: "); StringBuilderHelpers.Append(builder, ActionsMatchingRouteAndMethodAndDynamicConstraints, Formatter); builder.AppendLine(); - builder.Append("\tActions matching with at least one constraint: "); - StringBuilderHelpers.Append(builder, ActionsMatchingWithConstraints, Formatter); + builder.Append("\tBest Matches: "); + StringBuilderHelpers.Append(builder, FinalMatches, Formatter); builder.AppendLine(); builder.Append("\tSelected action: "); builder.Append(Formatter(SelectedAction)); diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index be86cd7f05..72f9f30018 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1482,6 +1482,22 @@ namespace Microsoft.AspNet.Mvc.Core return GetString("AttributeRoute_NullTemplateRepresentation"); } + /// + /// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1} + /// + internal static string DefaultActionSelector_AmbiguousActions + { + get { return GetString("DefaultActionSelector_AmbiguousActions"); } + } + + /// + /// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1} + /// + internal static string FormatDefaultActionSelector_AmbiguousActions(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DefaultActionSelector_AmbiguousActions"), p0, p1); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index dc3b141a1a..4633517ed2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -402,4 +402,8 @@ (none) + + Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1} + 0 is the newline - 1 is a newline separate list of action display names + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs index 607846e02a..34daccd566 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs @@ -87,26 +87,6 @@ namespace Microsoft.AspNet.Mvc Assert.Equal(null, result); } - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task HttpMethodAttribute_DefaultMethod_IgnoresMethodsWithCustomAttributesAndInvalidMethods(string verb) - { - // Arrange - // Note no action name is passed, hence should return a null action descriptor. - var routeContext = new RouteContext(GetHttpContext(verb)); - routeContext.RouteData.Values = new Dictionary - { - { "controller", "HttpMethodAttributeTests_DefaultMethodValidation" }, - }; - - // Act - var result = await InvokeActionSelector(routeContext); - - // Assert - Assert.Equal("Index", result.Name); - } - [Theory] [InlineData("Put")] [InlineData("RPCMethod")] @@ -198,12 +178,9 @@ namespace Microsoft.AspNet.Mvc var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer); var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider); - var bindingProvider = new Mock(); - var defaultActionSelector = new DefaultActionSelector( actionCollectionDescriptorProvider, decisionTreeProvider, - bindingProvider.Object, NullLoggerFactory.Instance); return await defaultActionSelector.SelectAsync(context); @@ -243,14 +220,15 @@ namespace Microsoft.AspNet.Mvc private class CustomActionConvention : DefaultActionDiscoveryConventions { - public override IEnumerable GetSupportedHttpMethods(MethodInfo methodInfo) + public override IEnumerable GetActions([NotNull]MethodInfo methodInfo, [NotNull]TypeInfo controllerTypeInfo) { - if (methodInfo.Name.Equals("PostSomething", StringComparison.OrdinalIgnoreCase)) + var actions = new List(base.GetActions(methodInfo, controllerTypeInfo)); + if (methodInfo.Name == "PostSomething") { - return new[] { "POST" }; + actions[0].HttpMethods = new string[] { "POST" }; } - return null; + return actions; } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs index 37088e7488..56ed6be735 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs @@ -21,16 +21,15 @@ namespace Microsoft.AspNet.Mvc { public class DefaultActionDiscoveryConventionsActionSelectionTests { - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task ActionSelection_IndexSelectedByDefaultInAbsenceOfVerbOnlyMethod(string verb) + [Fact] + public async Task ActionSelection_ActionSelectedByName() { // Arrange - var routeContext = new RouteContext(GetHttpContext(verb)); + var routeContext = new RouteContext(GetHttpContext("GET")); routeContext.RouteData.Values = new Dictionary { - { "controller", "RpcOnly" } + { "controller", "RpcOnly" }, + { "action", "Index" } }; // Act @@ -40,101 +39,7 @@ namespace Microsoft.AspNet.Mvc Assert.Equal("Index", result.Name); } - [Theory] - [InlineData("GET")] - [InlineData("POST")] - public async Task ActionSelection_PrefersVerbOnlyMethodOverIndex(string verb) - { - // Arrange - var routeContext = new RouteContext(GetHttpContext(verb)); - routeContext.RouteData.Values = new Dictionary - { - { "controller", "MixedRpcAndRest" } - }; - - // Act - var result = await InvokeActionSelector(routeContext); - - // Assert - Assert.Equal(verb, result.Name, StringComparer.OrdinalIgnoreCase); - } - - [Theory] - [InlineData("PUT")] - [InlineData("DELETE")] - [InlineData("PATCH")] - public async Task ActionSelection_IndexNotSelectedByDefaultExceptGetAndPostVerbs(string verb) - { - // Arrange - var routeContext = new RouteContext(GetHttpContext(verb)); - routeContext.RouteData.Values = new Dictionary - { - { "controller", "RpcOnly" } - }; - - // Act - var result = await InvokeActionSelector(routeContext); - - // Assert - Assert.Equal(null, result); - } - - [Theory] - [InlineData("HEAD")] - [InlineData("OPTIONS")] - public async Task ActionSelection_NoConventionBasedRoutingForHeadAndOptions(string verb) - { - // Arrange - var routeContext = new RouteContext(GetHttpContext(verb)); - routeContext.RouteData.Values = new Dictionary - { - { "controller", "MixedRpcAndRest" }, - }; - - // Act - var result = await InvokeActionSelector(routeContext); - - // Assert - Assert.Equal(null, result); - } - - [Theory] - [InlineData("HEAD")] - [InlineData("OPTIONS")] - public async Task ActionSelection_ActionNameBasedRoutingForHeadAndOptions(string verb) - { - // Arrange - var routeContext = new RouteContext(GetHttpContext(verb)); - routeContext.RouteData.Values = new Dictionary - { - { "controller", "MixedRpcAndRest" }, - { "action", verb }, - }; - - // Act - var result = await InvokeActionSelector(routeContext); - - // Assert - Assert.Equal(verb, result.Name, StringComparer.OrdinalIgnoreCase); - } - - [Fact] - public async Task ActionSelection_ChangeDefaultConventionPicksCustomMethodForPost_DefaultMethodIsSelectedForGet() - { - // Arrange - var routeContext = new RouteContext(GetHttpContext("GET")); - routeContext.RouteData.Values = new Dictionary - { - { "controller", "RpcOnly" } - }; - - // Act - var result = await InvokeActionSelector(routeContext, new CustomActionConvention()); - - // Assert - Assert.Equal("INDEX", result.Name, StringComparer.OrdinalIgnoreCase); - } - + // Uses custom conventions to map a web-api-style action [Fact] public async Task ActionSelection_ChangeDefaultConventionPicksCustomMethodForPost_CutomMethodIsSelected() { @@ -177,12 +82,9 @@ namespace Microsoft.AspNet.Mvc var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer); var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider); - var bindingProvider = new Mock(); - var defaultActionSelector = new DefaultActionSelector( actionCollectionDescriptorProvider, decisionTreeProvider, - bindingProvider.Object, NullLoggerFactory.Instance); return await defaultActionSelector.SelectAsync(context); @@ -222,64 +124,19 @@ namespace Microsoft.AspNet.Mvc .Contains(typeInfo); } - public override IEnumerable GetSupportedHttpMethods(MethodInfo methodInfo) + public override IEnumerable GetActions([NotNull]MethodInfo methodInfo, [NotNull]TypeInfo controllerTypeInfo) { - if (methodInfo.Name.Equals("PostSomething", StringComparison.OrdinalIgnoreCase)) + var actions = new List( + base.GetActions(methodInfo, controllerTypeInfo) ?? + new List()); + + if (methodInfo.Name == "PostSomething") { - return new[] { "POST" }; + actions[0].HttpMethods = new string[] { "POST" }; + actions[0].RequireActionNameMatch = false; } - return null; - } - } - - private class MixedRpcAndRestController - { - public void Index() - { - } - - public void Get() - { - } - - public void Post() - { } - - public void GetSomething() - { } - - // This will be treated as an RPC method. - public void Head() - { - } - - // This will be treated as an RPC method. - public void Options() - { - } - } - - private class RestOnlyController - { - public void Get() - { - } - - public void Put() - { - } - - public void Post() - { - } - - public void Delete() - { - } - - public void Patch() - { + return actions; } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs index d0fef35e57..2fa285e787 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs @@ -54,7 +54,7 @@ namespace Microsoft.AspNet.Mvc Assert.Empty(values.ActionsMatchingRouteConstraints); Assert.Empty(values.ActionsMatchingRouteAndMethodConstraints); Assert.Empty(values.ActionsMatchingRouteAndMethodAndDynamicConstraints); - Assert.Empty(values.ActionsMatchingWithConstraints); + Assert.Empty(values.FinalMatches); Assert.Null(values.SelectedAction); } @@ -106,12 +106,60 @@ namespace Microsoft.AspNet.Mvc Assert.Equal("DefaultActionSelector.SelectAsync", write.Scope); var values = Assert.IsType(write.State); Assert.Equal("DefaultActionSelector.SelectAsync", values.Name); - Assert.NotEmpty(values.ActionsMatchingRouteConstraints); - Assert.NotEmpty(values.ActionsMatchingRouteAndMethodConstraints); - Assert.NotEmpty(values.ActionsMatchingWithConstraints); + Assert.Equal(actions, values.ActionsMatchingRouteConstraints); + Assert.Equal(actions, values.ActionsMatchingRouteAndMethodConstraints); + Assert.Equal(matched, Assert.Single(values.FinalMatches)); Assert.Equal(matched, values.SelectedAction); } + [Fact] + public async void SelectAsync_AmbiguousActions_LogIsCorrect() + { + // Arrange + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink); + + var actions = new ActionDescriptor[] + { + new ActionDescriptor() { DisplayName = "A1" }, + new ActionDescriptor() { DisplayName = "A2" }, + }; + + var selector = CreateSelector(actions, loggerFactory); + + var routeContext = CreateRouteContext("POST"); + + // Act + await Assert.ThrowsAsync(async () => + { + await selector.SelectAsync(routeContext); + }); + + // Assert + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(DefaultActionSelector).FullName, scope.LoggerName); + Assert.Equal("DefaultActionSelector.SelectAsync", scope.Scope); + + // There is a record for IsEnabled and one for WriteCore. + Assert.Equal(2, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(typeof(DefaultActionSelector).FullName, enabled.LoggerName); + Assert.Equal("DefaultActionSelector.SelectAsync", enabled.Scope); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(typeof(DefaultActionSelector).FullName, write.LoggerName); + Assert.Equal("DefaultActionSelector.SelectAsync", write.Scope); + var values = Assert.IsType(write.State); + Assert.Equal("DefaultActionSelector.SelectAsync", values.Name); + Assert.Equal(actions, values.ActionsMatchingRouteConstraints); + Assert.Equal(actions, values.ActionsMatchingRouteAndMethodConstraints); + Assert.Equal(actions, values.FinalMatches); + Assert.Null(values.SelectedAction); + } + [Fact] public void HasValidAction_Match() { @@ -262,6 +310,43 @@ namespace Microsoft.AspNet.Mvc Assert.Null(action); } + [Fact] + public async Task SelectAsync_Ambiguous() + { + // Arrange + var expectedMessage = + "Multiple actions matched. " + + "The following actions matched route data and had all constraints satisfied:" + Environment.NewLine + + Environment.NewLine + + "Ambiguous1" + Environment.NewLine + + "Ambiguous2"; + + var actions = new ActionDescriptor[] + { + CreateAction(area: null, controller: "Store", action: "Buy"), + CreateAction(area: null, controller: "Store", action: "Buy"), + CreateAction(area: null, controller: "Store", action: "Cart"), + }; + + actions[0].DisplayName = "Ambiguous1"; + actions[1].DisplayName = "Ambiguous2"; + + var selector = CreateSelector(actions); + var context = CreateRouteContext("GET"); + + context.RouteData.Values.Add("controller", "Store"); + context.RouteData.Values.Add("action", "Buy"); + + // Act + var ex = await Assert.ThrowsAsync(async () => + { + await selector.SelectAsync(context); + }); + + // Assert + Assert.Equal(expectedMessage, ex.Message); + } + private static ActionDescriptor[] GetActions() { return new ActionDescriptor[] @@ -304,12 +389,7 @@ namespace Microsoft.AspNet.Mvc var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionProvider.Object); - var bindingProvider = new Mock(MockBehavior.Strict); - bindingProvider - .Setup(bp => bp.GetActionBindingContextAsync(It.IsAny())) - .Returns(Task.FromResult(null)); - - return new DefaultActionSelector(actionProvider.Object, decisionTreeProvider, bindingProvider.Object, loggerFactory); + return new DefaultActionSelector(actionProvider.Object, decisionTreeProvider, loggerFactory); } private static VirtualPathContext CreateContext(object routeValues)