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.
This commit is contained in:
Ryan Nowak 2014-09-23 16:09:32 -07:00
parent fee3b3cc6c
commit 414c009b80
9 changed files with 219 additions and 396 deletions

View File

@ -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
{
/// <summary>
/// An exception which indicates multiple matches in action selection.
/// </summary>
#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
}
}

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Reflection; using System.Reflection;
using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.Routing;
@ -12,26 +11,6 @@ namespace Microsoft.AspNet.Mvc
{ {
public class DefaultActionDiscoveryConventions : IActionDiscoveryConventions 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) public virtual bool IsController([NotNull] TypeInfo typeInfo)
{ {
if (!typeInfo.IsClass || if (!typeInfo.IsClass ||
@ -73,17 +52,20 @@ namespace Microsoft.AspNet.Mvc
} }
else 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; return actionInfos;
} }
protected virtual bool IsDefaultActionMethod([NotNull] MethodInfo methodInfo)
{
return String.Equals(methodInfo.Name, DefaultMethodName, StringComparison.OrdinalIgnoreCase);
}
/// <summary> /// <summary>
/// Determines whether the method is a valid action. /// Determines whether the method is a valid action.
/// </summary> /// </summary>
@ -107,18 +89,6 @@ namespace Microsoft.AspNet.Mvc
method.GetBaseDefinition().DeclaringType != typeof(object); method.GetBaseDefinition().DeclaringType != typeof(object);
} }
public virtual IEnumerable<string> 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) private bool HasCustomAttributes(MethodInfo methodInfo)
{ {
var actionAttributes = GetActionCustomAttributes(methodInfo); var actionAttributes = GetActionCustomAttributes(methodInfo);
@ -256,70 +226,6 @@ namespace Microsoft.AspNet.Mvc
return null; return null;
} }
private IEnumerable<ActionInfo> GetActionsForMethodsWithoutCustomAttributes(
MethodInfo methodInfo,
TypeInfo controllerTypeInfo)
{
var actionInfos = new List<ActionInfo>();
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<string>();
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 private class ActionAttributes
{ {
public ActionNameAttribute ActionNameAttribute { get; set; } public ActionNameAttribute ActionNameAttribute { get; set; }

View File

@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Logging; using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.Framework.Logging; using Microsoft.Framework.Logging;
@ -18,22 +17,19 @@ namespace Microsoft.AspNet.Mvc
{ {
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider; private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
private readonly IActionSelectorDecisionTreeProvider _decisionTreeProvider; private readonly IActionSelectorDecisionTreeProvider _decisionTreeProvider;
private readonly IActionBindingContextProvider _bindingProvider;
private ILogger _logger; private ILogger _logger;
public DefaultActionSelector( public DefaultActionSelector(
[NotNull] IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider, [NotNull] IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider,
[NotNull] IActionSelectorDecisionTreeProvider decisionTreeProvider, [NotNull] IActionSelectorDecisionTreeProvider decisionTreeProvider,
[NotNull] IActionBindingContextProvider bindingProvider,
[NotNull] ILoggerFactory loggerFactory) [NotNull] ILoggerFactory loggerFactory)
{ {
_actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider; _actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider;
_decisionTreeProvider = decisionTreeProvider; _decisionTreeProvider = decisionTreeProvider;
_bindingProvider = bindingProvider;
_logger = loggerFactory.Create<DefaultActionSelector>(); _logger = loggerFactory.Create<DefaultActionSelector>();
} }
public async Task<ActionDescriptor> SelectAsync([NotNull] RouteContext context) public Task<ActionDescriptor> SelectAsync([NotNull] RouteContext context)
{ {
using (_logger.BeginScope("DefaultActionSelector.SelectAsync")) using (_logger.BeginScope("DefaultActionSelector.SelectAsync"))
{ {
@ -67,7 +63,9 @@ namespace Microsoft.AspNet.Mvc
matching = matchesWithConstraints; matching = matchesWithConstraints;
} }
if (matching.Count == 0) var finalMatches = SelectBestActions(matching);
if (finalMatches.Count == 0)
{ {
if (_logger.IsEnabled(TraceType.Information)) if (_logger.IsEnabled(TraceType.Information))
{ {
@ -75,17 +73,17 @@ namespace Microsoft.AspNet.Mvc
{ {
ActionsMatchingRouteConstraints = matchingRouteConstraints, ActionsMatchingRouteConstraints = matchingRouteConstraints,
ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints, ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints,
ActionsMatchingRouteAndMethodAndDynamicConstraints = ActionsMatchingRouteAndMethodAndDynamicConstraints =
matchingRouteAndMethodAndDynamicConstraints, matchingRouteAndMethodAndDynamicConstraints,
ActionsMatchingWithConstraints = matchesWithConstraints FinalMatches = finalMatches,
}); });
} }
return null; return Task.FromResult<ActionDescriptor>(null);
} }
else else if (finalMatches.Count == 1)
{ {
var selectedAction = await SelectBestCandidate(context, matching); var selectedAction = finalMatches[0];
if (_logger.IsEnabled(TraceType.Information)) if (_logger.IsEnabled(TraceType.Information))
{ {
@ -95,16 +93,50 @@ namespace Microsoft.AspNet.Mvc
ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints, ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints,
ActionsMatchingRouteAndMethodAndDynamicConstraints = ActionsMatchingRouteAndMethodAndDynamicConstraints =
matchingRouteAndMethodAndDynamicConstraints, matchingRouteAndMethodAndDynamicConstraints,
ActionsMatchingWithConstraints = matchesWithConstraints, FinalMatches = finalMatches,
SelectedAction = selectedAction 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);
} }
} }
} }
/// <summary>
/// Returns the set of best matching actions.
/// </summary>
/// <param name="actions">The set of actions that satisfy all constraints.</param>
/// <returns>A list of the best matching actions.</returns>
protected virtual IReadOnlyList<ActionDescriptor> SelectBestActions(IReadOnlyList<ActionDescriptor> actions)
{
return actions;
}
private bool MatchMethodConstraints(ActionDescriptor descriptor, RouteContext context) private bool MatchMethodConstraints(ActionDescriptor descriptor, RouteContext context)
{ {
return descriptor.MethodConstraints == null || return descriptor.MethodConstraints == null ||
@ -117,76 +149,6 @@ namespace Microsoft.AspNet.Mvc
descriptor.DynamicConstraints.All(c => c.Accept(context)); descriptor.DynamicConstraints.All(c => c.Accept(context));
} }
protected virtual async Task<ActionDescriptor> SelectBestCandidate(
RouteContext context,
List<ActionDescriptor> candidates)
{
var applicableCandiates = new List<ActionDescriptorCandidate>();
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 // 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 // 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. // 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; return descriptors.Items;
} }
private class ActionDescriptorCandidate
{
public ActionDescriptor Action { get; set; }
public int FoundParameters { get; set; }
public int FoundOptionalParameters { get; set; }
}
} }
} }

View File

@ -38,12 +38,12 @@ namespace Microsoft.AspNet.Mvc.Logging
public IReadOnlyList<ActionDescriptor> ActionsMatchingRouteAndMethodAndDynamicConstraints { get; set; } public IReadOnlyList<ActionDescriptor> ActionsMatchingRouteAndMethodAndDynamicConstraints { get; set; }
/// <summary> /// <summary>
/// The actions that matched with at least one constraint. /// The list of actions that are the best matches. These match all constraints.
/// </summary> /// </summary>
public IReadOnlyList<ActionDescriptor> ActionsMatchingWithConstraints { get; set; } public IReadOnlyList<ActionDescriptor> FinalMatches { get; set; }
/// <summary> /// <summary>
/// The selected action. /// The selected action. Will be null if no matches are found or more than one match is found.
/// </summary> /// </summary>
public ActionDescriptor SelectedAction { get; set; } public ActionDescriptor SelectedAction { get; set; }
@ -65,8 +65,8 @@ namespace Microsoft.AspNet.Mvc.Logging
builder.Append("\tActions matching route, method, and dynamic constraints: "); builder.Append("\tActions matching route, method, and dynamic constraints: ");
StringBuilderHelpers.Append(builder, ActionsMatchingRouteAndMethodAndDynamicConstraints, Formatter); StringBuilderHelpers.Append(builder, ActionsMatchingRouteAndMethodAndDynamicConstraints, Formatter);
builder.AppendLine(); builder.AppendLine();
builder.Append("\tActions matching with at least one constraint: "); builder.Append("\tBest Matches: ");
StringBuilderHelpers.Append(builder, ActionsMatchingWithConstraints, Formatter); StringBuilderHelpers.Append(builder, FinalMatches, Formatter);
builder.AppendLine(); builder.AppendLine();
builder.Append("\tSelected action: "); builder.Append("\tSelected action: ");
builder.Append(Formatter(SelectedAction)); builder.Append(Formatter(SelectedAction));

View File

@ -1482,6 +1482,22 @@ namespace Microsoft.AspNet.Mvc.Core
return GetString("AttributeRoute_NullTemplateRepresentation"); return GetString("AttributeRoute_NullTemplateRepresentation");
} }
/// <summary>
/// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}
/// </summary>
internal static string DefaultActionSelector_AmbiguousActions
{
get { return GetString("DefaultActionSelector_AmbiguousActions"); }
}
/// <summary>
/// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}
/// </summary>
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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -402,4 +402,8 @@
<data name="AttributeRoute_NullTemplateRepresentation" xml:space="preserve"> <data name="AttributeRoute_NullTemplateRepresentation" xml:space="preserve">
<value>(none)</value> <value>(none)</value>
</data> </data>
<data name="DefaultActionSelector_AmbiguousActions" xml:space="preserve">
<value>Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}</value>
<comment>0 is the newline - 1 is a newline separate list of action display names</comment>
</data>
</root> </root>

View File

@ -87,26 +87,6 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(null, result); 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<string, object>
{
{ "controller", "HttpMethodAttributeTests_DefaultMethodValidation" },
};
// Act
var result = await InvokeActionSelector(routeContext);
// Assert
Assert.Equal("Index", result.Name);
}
[Theory] [Theory]
[InlineData("Put")] [InlineData("Put")]
[InlineData("RPCMethod")] [InlineData("RPCMethod")]
@ -198,12 +178,9 @@ namespace Microsoft.AspNet.Mvc
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer); var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider); var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
var bindingProvider = new Mock<IActionBindingContextProvider>();
var defaultActionSelector = new DefaultActionSelector( var defaultActionSelector = new DefaultActionSelector(
actionCollectionDescriptorProvider, actionCollectionDescriptorProvider,
decisionTreeProvider, decisionTreeProvider,
bindingProvider.Object,
NullLoggerFactory.Instance); NullLoggerFactory.Instance);
return await defaultActionSelector.SelectAsync(context); return await defaultActionSelector.SelectAsync(context);
@ -243,14 +220,15 @@ namespace Microsoft.AspNet.Mvc
private class CustomActionConvention : DefaultActionDiscoveryConventions private class CustomActionConvention : DefaultActionDiscoveryConventions
{ {
public override IEnumerable<string> GetSupportedHttpMethods(MethodInfo methodInfo) public override IEnumerable<ActionInfo> GetActions([NotNull]MethodInfo methodInfo, [NotNull]TypeInfo controllerTypeInfo)
{ {
if (methodInfo.Name.Equals("PostSomething", StringComparison.OrdinalIgnoreCase)) var actions = new List<ActionInfo>(base.GetActions(methodInfo, controllerTypeInfo));
if (methodInfo.Name == "PostSomething")
{ {
return new[] { "POST" }; actions[0].HttpMethods = new string[] { "POST" };
} }
return null; return actions;
} }
} }

View File

@ -21,16 +21,15 @@ namespace Microsoft.AspNet.Mvc
{ {
public class DefaultActionDiscoveryConventionsActionSelectionTests public class DefaultActionDiscoveryConventionsActionSelectionTests
{ {
[Theory] [Fact]
[InlineData("GET")] public async Task ActionSelection_ActionSelectedByName()
[InlineData("POST")]
public async Task ActionSelection_IndexSelectedByDefaultInAbsenceOfVerbOnlyMethod(string verb)
{ {
// Arrange // Arrange
var routeContext = new RouteContext(GetHttpContext(verb)); var routeContext = new RouteContext(GetHttpContext("GET"));
routeContext.RouteData.Values = new Dictionary<string, object> routeContext.RouteData.Values = new Dictionary<string, object>
{ {
{ "controller", "RpcOnly" } { "controller", "RpcOnly" },
{ "action", "Index" }
}; };
// Act // Act
@ -40,101 +39,7 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal("Index", result.Name); Assert.Equal("Index", result.Name);
} }
[Theory] // Uses custom conventions to map a web-api-style action
[InlineData("GET")]
[InlineData("POST")]
public async Task ActionSelection_PrefersVerbOnlyMethodOverIndex(string verb)
{
// Arrange
var routeContext = new RouteContext(GetHttpContext(verb));
routeContext.RouteData.Values = new Dictionary<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "controller", "RpcOnly" }
};
// Act
var result = await InvokeActionSelector(routeContext, new CustomActionConvention());
// Assert
Assert.Equal("INDEX", result.Name, StringComparer.OrdinalIgnoreCase);
}
[Fact] [Fact]
public async Task ActionSelection_ChangeDefaultConventionPicksCustomMethodForPost_CutomMethodIsSelected() public async Task ActionSelection_ChangeDefaultConventionPicksCustomMethodForPost_CutomMethodIsSelected()
{ {
@ -177,12 +82,9 @@ namespace Microsoft.AspNet.Mvc
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer); var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider); var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
var bindingProvider = new Mock<IActionBindingContextProvider>();
var defaultActionSelector = new DefaultActionSelector( var defaultActionSelector = new DefaultActionSelector(
actionCollectionDescriptorProvider, actionCollectionDescriptorProvider,
decisionTreeProvider, decisionTreeProvider,
bindingProvider.Object,
NullLoggerFactory.Instance); NullLoggerFactory.Instance);
return await defaultActionSelector.SelectAsync(context); return await defaultActionSelector.SelectAsync(context);
@ -222,64 +124,19 @@ namespace Microsoft.AspNet.Mvc
.Contains(typeInfo); .Contains(typeInfo);
} }
public override IEnumerable<string> GetSupportedHttpMethods(MethodInfo methodInfo) public override IEnumerable<ActionInfo> GetActions([NotNull]MethodInfo methodInfo, [NotNull]TypeInfo controllerTypeInfo)
{ {
if (methodInfo.Name.Equals("PostSomething", StringComparison.OrdinalIgnoreCase)) var actions = new List<ActionInfo>(
base.GetActions(methodInfo, controllerTypeInfo) ??
new List<ActionInfo>());
if (methodInfo.Name == "PostSomething")
{ {
return new[] { "POST" }; actions[0].HttpMethods = new string[] { "POST" };
actions[0].RequireActionNameMatch = false;
} }
return null; return actions;
}
}
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()
{
} }
} }

View File

@ -54,7 +54,7 @@ namespace Microsoft.AspNet.Mvc
Assert.Empty(values.ActionsMatchingRouteConstraints); Assert.Empty(values.ActionsMatchingRouteConstraints);
Assert.Empty(values.ActionsMatchingRouteAndMethodConstraints); Assert.Empty(values.ActionsMatchingRouteAndMethodConstraints);
Assert.Empty(values.ActionsMatchingRouteAndMethodAndDynamicConstraints); Assert.Empty(values.ActionsMatchingRouteAndMethodAndDynamicConstraints);
Assert.Empty(values.ActionsMatchingWithConstraints); Assert.Empty(values.FinalMatches);
Assert.Null(values.SelectedAction); Assert.Null(values.SelectedAction);
} }
@ -106,12 +106,60 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal("DefaultActionSelector.SelectAsync", write.Scope); Assert.Equal("DefaultActionSelector.SelectAsync", write.Scope);
var values = Assert.IsType<DefaultActionSelectorSelectAsyncValues>(write.State); var values = Assert.IsType<DefaultActionSelectorSelectAsyncValues>(write.State);
Assert.Equal("DefaultActionSelector.SelectAsync", values.Name); Assert.Equal("DefaultActionSelector.SelectAsync", values.Name);
Assert.NotEmpty(values.ActionsMatchingRouteConstraints); Assert.Equal<ActionDescriptor>(actions, values.ActionsMatchingRouteConstraints);
Assert.NotEmpty(values.ActionsMatchingRouteAndMethodConstraints); Assert.Equal<ActionDescriptor>(actions, values.ActionsMatchingRouteAndMethodConstraints);
Assert.NotEmpty(values.ActionsMatchingWithConstraints); Assert.Equal(matched, Assert.Single(values.FinalMatches));
Assert.Equal(matched, values.SelectedAction); 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<AmbiguousActionException>(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<DefaultActionSelectorSelectAsyncValues>(write.State);
Assert.Equal("DefaultActionSelector.SelectAsync", values.Name);
Assert.Equal<ActionDescriptor>(actions, values.ActionsMatchingRouteConstraints);
Assert.Equal<ActionDescriptor>(actions, values.ActionsMatchingRouteAndMethodConstraints);
Assert.Equal<ActionDescriptor>(actions, values.FinalMatches);
Assert.Null(values.SelectedAction);
}
[Fact] [Fact]
public void HasValidAction_Match() public void HasValidAction_Match()
{ {
@ -262,6 +310,43 @@ namespace Microsoft.AspNet.Mvc
Assert.Null(action); 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<AmbiguousActionException>(async () =>
{
await selector.SelectAsync(context);
});
// Assert
Assert.Equal(expectedMessage, ex.Message);
}
private static ActionDescriptor[] GetActions() private static ActionDescriptor[] GetActions()
{ {
return new ActionDescriptor[] return new ActionDescriptor[]
@ -304,12 +389,7 @@ namespace Microsoft.AspNet.Mvc
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionProvider.Object); var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionProvider.Object);
var bindingProvider = new Mock<IActionBindingContextProvider>(MockBehavior.Strict); return new DefaultActionSelector(actionProvider.Object, decisionTreeProvider, loggerFactory);
bindingProvider
.Setup(bp => bp.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
.Returns(Task.FromResult<ActionBindingContext>(null));
return new DefaultActionSelector(actionProvider.Object, decisionTreeProvider, bindingProvider.Object, loggerFactory);
} }
private static VirtualPathContext CreateContext(object routeValues) private static VirtualPathContext CreateContext(object routeValues)