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:
parent
fee3b3cc6c
commit
414c009b80
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the method is a valid action.
|
||||
/// </summary>
|
||||
|
|
@ -107,18 +89,6 @@ namespace Microsoft.AspNet.Mvc
|
|||
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)
|
||||
{
|
||||
var actionAttributes = GetActionCustomAttributes(methodInfo);
|
||||
|
|
@ -256,70 +226,6 @@ namespace Microsoft.AspNet.Mvc
|
|||
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
|
||||
{
|
||||
public ActionNameAttribute ActionNameAttribute { get; set; }
|
||||
|
|
|
|||
|
|
@ -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<DefaultActionSelector>();
|
||||
}
|
||||
|
||||
public async Task<ActionDescriptor> SelectAsync([NotNull] RouteContext context)
|
||||
public Task<ActionDescriptor> 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<ActionDescriptor>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
return descriptor.MethodConstraints == null ||
|
||||
|
|
@ -117,76 +149,6 @@ namespace Microsoft.AspNet.Mvc
|
|||
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
|
||||
// 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,12 +38,12 @@ namespace Microsoft.AspNet.Mvc.Logging
|
|||
public IReadOnlyList<ActionDescriptor> ActionsMatchingRouteAndMethodAndDynamicConstraints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The actions that matched with at least one constraint.
|
||||
/// The list of actions that are the best matches. These match all constraints.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ActionDescriptor> ActionsMatchingWithConstraints { get; set; }
|
||||
public IReadOnlyList<ActionDescriptor> FinalMatches { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected action.
|
||||
/// The selected action. Will be null if no matches are found or more than one match is found.
|
||||
/// </summary>
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -1482,6 +1482,22 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
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)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -402,4 +402,8 @@
|
|||
<data name="AttributeRoute_NullTemplateRepresentation" xml:space="preserve">
|
||||
<value>(none)</value>
|
||||
</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>
|
||||
|
|
@ -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<string, object>
|
||||
{
|
||||
{ "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<IActionBindingContextProvider>();
|
||||
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, object>
|
||||
{
|
||||
{ "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<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);
|
||||
}
|
||||
|
||||
// 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<IActionBindingContextProvider>();
|
||||
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<DefaultActionSelectorSelectAsyncValues>(write.State);
|
||||
Assert.Equal("DefaultActionSelector.SelectAsync", values.Name);
|
||||
Assert.NotEmpty(values.ActionsMatchingRouteConstraints);
|
||||
Assert.NotEmpty(values.ActionsMatchingRouteAndMethodConstraints);
|
||||
Assert.NotEmpty(values.ActionsMatchingWithConstraints);
|
||||
Assert.Equal<ActionDescriptor>(actions, values.ActionsMatchingRouteConstraints);
|
||||
Assert.Equal<ActionDescriptor>(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<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]
|
||||
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<AmbiguousActionException>(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<IActionBindingContextProvider>(MockBehavior.Strict);
|
||||
bindingProvider
|
||||
.Setup(bp => bp.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
|
||||
.Returns(Task.FromResult<ActionBindingContext>(null));
|
||||
|
||||
return new DefaultActionSelector(actionProvider.Object, decisionTreeProvider, bindingProvider.Object, loggerFactory);
|
||||
return new DefaultActionSelector(actionProvider.Object, decisionTreeProvider, loggerFactory);
|
||||
}
|
||||
|
||||
private static VirtualPathContext CreateContext(object routeValues)
|
||||
|
|
|
|||
Loading…
Reference in New Issue