[Fixes #739] Attribute Routing: Multiple routes per-action
1. Support multiple [Http*] attributes on an action.
2. Support multiple [Route] attributes on a controller and on an action.
3. Support creating multiple attribute routes using [AcceptVerbs("...", Route = "...")]
4. Detect attribute routed actions during action discovery and return one action per [Http*],
[Route] or [AcceptVerbs] attribute found on the method when there is at least one valid attribute route.
5. Merge all the HTTP methods of [Http*] and [AcceptVerbs] attributes in a method during
action discovery when there are no valid attribute routes defined on the action.
6. Build one action descriptor per controller [Route] + action [Http*], [AcceptVerbs]
or [Route] combination in an action.
7. Disallow the use of attributes that do not implement IActionHttpMethodProvider and
IRouteTemplateProvider simultaneously in methods that define attribute routed
actions and throw an exception during startup.
8. Disallow mixing attribute routed and non attribute routed actions on the same method
and throw an exception during startup.
This commit is contained in:
parent
7a3dc352c9
commit
775a780f49
|
|
@ -4,16 +4,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies what HTTP methods an action supports.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
|
||||
{
|
||||
private readonly IEnumerable<string> _httpMethods;
|
||||
private int? _order;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AcceptVerbsAttribute" /> class.
|
||||
|
|
@ -45,5 +47,40 @@ namespace Microsoft.AspNet.Mvc
|
|||
return _httpMethods;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The route template. May be null.
|
||||
/// </summary>
|
||||
public string Route { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
string IRouteTemplateProvider.Template
|
||||
{
|
||||
get { return Route; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route order. The order determines the order of route execution. Routes with a lower
|
||||
/// order value are tried first. When a route doesn't specify a value, it gets the value of the
|
||||
/// <see cref="RouteAttribute.Order"/> or a default value of 0 if the <see cref="RouteAttribute"/>
|
||||
/// doesn't define a value on the controller.
|
||||
/// </summary>
|
||||
public int Order
|
||||
{
|
||||
get { return _order ?? 0; }
|
||||
set { _order = value; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
int? IRouteTemplateProvider.Order
|
||||
{
|
||||
get
|
||||
{
|
||||
return _order;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
|
|
@ -9,6 +10,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
{
|
||||
public string ActionName { get; set; }
|
||||
public string[] HttpMethods { get; set; }
|
||||
public IRouteTemplateProvider AttributeRoute { get; set; }
|
||||
public bool RequireActionNameMatch { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
|
|
@ -55,7 +56,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
// If the convention is All methods starting with Get do not have an action name,
|
||||
// for a input GetXYZ methodInfo, the return value will be
|
||||
// { { HttpMethods = "GET", ActionName = "GetXYZ", RequireActionNameMatch = false }}
|
||||
// { { HttpMethods = "GET", ActionName = "GetXYZ", RequireActionNameMatch = false, AttributeRoute = null }}
|
||||
public virtual IEnumerable<ActionInfo> GetActions(
|
||||
[NotNull] MethodInfo methodInfo,
|
||||
[NotNull] TypeInfo controllerTypeInfo)
|
||||
|
|
@ -65,7 +66,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
return null;
|
||||
}
|
||||
|
||||
var actionInfos = GetActionsForMethodsWithCustomAttributes(methodInfo);
|
||||
var actionInfos = GetActionsForMethodsWithCustomAttributes(methodInfo, controllerTypeInfo);
|
||||
if (actionInfos.Any())
|
||||
{
|
||||
return actionInfos;
|
||||
|
|
@ -129,26 +130,77 @@ namespace Microsoft.AspNet.Mvc
|
|||
var attributes = methodInfo.GetCustomAttributes();
|
||||
var actionNameAttribute = attributes.OfType<ActionNameAttribute>().FirstOrDefault();
|
||||
var httpMethodConstraints = attributes.OfType<IActionHttpMethodProvider>();
|
||||
var routeTemplates = attributes.OfType<IRouteTemplateProvider>();
|
||||
|
||||
return new ActionAttributes()
|
||||
{
|
||||
ActionNameAttribute = actionNameAttribute,
|
||||
HttpMethodProviderAttributes = httpMethodConstraints,
|
||||
ActionNameAttribute = actionNameAttribute
|
||||
RouteTemplateProviderAttributes = routeTemplates,
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<ActionInfo> GetActionsForMethodsWithCustomAttributes(MethodInfo methodInfo)
|
||||
private IEnumerable<ActionInfo> GetActionsForMethodsWithCustomAttributes(
|
||||
MethodInfo methodInfo,
|
||||
TypeInfo controller)
|
||||
{
|
||||
var hasControllerAttributeRoutes = HasValidControllerRouteTemplates(controller);
|
||||
var actionAttributes = GetActionCustomAttributes(methodInfo);
|
||||
if (!actionAttributes.Any())
|
||||
|
||||
// We need to check controllerRouteTemplates to take into account the
|
||||
// case where the controller has [Route] on it and the action does not have any
|
||||
// attributes applied to it.
|
||||
if (actionAttributes.Any() || hasControllerAttributeRoutes)
|
||||
{
|
||||
var actionNameAttribute = actionAttributes.ActionNameAttribute;
|
||||
var actionName = actionNameAttribute != null ? actionNameAttribute.Name : methodInfo.Name;
|
||||
|
||||
// The moment we see a non null attribute route template in the method or
|
||||
// in the controller we consider the whole group to be attribute routed actions.
|
||||
// If a combination ends up producing a non attribute routed action we consider
|
||||
// that an error and throw at a later point in the pipeline.
|
||||
if (hasControllerAttributeRoutes || ActionHasAttributeRoutes(actionAttributes))
|
||||
{
|
||||
return GetAttributeRoutedActions(actionAttributes, actionName);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetHttpConstrainedActions(actionAttributes, actionName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the action is not decorated with any of the attributes,
|
||||
// it would be handled by convention.
|
||||
yield break;
|
||||
return Enumerable.Empty<ActionInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
var actionNameAttribute = actionAttributes.ActionNameAttribute;
|
||||
var actionName = actionNameAttribute != null ? actionNameAttribute.Name : methodInfo.Name;
|
||||
private static bool ActionHasAttributeRoutes(ActionAttributes actionAttributes)
|
||||
{
|
||||
// We neet to check for null as some attributes implement IActionHttpMethodProvider
|
||||
// and IRouteTemplateProvider and allow the user to provide a null template. An example
|
||||
// of this is HttpGetAttribute. If the user provides a template, the attribute marks the
|
||||
// action as attribute routed, but in other case, the attribute only adds a constraint
|
||||
// that allows the action to be called with the GET HTTP method.
|
||||
return actionAttributes.RouteTemplateProviderAttributes
|
||||
.Any(rtp => rtp.Template != null);
|
||||
}
|
||||
|
||||
private static bool HasValidControllerRouteTemplates(TypeInfo controller)
|
||||
{
|
||||
// A method inside a controller is considered to create attribute routed actions if the controller
|
||||
// has one or more attributes that implement IRouteTemplateProvider with a non null template applied
|
||||
// to it.
|
||||
return controller.GetCustomAttributes()
|
||||
.OfType<IRouteTemplateProvider>()
|
||||
.Any(cr => cr.Template != null);
|
||||
}
|
||||
|
||||
private static IEnumerable<ActionInfo> GetHttpConstrainedActions(
|
||||
ActionAttributes actionAttributes,
|
||||
string actionName)
|
||||
{
|
||||
var httpMethodProviders = actionAttributes.HttpMethodProviderAttributes;
|
||||
var httpMethods = httpMethodProviders.SelectMany(x => x.HttpMethods).Distinct().ToArray();
|
||||
|
||||
|
|
@ -156,10 +208,54 @@ namespace Microsoft.AspNet.Mvc
|
|||
{
|
||||
HttpMethods = httpMethods,
|
||||
ActionName = actionName,
|
||||
RequireActionNameMatch = true
|
||||
RequireActionNameMatch = true,
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<ActionInfo> GetAttributeRoutedActions(
|
||||
ActionAttributes actionAttributes,
|
||||
string actionName)
|
||||
{
|
||||
var actions = new List<ActionInfo>();
|
||||
|
||||
// This is the case where the controller has [Route] applied to it and
|
||||
// the action doesn't have any [Route] or [Http*] attribute applied.
|
||||
if (!actionAttributes.RouteTemplateProviderAttributes.Any())
|
||||
{
|
||||
actions.Add(new ActionInfo
|
||||
{
|
||||
ActionName = actionName,
|
||||
HttpMethods = null,
|
||||
RequireActionNameMatch = true,
|
||||
AttributeRoute = null
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var routeTemplateProvider in actionAttributes.RouteTemplateProviderAttributes)
|
||||
{
|
||||
actions.Add(new ActionInfo()
|
||||
{
|
||||
ActionName = actionName,
|
||||
HttpMethods = GetRouteTemplateHttpMethods(routeTemplateProvider),
|
||||
RequireActionNameMatch = true,
|
||||
AttributeRoute = routeTemplateProvider
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static string[] GetRouteTemplateHttpMethods(IRouteTemplateProvider routeTemplateProvider)
|
||||
{
|
||||
var provider = routeTemplateProvider as IActionHttpMethodProvider;
|
||||
if (provider != null && provider.HttpMethods != null)
|
||||
{
|
||||
return provider.HttpMethods.ToArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IEnumerable<ActionInfo> GetActionsForMethodsWithoutCustomAttributes(
|
||||
MethodInfo methodInfo,
|
||||
TypeInfo controllerTypeInfo)
|
||||
|
|
@ -226,12 +322,15 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
private class ActionAttributes
|
||||
{
|
||||
public IEnumerable<IActionHttpMethodProvider> HttpMethodProviderAttributes { get; set; }
|
||||
public ActionNameAttribute ActionNameAttribute { get; set; }
|
||||
public IEnumerable<IActionHttpMethodProvider> HttpMethodProviderAttributes { get; set; }
|
||||
public IEnumerable<IRouteTemplateProvider> RouteTemplateProviderAttributes { get; set; }
|
||||
|
||||
public bool Any()
|
||||
{
|
||||
return ActionNameAttribute != null || HttpMethodProviderAttributes.Any();
|
||||
return ActionNameAttribute != null ||
|
||||
HttpMethodProviderAttributes.Any() ||
|
||||
RouteTemplateProviderAttributes.Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// <summary>
|
||||
/// Identifies an action that only supports a given set of HTTP methods.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public abstract class HttpMethodAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
|
||||
{
|
||||
private int? _order;
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,86 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_AggregateErrorMessage_ErrorNumber"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method '{0}' that defines attribute routed actions must not have attributes that implement '{1}' and do not implement '{2}':{3}{4}
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_InvalidHttpConstraints
|
||||
{
|
||||
get { return GetString("AttributeRoute_InvalidHttpConstraints"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method '{0}' that defines attribute routed actions must not have attributes that implement '{1}' and do not implement '{2}':{3}{4}
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_InvalidHttpConstraints(object p0, object p1, object p2, object p3, object p4)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_InvalidHttpConstraints"), p0, p1, p2, p3, p4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action '{0}' with route template '{1}' has '{2}' invalid '{3}' attributes.
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_InvalidHttpConstraints_Item
|
||||
{
|
||||
get { return GetString("AttributeRoute_InvalidHttpConstraints_Item"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action '{0}' with route template '{1}' has '{2}' invalid '{3}' attributes.
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_InvalidHttpConstraints_Item(object p0, object p1, object p2, object p3)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_InvalidHttpConstraints_Item"), p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method '{0}' must not define attribute routed actions and non attribute routed actions at the same time:{1}{2}
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod
|
||||
{
|
||||
get { return GetString("AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method '{0}' must not define attribute routed actions and non attribute routed actions at the same time:{1}{2}
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod(object p0, object p1, object p2)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod"), p0, p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action: '{0}' - Template: '{1}'
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item
|
||||
{
|
||||
get { return GetString("AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action: '{0}' - Template: '{1}'
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item(object p0, object p1)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item"), p0, p1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// (none)
|
||||
/// </summary>
|
||||
internal static string AttributeRoute_NullTemplateRepresentation
|
||||
{
|
||||
get { return GetString("AttributeRoute_NullTemplateRepresentation"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// (none)
|
||||
/// </summary>
|
||||
internal static string FormatAttributeRoute_NullTemplateRepresentation()
|
||||
{
|
||||
return GetString("AttributeRoute_NullTemplateRepresentation");
|
||||
}
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
#if ASPNETCORE50
|
||||
using System.Reflection;
|
||||
#endif
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
|
|
@ -99,6 +97,12 @@ namespace Microsoft.AspNet.Mvc
|
|||
actionModel.IsActionNameMatchRequired = actionInfo.RequireActionNameMatch;
|
||||
actionModel.HttpMethods.AddRange(actionInfo.HttpMethods ?? Enumerable.Empty<string>());
|
||||
|
||||
if (actionInfo.AttributeRoute != null)
|
||||
{
|
||||
actionModel.AttributeRouteModel = new ReflectedAttributeRouteModel(
|
||||
actionInfo.AttributeRoute);
|
||||
}
|
||||
|
||||
foreach (var parameter in methodInfo.GetParameters())
|
||||
{
|
||||
actionModel.Parameters.Add(new ReflectedParameterModel(parameter));
|
||||
|
|
@ -119,48 +123,74 @@ namespace Microsoft.AspNet.Mvc
|
|||
var hasAttributeRoutes = false;
|
||||
var removalConstraints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var methodInfoMap = new MethodToActionMap();
|
||||
|
||||
var routeTemplateErrors = new List<string>();
|
||||
var attributeRoutingConfigurationErrors = new Dictionary<MethodInfo, string>();
|
||||
|
||||
foreach (var controller in model.Controllers)
|
||||
{
|
||||
var controllerDescriptor = new ControllerDescriptor(controller.ControllerType);
|
||||
foreach (var action in controller.Actions)
|
||||
{
|
||||
var actionDescriptor = CreateActionDescriptor(
|
||||
action,
|
||||
controller,
|
||||
controllerDescriptor,
|
||||
model.Filters);
|
||||
// Controllers with multiple [Route] attributes (or user defined implementation of
|
||||
// IRouteTemplateProvider) will generate one action descriptor per IRouteTemplateProvider
|
||||
// instance.
|
||||
// Actions with multiple [Http*] attributes or other (IRouteTemplateProvider implementations
|
||||
// have already been identified as different actions during action discovery.
|
||||
var actionDescriptors = CreateActionDescriptors(action, controller, controllerDescriptor);
|
||||
|
||||
AddActionConstraints(actionDescriptor, action, controller);
|
||||
AddControllerRouteConstraints(actionDescriptor, controller.RouteConstraints, removalConstraints);
|
||||
|
||||
if (IsAttributeRoutedAction(actionDescriptor))
|
||||
foreach (var actionDescriptor in actionDescriptors)
|
||||
{
|
||||
hasAttributeRoutes = true;
|
||||
AddActionFilters(actionDescriptor, action.Filters, controller.Filters, model.Filters);
|
||||
AddActionConstraints(actionDescriptor, action, controller);
|
||||
AddControllerRouteConstraints(
|
||||
actionDescriptor,
|
||||
controller.RouteConstraints,
|
||||
removalConstraints);
|
||||
|
||||
// An attribute routed action will ignore conventional routed constraints. We still
|
||||
// want to provide these values as ambient values for link generation.
|
||||
AddConstraintsAsDefaultRouteValues(actionDescriptor);
|
||||
if (IsAttributeRoutedAction(actionDescriptor))
|
||||
{
|
||||
hasAttributeRoutes = true;
|
||||
|
||||
// Replaces tokens like [controller]/[action] in the route template with the actual values
|
||||
// for this action.
|
||||
ReplaceAttributeRouteTokens(actionDescriptor, routeTemplateErrors);
|
||||
// An attribute routed action will ignore conventional routed constraints. We still
|
||||
// want to provide these values as ambient values for link generation.
|
||||
AddConstraintsAsDefaultRouteValues(actionDescriptor);
|
||||
|
||||
// Attribute routed actions will ignore conventional routed constraints. Instead they have
|
||||
// a single route constraint "RouteGroup" associated with it.
|
||||
ReplaceRouteConstraints(actionDescriptor);
|
||||
// Replaces tokens like [controller]/[action] in the route template with the actual values
|
||||
// for this action.
|
||||
ReplaceAttributeRouteTokens(actionDescriptor, routeTemplateErrors);
|
||||
|
||||
// Attribute routed actions will ignore conventional routed constraints. Instead they have
|
||||
// a single route constraint "RouteGroup" associated with it.
|
||||
ReplaceRouteConstraints(actionDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
actions.Add(actionDescriptor);
|
||||
methodInfoMap.AddToMethodInfo(action, actionDescriptors);
|
||||
actions.AddRange(actionDescriptors);
|
||||
}
|
||||
}
|
||||
|
||||
var actionsByRouteName = new Dictionary<string, IList<ActionDescriptor>>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Keeps track of all the methods that we've validated to avoid visiting each action group
|
||||
// more than once.
|
||||
var validatedMethods = new HashSet<MethodInfo>();
|
||||
|
||||
foreach (var actionDescriptor in actions)
|
||||
{
|
||||
if (!validatedMethods.Contains(actionDescriptor.MethodInfo))
|
||||
{
|
||||
ValidateActionGroupConfiguration(
|
||||
methodInfoMap,
|
||||
actionDescriptor,
|
||||
attributeRoutingConfigurationErrors);
|
||||
|
||||
validatedMethods.Add(actionDescriptor.MethodInfo);
|
||||
}
|
||||
|
||||
if (!IsAttributeRoutedAction(actionDescriptor))
|
||||
{
|
||||
// Any attribute routes are in use, then non-attribute-routed action descriptors can't be
|
||||
|
|
@ -203,34 +233,71 @@ namespace Microsoft.AspNet.Mvc
|
|||
}
|
||||
}
|
||||
|
||||
if (attributeRoutingConfigurationErrors.Any())
|
||||
{
|
||||
var message = CreateAttributeRoutingAggregateErrorMessage(
|
||||
attributeRoutingConfigurationErrors.Values);
|
||||
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
var namedRoutedErrors = ValidateNamedAttributeRoutedActions(actionsByRouteName);
|
||||
if (namedRoutedErrors.Any())
|
||||
{
|
||||
namedRoutedErrors = AddErrorNumbers(namedRoutedErrors);
|
||||
|
||||
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
|
||||
Environment.NewLine,
|
||||
string.Join(Environment.NewLine + Environment.NewLine, namedRoutedErrors));
|
||||
|
||||
var message = CreateAttributeRoutingAggregateErrorMessage(namedRoutedErrors);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
if (routeTemplateErrors.Any())
|
||||
{
|
||||
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
|
||||
Environment.NewLine,
|
||||
string.Join(Environment.NewLine + Environment.NewLine, routeTemplateErrors));
|
||||
|
||||
var message = CreateAttributeRoutingAggregateErrorMessage(routeTemplateErrors);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static ReflectedActionDescriptor CreateActionDescriptor(ReflectedActionModel action,
|
||||
private static IList<ReflectedActionDescriptor> CreateActionDescriptors(
|
||||
ReflectedActionModel action,
|
||||
ReflectedControllerModel controller,
|
||||
ControllerDescriptor controllerDescriptor,
|
||||
IEnumerable<IFilter> globalFilters)
|
||||
ControllerDescriptor controllerDescriptor)
|
||||
{
|
||||
var actionDescriptors = new List<ReflectedActionDescriptor>();
|
||||
|
||||
// We check the action to see if the template allows combination behavior
|
||||
// (It doesn't start with / or ~/) so that in the case where we have multiple
|
||||
// [Route] attributes on the controller we don't end up creating multiple
|
||||
// attribute identical attribute routes.
|
||||
if (controller.AttributeRoutes != null &&
|
||||
controller.AttributeRoutes.Count > 0 &&
|
||||
(action.AttributeRouteModel == null ||
|
||||
!action.AttributeRouteModel.IsAbsoluteTemplate))
|
||||
{
|
||||
foreach (var controllerAttributeRoute in controller.AttributeRoutes)
|
||||
{
|
||||
var actionDescriptor = CreateActionDescriptor(
|
||||
action,
|
||||
controllerAttributeRoute,
|
||||
controllerDescriptor);
|
||||
|
||||
actionDescriptors.Add(actionDescriptor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
actionDescriptors.Add(CreateActionDescriptor(
|
||||
action,
|
||||
controllerAttributeRoute: null,
|
||||
controllerDescriptor: controllerDescriptor));
|
||||
}
|
||||
|
||||
return actionDescriptors;
|
||||
}
|
||||
|
||||
private static ReflectedActionDescriptor CreateActionDescriptor(
|
||||
ReflectedActionModel action,
|
||||
ReflectedAttributeRouteModel controllerAttributeRoute,
|
||||
ControllerDescriptor controllerDescriptor)
|
||||
{
|
||||
var parameterDescriptors = new List<ParameterDescriptor>();
|
||||
foreach (var parameter in action.Parameters)
|
||||
|
|
@ -257,7 +324,9 @@ namespace Microsoft.AspNet.Mvc
|
|||
parameterDescriptors.Add(paramDescriptor);
|
||||
}
|
||||
|
||||
var attributeRouteInfo = CreateAttributeRouteInfo(action, controller);
|
||||
var attributeRouteInfo = CreateAttributeRouteInfo(
|
||||
action.AttributeRouteModel,
|
||||
controllerAttributeRoute);
|
||||
|
||||
var actionDescriptor = new ReflectedActionDescriptor()
|
||||
{
|
||||
|
|
@ -274,23 +343,30 @@ namespace Microsoft.AspNet.Mvc
|
|||
action.ActionMethod.DeclaringType.FullName,
|
||||
action.ActionMethod.Name);
|
||||
|
||||
actionDescriptor.FilterDescriptors =
|
||||
action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
|
||||
.Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
|
||||
.Concat(globalFilters.Select(f => new FilterDescriptor(f, FilterScope.Global)))
|
||||
.OrderBy(d => d, FilterDescriptorOrderComparer.Comparer)
|
||||
.ToList();
|
||||
|
||||
return actionDescriptor;
|
||||
}
|
||||
|
||||
private static void AddActionFilters(
|
||||
ReflectedActionDescriptor actionDescriptor,
|
||||
IEnumerable<IFilter> actionFilters,
|
||||
IEnumerable<IFilter> controllerFilters,
|
||||
IEnumerable<IFilter> globalFilters)
|
||||
{
|
||||
actionDescriptor.FilterDescriptors = actionFilters
|
||||
.Select(f => new FilterDescriptor(f, FilterScope.Action))
|
||||
.Concat(controllerFilters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
|
||||
.Concat(globalFilters.Select(f => new FilterDescriptor(f, FilterScope.Global)))
|
||||
.OrderBy(d => d, FilterDescriptorOrderComparer.Comparer)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static AttributeRouteInfo CreateAttributeRouteInfo(
|
||||
ReflectedActionModel action,
|
||||
ReflectedControllerModel controller)
|
||||
ReflectedAttributeRouteModel action,
|
||||
ReflectedAttributeRouteModel controller)
|
||||
{
|
||||
var combinedRoute = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(
|
||||
controller.AttributeRouteModel,
|
||||
action.AttributeRouteModel);
|
||||
controller,
|
||||
action);
|
||||
|
||||
if (combinedRoute == null)
|
||||
{
|
||||
|
|
@ -471,7 +547,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
}
|
||||
|
||||
private static IList<string> AddErrorNumbers(
|
||||
IList<string> namedRoutedErrors)
|
||||
IEnumerable<string> namedRoutedErrors)
|
||||
{
|
||||
return namedRoutedErrors
|
||||
.Select((nre, i) =>
|
||||
|
|
@ -527,10 +603,240 @@ namespace Microsoft.AspNet.Mvc
|
|||
return namedRouteErrors;
|
||||
}
|
||||
|
||||
private void ValidateActionGroupConfiguration(
|
||||
IDictionary<MethodInfo, IDictionary<ReflectedActionModel, IList<ReflectedActionDescriptor>>> methodMap,
|
||||
ReflectedActionDescriptor actionDescriptor,
|
||||
IDictionary<MethodInfo, string> routingConfigurationErrors)
|
||||
{
|
||||
string combinedErrorMessage = null;
|
||||
|
||||
var hasAttributeRoutedActions = false;
|
||||
var hasConventionallyRoutedActions = false;
|
||||
|
||||
var invalidHttpMethodActions = new Dictionary<ReflectedActionModel, IEnumerable<string>>();
|
||||
|
||||
var actionsForMethod = methodMap[actionDescriptor.MethodInfo];
|
||||
foreach (var reflectedAction in actionsForMethod)
|
||||
{
|
||||
foreach (var action in reflectedAction.Value)
|
||||
{
|
||||
if (IsAttributeRoutedAction(action))
|
||||
{
|
||||
hasAttributeRoutedActions = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasConventionallyRoutedActions = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep a list of actions with possible invalid IHttpActionMethodProvider attributes
|
||||
// to generate an error in case the method generates attribute routed actions.
|
||||
ValidateActionHttpMethodProviders(reflectedAction.Key, invalidHttpMethodActions);
|
||||
}
|
||||
|
||||
// Validate that no method result in attribute and non attribute actions at the same time.
|
||||
// By design, mixing attribute and conventionally actions in the same method is not allowed.
|
||||
// This is for example the case when someone uses[HttpGet("Products")] and[HttpPost]
|
||||
// on the same method.
|
||||
if (hasAttributeRoutedActions && hasConventionallyRoutedActions)
|
||||
{
|
||||
combinedErrorMessage = CreateMixedRoutedActionDescriptorsErrorMessage(
|
||||
actionDescriptor,
|
||||
actionsForMethod);
|
||||
}
|
||||
|
||||
// Validate that no method that creates attribute routed actions and
|
||||
// also uses attributes that only constrain the set of HTTP methods. For example,
|
||||
// if an attribute that implements IActionHttpMethodProvider but does not implement
|
||||
// IRouteTemplateProvider is used with an attribute that implements IRouteTemplateProvider on
|
||||
// the same action, the HTTP methods provided by the attribute that only implements
|
||||
// IActionHttpMethodProvider would be silently ignored, so we choose to throw to
|
||||
// inform the user of the invalid configuration.
|
||||
if (hasAttributeRoutedActions && invalidHttpMethodActions.Any())
|
||||
{
|
||||
var errorMessage = CreateInvalidActionHttpMethodProviderErrorMessage(
|
||||
actionDescriptor,
|
||||
invalidHttpMethodActions,
|
||||
actionsForMethod);
|
||||
|
||||
combinedErrorMessage = CombineErrorMessage(combinedErrorMessage, errorMessage);
|
||||
}
|
||||
|
||||
if (combinedErrorMessage != null)
|
||||
{
|
||||
routingConfigurationErrors.Add(actionDescriptor.MethodInfo, combinedErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateActionHttpMethodProviders(
|
||||
ReflectedActionModel reflectedAction,
|
||||
IDictionary<ReflectedActionModel, IEnumerable<string>> invalidHttpMethodActions)
|
||||
{
|
||||
var invalidHttpMethodProviderAttributes = reflectedAction.Attributes
|
||||
.Where(attr => attr is IActionHttpMethodProvider &&
|
||||
!(attr is IRouteTemplateProvider))
|
||||
.Select(attr => attr.GetType().FullName);
|
||||
|
||||
if (invalidHttpMethodProviderAttributes.Any())
|
||||
{
|
||||
invalidHttpMethodActions.Add(
|
||||
reflectedAction,
|
||||
invalidHttpMethodProviderAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CombineErrorMessage(string combinedErrorMessage, string errorMessage)
|
||||
{
|
||||
if (combinedErrorMessage == null)
|
||||
{
|
||||
combinedErrorMessage = errorMessage;
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedErrorMessage = string.Join(
|
||||
Environment.NewLine,
|
||||
combinedErrorMessage,
|
||||
errorMessage);
|
||||
}
|
||||
|
||||
return combinedErrorMessage;
|
||||
}
|
||||
|
||||
private static string CreateInvalidActionHttpMethodProviderErrorMessage(
|
||||
ReflectedActionDescriptor actionDescriptor,
|
||||
IDictionary<ReflectedActionModel, IEnumerable<string>> invalidHttpMethodActions,
|
||||
IDictionary<ReflectedActionModel, IList<ReflectedActionDescriptor>> actionsForMethod)
|
||||
{
|
||||
var messagesForMethodInfo = new List<string>();
|
||||
foreach (var invalidAction in invalidHttpMethodActions)
|
||||
{
|
||||
var invalidAttributesList = string.Join(", ", invalidAction.Value);
|
||||
|
||||
foreach (var descriptor in actionsForMethod[invalidAction.Key])
|
||||
{
|
||||
// We only report errors in attribute routed actions. For example, an action
|
||||
// that contains [HttpGet("Products")], [HttpPost] and [HttpHead], where [HttpHead]
|
||||
// only implements IHttpActionMethodProvider and restricts the action to only allow
|
||||
// the head method, will report that the action contains invalid IActionHttpMethodProvider
|
||||
// attributes only for the action generated by [HttpGet("Products")].
|
||||
// [HttpPost] will be treated as an action that produces a conventionally routed action
|
||||
// and the fact that the method generates attribute and non attributed actions will be
|
||||
// reported as a different error.
|
||||
if (IsAttributeRoutedAction(descriptor))
|
||||
{
|
||||
var messageItem = Resources.FormatAttributeRoute_InvalidHttpConstraints_Item(
|
||||
descriptor.DisplayName,
|
||||
descriptor.AttributeRouteInfo.Template,
|
||||
invalidAttributesList,
|
||||
typeof(IActionHttpMethodProvider).FullName);
|
||||
|
||||
messagesForMethodInfo.Add(messageItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var methodFullName = string.Format("{0}.{1}",
|
||||
actionDescriptor.MethodInfo.DeclaringType.FullName,
|
||||
actionDescriptor.MethodInfo.Name);
|
||||
|
||||
// Sample message:
|
||||
// A method 'MyApplication.CustomerController.Index' that defines attribute routed actions must
|
||||
// not have attributes that implement 'Microsoft.AspNet.Mvc.IActionHttpMethodProvider'
|
||||
// and do not implement 'Microsoft.AspNet.Mvc.Routing.IRouteTemplateProvider':
|
||||
// Action 'MyApplication.CustomerController.Index' has 'Namespace.CustomHttpMethodAttribute'
|
||||
// invalid 'Microsoft.AspNet.Mvc.IActionHttpMethodProvider' attributes.
|
||||
return
|
||||
Resources.FormatAttributeRoute_InvalidHttpConstraints(
|
||||
methodFullName,
|
||||
typeof(IActionHttpMethodProvider).FullName,
|
||||
typeof(IRouteTemplateProvider).FullName,
|
||||
Environment.NewLine,
|
||||
string.Join(Environment.NewLine, messagesForMethodInfo));
|
||||
}
|
||||
|
||||
private static string CreateMixedRoutedActionDescriptorsErrorMessage(
|
||||
ReflectedActionDescriptor actionDescriptor,
|
||||
IDictionary<ReflectedActionModel, IList<ReflectedActionDescriptor>> actionsForMethod)
|
||||
{
|
||||
// Text to show as the attribute route template for conventionally routed actions.
|
||||
var nullTemplate = Resources.AttributeRoute_NullTemplateRepresentation;
|
||||
|
||||
var actionDescriptions = actionsForMethod
|
||||
.SelectMany(a => a.Value)
|
||||
.Select(ad =>
|
||||
Resources.FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item(
|
||||
ad.DisplayName,
|
||||
ad.AttributeRouteInfo != null ? ad.AttributeRouteInfo.Template : nullTemplate));
|
||||
|
||||
var methodFullName = string.Format("{0}.{1}",
|
||||
actionDescriptor.MethodInfo.DeclaringType.FullName,
|
||||
actionDescriptor.MethodInfo.Name);
|
||||
|
||||
// Sample error message:
|
||||
// A method 'MyApplication.CustomerController.Index' must not define attributed actions and
|
||||
// non attributed actions at the same time:
|
||||
// Action: 'MyApplication.CustomerController.Index' - Template: 'Products'
|
||||
// Action: 'MyApplication.CustomerController.Index' - Template: '(none)'
|
||||
return
|
||||
Resources.FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod(
|
||||
methodFullName,
|
||||
Environment.NewLine,
|
||||
string.Join(Environment.NewLine, actionDescriptions));
|
||||
}
|
||||
|
||||
private static string CreateAttributeRoutingAggregateErrorMessage(
|
||||
IEnumerable<string> individualErrors)
|
||||
{
|
||||
var errorMessages = AddErrorNumbers(individualErrors);
|
||||
|
||||
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
|
||||
Environment.NewLine,
|
||||
string.Join(Environment.NewLine + Environment.NewLine, errorMessages));
|
||||
return message;
|
||||
}
|
||||
|
||||
private static string GetRouteGroupValue(int order, string template)
|
||||
{
|
||||
var group = string.Format("{0}-{1}", order, template);
|
||||
return ("__route__" + group).ToUpperInvariant();
|
||||
}
|
||||
|
||||
// We need to build a map of methods to reflected actions and reflected actions to
|
||||
// action descriptors so that we can validate later that no method produced attribute
|
||||
// and non attributed actions at the same time, and that no method that produced attribute
|
||||
// routed actions has no attributes that implement IActionHttpMethodProvider and do not
|
||||
// implement IRouteTemplateProvider. For example:
|
||||
//
|
||||
// public class ProductsController
|
||||
// {
|
||||
// [HttpGet("Products")]
|
||||
// [HttpPost]
|
||||
// public ActionResult Items(){ ... }
|
||||
//
|
||||
// [HttpGet("Products")]
|
||||
// [CustomHttpMethods("POST, PUT")]
|
||||
// public ActionResult List(){ ... }
|
||||
// }
|
||||
private class MethodToActionMap :
|
||||
Dictionary<MethodInfo, IDictionary<ReflectedActionModel, IList<ReflectedActionDescriptor>>>
|
||||
{
|
||||
public void AddToMethodInfo(ReflectedActionModel action,
|
||||
IList<ReflectedActionDescriptor> actionDescriptors)
|
||||
{
|
||||
IDictionary<ReflectedActionModel, IList<ReflectedActionDescriptor>> actionsForMethod = null;
|
||||
if (TryGetValue(action.ActionMethod, out actionsForMethod))
|
||||
{
|
||||
actionsForMethod.Add(action, actionDescriptors);
|
||||
}
|
||||
else
|
||||
{
|
||||
var reflectedActionMap =
|
||||
new Dictionary<ReflectedActionModel, IList<ReflectedActionDescriptor>>();
|
||||
reflectedActionMap.Add(action, actionDescriptors);
|
||||
Add(action.ActionMethod, reflectedActionMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,6 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
Attributes = actionMethod.GetCustomAttributes(inherit: true).OfType<object>().ToList();
|
||||
|
||||
Filters = Attributes.OfType<IFilter>().ToList();
|
||||
|
||||
var routeTemplateAttribute = Attributes.OfType<IRouteTemplateProvider>().FirstOrDefault();
|
||||
if (routeTemplateAttribute != null)
|
||||
{
|
||||
AttributeRouteModel = new ReflectedAttributeRouteModel(routeTemplateAttribute);
|
||||
}
|
||||
|
||||
HttpMethods = new List<string>();
|
||||
Parameters = new List<ReflectedParameterModel>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool IsAbsoluteTemplate
|
||||
{
|
||||
get
|
||||
{
|
||||
return Template != null &&
|
||||
IsOverridePattern(Template);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines two <see cref="ReflectedAttributeRouteModel"/> instances and returns
|
||||
/// a new <see cref="ReflectedAttributeRouteModel"/> instance with the result.
|
||||
|
|
|
|||
|
|
@ -24,11 +24,9 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
Filters = Attributes.OfType<IFilter>().ToList();
|
||||
RouteConstraints = Attributes.OfType<RouteConstraintAttribute>().ToList();
|
||||
|
||||
var routeTemplateAttribute = Attributes.OfType<IRouteTemplateProvider>().FirstOrDefault();
|
||||
if (routeTemplateAttribute != null)
|
||||
{
|
||||
AttributeRouteModel = new ReflectedAttributeRouteModel(routeTemplateAttribute);
|
||||
}
|
||||
AttributeRoutes = Attributes.OfType<IRouteTemplateProvider>()
|
||||
.Select(rtp => new ReflectedAttributeRouteModel(rtp))
|
||||
.ToList();
|
||||
|
||||
ControllerName = controllerType.Name.EndsWith("Controller", StringComparison.Ordinal)
|
||||
? controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length)
|
||||
|
|
@ -47,6 +45,6 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
|
||||
public List<RouteConstraintAttribute> RouteConstraints { get; private set; }
|
||||
|
||||
public ReflectedAttributeRouteModel AttributeRouteModel { get; set; }
|
||||
public List<ReflectedAttributeRouteModel> AttributeRoutes { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -375,4 +375,22 @@
|
|||
<value>Error {0}:{1}{2}</value>
|
||||
<comment>{0} is the error number, {1} is Environment.NewLine {2} is the error message</comment>
|
||||
</data>
|
||||
<data name="AttributeRoute_InvalidHttpConstraints" xml:space="preserve">
|
||||
<value>A method '{0}' that defines attribute routed actions must not have attributes that implement '{1}' and do not implement '{2}':{3}{4}</value>
|
||||
<comment>{0} is the MethodInfo.FullName, {1} is typeof(IActionHttpMethodProvider).FullName, {2} is typeof(IRouteTemplateProvider).FullName, {3} is Environment.NewLine, {4} is the list of actions and their respective invalid IActionHttpMethodProvider attributes formatted using AttributeRoute_InvalidHttpMethodConstraints_Item</comment>
|
||||
</data>
|
||||
<data name="AttributeRoute_InvalidHttpConstraints_Item" xml:space="preserve">
|
||||
<value>Action '{0}' with route template '{1}' has '{2}' invalid '{3}' attributes.</value>
|
||||
<comment>{0} The display name of the action, {1} the route template, {2} the formatted list of invalid attributes using string.Join(", ", attributes), {3} is typeof(IActionHttpMethodProvider).FullName.</comment>
|
||||
</data>
|
||||
<data name="AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod" xml:space="preserve">
|
||||
<value>A method '{0}' must not define attribute routed actions and non attribute routed actions at the same time:{1}{2}</value>
|
||||
<comment>{0} is the MethodInfo.FullName, {1} is Environment.NewLine, {2} is the formatted list of actions defined by that method info.</comment>
|
||||
</data>
|
||||
<data name="AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item" xml:space="preserve">
|
||||
<value>Action: '{0}' - Template: '{1}'</value>
|
||||
</data>
|
||||
<data name="AttributeRoute_NullTemplateRepresentation" xml:space="preserve">
|
||||
<value>(none)</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// <summary>
|
||||
/// Specifies an attribute route on a controller.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public class RouteAttribute : Attribute, IRouteTemplateProvider
|
||||
{
|
||||
private int? _order;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
using System.Reflection;
|
||||
using Microsoft.AspNet.Mvc.DefaultActionDiscoveryConventionsControllers;
|
||||
using Xunit;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
|
|
@ -146,6 +149,279 @@ namespace Microsoft.AspNet.Mvc
|
|||
Assert.False(isValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_ConventionallyRoutedAction_WithoutHttpConstraints()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
|
||||
var actionName = nameof(ConventionallyRoutedController.Edit);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
Assert.Equal("Edit", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
Assert.Null(action.HttpMethods);
|
||||
Assert.Null(action.AttributeRoute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_ConventionallyRoutedAction_WithHttpConstraints()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
|
||||
var actionName = nameof(ConventionallyRoutedController.Update);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
Assert.Equal("Update", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
Assert.Equal(new[] { "PUT", "PATCH" }, action.HttpMethods);
|
||||
Assert.Null(action.AttributeRoute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_ConventionallyRoutedActionWithHttpConstraints_AndInvalidRouteTemplateProvider()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
|
||||
var actionName = nameof(ConventionallyRoutedController.Delete);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
Assert.Equal("Delete", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
|
||||
var httpMethod = Assert.Single(action.HttpMethods);
|
||||
Assert.Equal("DELETE", httpMethod);
|
||||
Assert.Null(action.AttributeRoute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_ConventionallyRoutedAction_WithMultipleHttpConstraints()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
|
||||
var actionName = nameof(ConventionallyRoutedController.Details);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
Assert.Equal("Details", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
Assert.Equal(new[] { "POST", "GET" }, action.HttpMethods);
|
||||
Assert.Null(action.AttributeRoute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_ConventionallyRoutedAction_WithMultipleOverlappingHttpConstraints()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(ConventionallyRoutedController).GetTypeInfo();
|
||||
var actionName = nameof(ConventionallyRoutedController.List);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
Assert.Equal("List", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
Assert.Equal(new[] { "GET", "PUT", "POST" }, action.HttpMethods);
|
||||
Assert.Null(action.AttributeRoute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_AttributeRouteOnAction()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
|
||||
var actionName = nameof(NoRouteAttributeOnControllerController.Edit);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
|
||||
Assert.Equal("Edit", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
|
||||
var httpMethod = Assert.Single(action.HttpMethods);
|
||||
Assert.Equal("POST", httpMethod);
|
||||
|
||||
Assert.NotNull(action.AttributeRoute);
|
||||
Assert.Equal("Change", action.AttributeRoute.Template);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_AttributeRouteOnAction_RouteAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
|
||||
var actionName = nameof(NoRouteAttributeOnControllerController.Update);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
|
||||
Assert.Equal("Update", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
|
||||
Assert.Null(action.HttpMethods);
|
||||
|
||||
Assert.NotNull(action.AttributeRoute);
|
||||
Assert.Equal("Update", action.AttributeRoute.Template);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_AttributeRouteOnAction_AcceptVerbsAttributeWithTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
|
||||
var actionName = nameof(NoRouteAttributeOnControllerController.List);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
|
||||
Assert.Equal("List", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
|
||||
Assert.Equal(new[] { "GET", "HEAD" }, action.HttpMethods);
|
||||
|
||||
Assert.NotNull(action.AttributeRoute);
|
||||
Assert.Equal("ListAll", action.AttributeRoute.Template);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_AttributeRouteOnAction_CreatesOneActionInforPerRouteTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
|
||||
var actionName = nameof(NoRouteAttributeOnControllerController.Index);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, actionInfos.Count());
|
||||
|
||||
foreach (var action in actionInfos)
|
||||
{
|
||||
Assert.Equal("Index", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
|
||||
Assert.NotNull(action.AttributeRoute);
|
||||
}
|
||||
|
||||
var list = Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("List"));
|
||||
var listMethod = Assert.Single(list.HttpMethods);
|
||||
Assert.Equal("POST", listMethod);
|
||||
|
||||
var all = Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("All"));
|
||||
var allMethod = Assert.Single(all.HttpMethods);
|
||||
Assert.Equal("GET", allMethod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActions_NoRouteOnController_AllowsConventionallyRoutedActions_OnTheSameController()
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = typeof(NoRouteAttributeOnControllerController).GetTypeInfo();
|
||||
var actionName = nameof(NoRouteAttributeOnControllerController.Remove);
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod(actionName), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
|
||||
Assert.Equal("Remove", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
|
||||
Assert.Null(action.HttpMethods);
|
||||
|
||||
Assert.Null(action.AttributeRoute);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(SingleRouteAttributeController))]
|
||||
[InlineData(typeof(MultipleRouteAttributeController))]
|
||||
public void GetActions_RouteAttributeOnController_CreatesAttributeRoute_ForNonAttributedActions(Type controller)
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = controller.GetTypeInfo();
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod("Delete"), typeInfo);
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(actionInfos);
|
||||
|
||||
Assert.Equal("Delete", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
|
||||
Assert.Null(action.HttpMethods);
|
||||
|
||||
Assert.Null(action.AttributeRoute);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(SingleRouteAttributeController))]
|
||||
[InlineData(typeof(MultipleRouteAttributeController))]
|
||||
public void GetActions_RouteOnController_CreatesOneActionInforPerRouteTemplateOnAction(Type controller)
|
||||
{
|
||||
// Arrange
|
||||
var conventions = new DefaultActionDiscoveryConventions();
|
||||
var typeInfo = controller.GetTypeInfo();
|
||||
|
||||
// Act
|
||||
var actionInfos = conventions.GetActions(typeInfo.GetMethod("Index"), typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, actionInfos.Count());
|
||||
|
||||
foreach (var action in actionInfos)
|
||||
{
|
||||
Assert.Equal("Index", action.ActionName);
|
||||
Assert.True(action.RequireActionNameMatch);
|
||||
|
||||
var httpMethod = Assert.Single(action.HttpMethods);
|
||||
Assert.Equal("GET", httpMethod);
|
||||
|
||||
Assert.NotNull(action.AttributeRoute);
|
||||
}
|
||||
|
||||
Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("List"));
|
||||
Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("All"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsController_UserDefinedClass()
|
||||
{
|
||||
|
|
@ -408,4 +684,84 @@ namespace Microsoft.AspNet.Mvc.DefaultActionDiscoveryConventionsControllers
|
|||
return new OperatorOverloadingController();
|
||||
}
|
||||
}
|
||||
|
||||
public class NoRouteAttributeOnControllerController : Controller
|
||||
{
|
||||
[HttpGet("All")]
|
||||
[HttpPost("List")]
|
||||
public void Index() { }
|
||||
|
||||
[HttpPost("Change")]
|
||||
public void Edit() { }
|
||||
|
||||
public void Remove() { }
|
||||
|
||||
[Route("Update")]
|
||||
public void Update() { }
|
||||
|
||||
[AcceptVerbs("GET", "HEAD", Route = "ListAll")]
|
||||
public void List() { }
|
||||
}
|
||||
|
||||
[Route("Products")]
|
||||
public class SingleRouteAttributeController : Controller
|
||||
{
|
||||
[HttpGet("All")]
|
||||
[HttpGet("List")]
|
||||
public void Index() { }
|
||||
|
||||
public void Delete() { }
|
||||
}
|
||||
|
||||
[Route("Products")]
|
||||
[Route("Items")]
|
||||
public class MultipleRouteAttributeController : Controller
|
||||
{
|
||||
[HttpGet("All")]
|
||||
[HttpGet("List")]
|
||||
public void Index() { }
|
||||
|
||||
public void Delete() { }
|
||||
}
|
||||
|
||||
// Here the constraints on the methods are acting as an IActionHttpMethodProvider and
|
||||
// not as an IRouteTemplateProvider given that there is no RouteAttribute
|
||||
// on the controller and the template for all the constraints on a method is null.
|
||||
public class ConventionallyRoutedController : Controller
|
||||
{
|
||||
public void Edit() { }
|
||||
|
||||
[CustomHttpMethods("PUT", "PATCH")]
|
||||
public void Update() { }
|
||||
|
||||
[HttpDelete]
|
||||
public void Delete() { }
|
||||
|
||||
[HttpPost]
|
||||
[HttpGet]
|
||||
public void Details() { }
|
||||
|
||||
[HttpGet]
|
||||
[HttpPut]
|
||||
[AcceptVerbs("GET", "POST")]
|
||||
public void List() { }
|
||||
|
||||
// Keep it private and nested to avoid polluting the namespace.
|
||||
private class CustomHttpMethodsAttribute : Attribute, IActionHttpMethodProvider
|
||||
{
|
||||
private readonly string[] _methods;
|
||||
|
||||
public CustomHttpMethodsAttribute(params string [] methods)
|
||||
{
|
||||
_methods = methods;
|
||||
}
|
||||
public IEnumerable<string> HttpMethods
|
||||
{
|
||||
get
|
||||
{
|
||||
return _methods;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,23 +58,55 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
Assert.Equal(FilterScope.Action, filter3.Scope);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(HttpMethodController), nameof(HttpMethodController.OnlyPost), "POST")]
|
||||
[InlineData(typeof(AttributeRoutedHttpMethodController), nameof(AttributeRoutedHttpMethodController.PutOrPatch), "PUT,PATCH")]
|
||||
public void GetDescriptors_AddsHttpMethodConstraints(Type controllerType, string actionName, string expectedMethods)
|
||||
[Fact]
|
||||
public void GetDescriptors_AddsHttpMethodConstraints_ForConventionallyRoutedActions()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(controllerType.GetTypeInfo());
|
||||
var provider = GetProvider(typeof(HttpMethodController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var descriptors = provider.GetDescriptors();
|
||||
var descriptor = Assert.Single(descriptors);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(actionName, descriptor.Name);
|
||||
Assert.Equal("OnlyPost", descriptor.Name);
|
||||
|
||||
Assert.Single(descriptor.MethodConstraints);
|
||||
Assert.Equal(expectedMethods.Split(','), descriptor.MethodConstraints[0].HttpMethods);
|
||||
Assert.Equal(new string[] { "POST" }, descriptor.MethodConstraints[0].HttpMethods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDescriptors_ThrowsIfHttpMethodConstraints_OnAttributeRoutedActions()
|
||||
{
|
||||
// Arrange
|
||||
var expectedExceptionMessage =
|
||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"Error 1:" + Environment.NewLine +
|
||||
"A method 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"AttributeRoutedHttpMethodController.PutOrPatch'" +
|
||||
" that defines attribute routed actions must not have attributes that implement " +
|
||||
"'Microsoft.AspNet.Mvc.IActionHttpMethodProvider' and do not implement " +
|
||||
"'Microsoft.AspNet.Mvc.Routing.IRouteTemplateProvider':" + Environment.NewLine +
|
||||
"Action 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"AttributeRoutedHttpMethodController.PutOrPatch' with route template 'Products' has " +
|
||||
"'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+CustomHttpMethodConstraintAttribute'" +
|
||||
" invalid 'Microsoft.AspNet.Mvc.IActionHttpMethodProvider' attributes." + Environment.NewLine +
|
||||
"Action 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"AttributeRoutedHttpMethodController.PutOrPatch' with route template 'Items' has " +
|
||||
"'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+CustomHttpMethodConstraintAttribute'" +
|
||||
" invalid 'Microsoft.AspNet.Mvc.IActionHttpMethodProvider' attributes.";
|
||||
|
||||
var provider = GetProvider(
|
||||
typeof(AttributeRoutedHttpMethodController)
|
||||
.GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => provider.GetDescriptors());
|
||||
|
||||
// Act
|
||||
Assert.Equal(expectedExceptionMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -450,13 +482,13 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
|
||||
var conventional = Assert.Single(model.Controllers,
|
||||
c => c.ControllerName == "ConventionallyRouted");
|
||||
Assert.Null(conventional.AttributeRouteModel);
|
||||
Assert.Empty(conventional.AttributeRoutes);
|
||||
Assert.Single(conventional.Actions);
|
||||
|
||||
var attributeRouted = Assert.Single(model.Controllers,
|
||||
c => c.ControllerName == "AttributeRouted");
|
||||
Assert.Single(attributeRouted.Actions);
|
||||
Assert.NotNull(attributeRouted.AttributeRouteModel);
|
||||
Assert.Single(attributeRouted.AttributeRoutes);
|
||||
|
||||
var empty = Assert.Single(model.Controllers,
|
||||
c => c.ControllerName == "Empty");
|
||||
|
|
@ -521,7 +553,9 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
|
||||
// Assert
|
||||
var controller = Assert.Single(model.Controllers);
|
||||
Assert.Equal("api/Token/[key]/[controller]", controller.AttributeRouteModel.Template);
|
||||
|
||||
var attributeRouteModel = Assert.Single(controller.AttributeRoutes);
|
||||
Assert.Equal("api/Token/[key]/[controller]", attributeRouteModel.Template);
|
||||
|
||||
var action = Assert.Single(controller.Actions);
|
||||
Assert.Equal("stub/[action]", action.AttributeRouteModel.Template);
|
||||
|
|
@ -550,11 +584,13 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
var expectedMessage =
|
||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"Error 1:" + Environment.NewLine +
|
||||
"For action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"MultipleErrorsController.Unknown'" + Environment.NewLine +
|
||||
"Error: While processing template 'stub/[action]/[unknown]', a replacement value for the token 'unknown' " +
|
||||
"could not be found. Available tokens: 'controller, action'." + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"Error 2:" + Environment.NewLine +
|
||||
"For action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"MultipleErrorsController.Invalid'" + Environment.NewLine +
|
||||
"Error: The route template '[invalid/syntax' has invalid syntax. A replacement token is not closed.";
|
||||
|
|
@ -566,6 +602,211 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
Assert.Equal(expectedMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_CreatesOneActionDescriptor_PerControllerAndActionRouteCombination()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(MultiRouteAttributesController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var descriptors = provider.GetDescriptors();
|
||||
|
||||
// Assert
|
||||
var actions = descriptors.Where(d => d.Name == "MultipleHttpGet");
|
||||
Assert.Equal(4, actions.Count());
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
Assert.Equal("MultipleHttpGet", action.Name);
|
||||
Assert.Equal("MultiRouteAttributes", action.ControllerName);
|
||||
}
|
||||
|
||||
Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v1/List"));
|
||||
Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v1/All"));
|
||||
Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v2/List"));
|
||||
Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v2/All"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_AcceptVerbsOnAction_CreatesActionPerControllerAttributeRouteCombination()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(MultiRouteAttributesController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var descriptors = provider.GetDescriptors();
|
||||
|
||||
// Assert
|
||||
var actions = descriptors.Where(d => d.Name == "AcceptVerbs");
|
||||
Assert.Equal(2, actions.Count());
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
Assert.Equal("MultiRouteAttributes", action.ControllerName);
|
||||
|
||||
Assert.NotNull(action.MethodConstraints);
|
||||
var methodConstraint = Assert.Single(action.MethodConstraints);
|
||||
|
||||
Assert.NotNull(methodConstraint.HttpMethods);
|
||||
Assert.Equal(new[] { "POST" }, methodConstraint.HttpMethods);
|
||||
}
|
||||
|
||||
Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v1/List"));
|
||||
Assert.Single(actions, a => a.AttributeRouteInfo.Template.Equals("v2/List"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_AcceptVerbsOnActionWithOverrideTemplate_CreatesSingleAttributeRoutedAction()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(MultiRouteAttributesController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var descriptors = provider.GetDescriptors();
|
||||
|
||||
// Assert
|
||||
var action = Assert.Single(descriptors, d => d.Name == "AcceptVerbsOverride");
|
||||
|
||||
Assert.Equal("MultiRouteAttributes", action.ControllerName);
|
||||
|
||||
Assert.NotNull(action.MethodConstraints);
|
||||
var methodConstraint = Assert.Single(action.MethodConstraints);
|
||||
|
||||
Assert.NotNull(methodConstraint.HttpMethods);
|
||||
Assert.Equal(new[] { "PUT" }, methodConstraint.HttpMethods);
|
||||
|
||||
Assert.NotNull(action.AttributeRouteInfo);
|
||||
Assert.Equal("Override", action.AttributeRouteInfo.Template);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_AcceptVerbsOnAction_DoesNotApplyHttpMethods_ToOtherAttributeRoutes()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(MultiRouteAttributesController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var descriptors = provider.GetDescriptors();
|
||||
|
||||
// Assert
|
||||
var actions = descriptors.Where(d => d.Name == "AcceptVerbsRouteAttributeAndHttpPut");
|
||||
Assert.Equal(6, actions.Count());
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
Assert.Equal("MultiRouteAttributes", action.ControllerName);
|
||||
|
||||
Assert.NotNull(action.AttributeRouteInfo);
|
||||
Assert.NotNull(action.AttributeRouteInfo.Template);
|
||||
}
|
||||
|
||||
var constrainedActions = actions.Where(a => a.MethodConstraints != null);
|
||||
Assert.Equal(4, constrainedActions.Count());
|
||||
|
||||
// Actions generated by AcceptVerbs
|
||||
var postActions = constrainedActions.Where(a => a.MethodConstraints.Single().HttpMethods.Single() == "POST");
|
||||
Assert.Equal(2, postActions.Count());
|
||||
Assert.Single(postActions, a => a.AttributeRouteInfo.Template.Equals("v1"));
|
||||
Assert.Single(postActions, a => a.AttributeRouteInfo.Template.Equals("v2"));
|
||||
|
||||
// Actions generated by PutAttribute
|
||||
var putActions = constrainedActions.Where(a => a.MethodConstraints.Single().HttpMethods.Single() == "PUT");
|
||||
Assert.Equal(2, putActions.Count());
|
||||
Assert.Single(putActions, a => a.AttributeRouteInfo.Template.Equals("v1/All"));
|
||||
Assert.Single(putActions, a => a.AttributeRouteInfo.Template.Equals("v2/All"));
|
||||
|
||||
// Actions generated by RouteAttribute
|
||||
var unconstrainedActions = actions.Where(a => a.MethodConstraints == null);
|
||||
Assert.Equal(2, unconstrainedActions.Count());
|
||||
Assert.Single(unconstrainedActions, a => a.AttributeRouteInfo.Template.Equals("v1/List"));
|
||||
Assert.Single(unconstrainedActions, a => a.AttributeRouteInfo.Template.Equals("v2/List"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_AllowsDuplicateAttributeRoutedActions_WithTheSameTemplateAndSameHttpMethodsOnDifferentActions()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(NonDuplicatedAttributeRouteController).GetTypeInfo());
|
||||
var firstActionName = nameof(NonDuplicatedAttributeRouteController.ControllerAndAction);
|
||||
var secondActionName = nameof(NonDuplicatedAttributeRouteController.OverrideOnAction);
|
||||
|
||||
// Act
|
||||
var actions = provider.GetDescriptors();
|
||||
|
||||
// Assert
|
||||
var controllerAndAction = Assert.Single(actions, a => a.Name.Equals(firstActionName));
|
||||
Assert.NotNull(controllerAndAction.AttributeRouteInfo);
|
||||
|
||||
var controllerActionAndOverride = Assert.Single(actions, a => a.Name.Equals(secondActionName));
|
||||
Assert.NotNull(controllerActionAndOverride.AttributeRouteInfo);
|
||||
|
||||
Assert.Equal(
|
||||
controllerAndAction.AttributeRouteInfo.Template,
|
||||
controllerActionAndOverride.AttributeRouteInfo.Template,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_AllowsDuplicateAttributeRoutedActions_WithTheSameTemplateAndDifferentHttpMethodsOnTheSameAction()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(typeof(NonDuplicatedAttributeRouteController).GetTypeInfo());
|
||||
var actionName = nameof(NonDuplicatedAttributeRouteController.DifferentHttpMethods);
|
||||
|
||||
// Act
|
||||
var descriptors = provider.GetDescriptors();
|
||||
|
||||
// Assert
|
||||
var actions = descriptors.Where(d => d.Name.Equals(actionName));
|
||||
Assert.Equal(5, actions.Count());
|
||||
|
||||
foreach (var method in new[] { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
{
|
||||
var action = Assert.Single(
|
||||
actions,
|
||||
a => a.MethodConstraints.SelectMany(c => c.HttpMethods).Contains(method));
|
||||
|
||||
Assert.NotNull(action.AttributeRouteInfo);
|
||||
Assert.Equal("Products/list", action.AttributeRouteInfo.Template);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_ThrowsIfAttributeRoutedAndNonAttributedActions_OnTheSameMethod()
|
||||
{
|
||||
// Arrange
|
||||
var expectedMessage =
|
||||
"The following errors occurred with attribute routing information:" + Environment.NewLine +
|
||||
Environment.NewLine +
|
||||
"Error 1:" + Environment.NewLine +
|
||||
"A method 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method'" +
|
||||
" must not define attribute routed actions and non attribute routed actions at the same time:" + Environment.NewLine +
|
||||
"Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method' - Template: 'AttributeRouted'" + Environment.NewLine +
|
||||
"Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method' - Template: '(none)'" + Environment.NewLine +
|
||||
"Action: 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method' - Template: '(none)'" + Environment.NewLine +
|
||||
"A method 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method' that defines attribute routed actions must not" +
|
||||
" have attributes that implement 'Microsoft.AspNet.Mvc.IActionHttpMethodProvider' and do not implement" +
|
||||
" 'Microsoft.AspNet.Mvc.Routing.IRouteTemplateProvider':" + Environment.NewLine +
|
||||
"Action 'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+" +
|
||||
"AttributeAndNonAttributeRoutedActionsOnSameMethodController.Method' with route template 'AttributeRouted' has " +
|
||||
"'Microsoft.AspNet.Mvc.Test.ReflectedActionDescriptorProviderTests+CustomHttpMethodConstraintAttribute'" +
|
||||
" invalid 'Microsoft.AspNet.Mvc.IActionHttpMethodProvider' attributes.";
|
||||
|
||||
var provider = GetProvider(
|
||||
typeof(AttributeAndNonAttributeRoutedActionsOnSameMethodController).GetTypeInfo());
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => provider.GetDescriptors());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeRouting_RouteOnControllerAndAction_CreatesActionDescriptorWithoutHttpConstraints()
|
||||
{
|
||||
|
|
@ -843,9 +1084,10 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
}
|
||||
|
||||
[Route("Products")]
|
||||
[Route("Items")]
|
||||
private class AttributeRoutedHttpMethodController
|
||||
{
|
||||
[AcceptVerbs("PUT", "PATCH")]
|
||||
[CustomHttpMethodConstraint("PUT", "PATCH")]
|
||||
public void PutOrPatch() { }
|
||||
}
|
||||
|
||||
|
|
@ -1013,6 +1255,26 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
public void Delete(int id) { }
|
||||
}
|
||||
|
||||
[Route("v1")]
|
||||
[Route("v2")]
|
||||
public class MultiRouteAttributesController
|
||||
{
|
||||
[HttpGet("List")]
|
||||
[HttpGet("All")]
|
||||
public void MultipleHttpGet() { }
|
||||
|
||||
[AcceptVerbs("POST", Route = "List")]
|
||||
public void AcceptVerbs() { }
|
||||
|
||||
[AcceptVerbs("PUT", Route = "/Override")]
|
||||
public void AcceptVerbsOverride() { }
|
||||
|
||||
[AcceptVerbs("POST")]
|
||||
[Route("List")]
|
||||
[HttpPut("All")]
|
||||
public void AcceptVerbsRouteAttributeAndHttpPut() { }
|
||||
}
|
||||
|
||||
[Route("Products")]
|
||||
public class OnlyRouteController
|
||||
{
|
||||
|
|
@ -1020,6 +1282,48 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
public void Action() { }
|
||||
}
|
||||
|
||||
public class AttributeAndNonAttributeRoutedActionsOnSameMethodController
|
||||
{
|
||||
[HttpGet("AttributeRouted")]
|
||||
[HttpPost]
|
||||
[AcceptVerbs("PUT", "PATCH")]
|
||||
[CustomHttpMethodConstraint("DELETE")]
|
||||
public void Method() { }
|
||||
}
|
||||
|
||||
[Route("Product")]
|
||||
[Route("/Product")]
|
||||
[Route("/product")]
|
||||
public class DuplicatedAttributeRouteController : Controller
|
||||
{
|
||||
[HttpGet("/List")]
|
||||
[HttpGet("/List")]
|
||||
public void Action() { }
|
||||
|
||||
public void Controller() { }
|
||||
|
||||
[HttpPut("list")]
|
||||
[PutOrPatch("list")]
|
||||
public void CommonHttpMethod() { }
|
||||
}
|
||||
|
||||
[Route("Products")]
|
||||
public class NonDuplicatedAttributeRouteController : Controller
|
||||
{
|
||||
[HttpGet("list")]
|
||||
public void ControllerAndAction() { }
|
||||
|
||||
[HttpGet("/PRODUCTS/LIST")]
|
||||
public void OverrideOnAction() { }
|
||||
|
||||
[HttpGet("list")]
|
||||
[HttpPost("list")]
|
||||
[HttpPut("list")]
|
||||
[HttpPatch("list")]
|
||||
[HttpDelete("list")]
|
||||
public void DifferentHttpMethods() { }
|
||||
}
|
||||
|
||||
[MyRouteConstraint(blockNonAttributedActions: true)]
|
||||
[MySecondRouteConstraint(blockNonAttributedActions: true)]
|
||||
private class ConstrainedController
|
||||
|
|
@ -1066,6 +1370,34 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
public void Action() { }
|
||||
}
|
||||
|
||||
private class CustomHttpMethodConstraintAttribute : Attribute, IActionHttpMethodProvider
|
||||
{
|
||||
private readonly string[] _methods;
|
||||
|
||||
public CustomHttpMethodConstraintAttribute(params string[] methods)
|
||||
{
|
||||
_methods = methods;
|
||||
}
|
||||
|
||||
public IEnumerable<string> HttpMethods
|
||||
{
|
||||
get
|
||||
{
|
||||
return _methods;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PutOrPatchAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly string[] _httpMethods = new string[] { "PUT", "PATCH" };
|
||||
|
||||
public PutOrPatchAttribute(string template)
|
||||
: base(_httpMethods, template)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class TestActionParameter
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
|
|
|||
|
|
@ -38,20 +38,6 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
|
|||
Assert.IsType<MyFilterAttribute>(model.Filters[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReflectedActionModel_PopulatesAttributeRouteInfo()
|
||||
{
|
||||
// Arrange
|
||||
var actionMethod = typeof(BlogController).GetMethod("Edit");
|
||||
|
||||
// Act
|
||||
var model = new ReflectedActionModel(actionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(model.AttributeRouteModel);
|
||||
Assert.Equal("Edit", model.AttributeRouteModel.Template);
|
||||
}
|
||||
|
||||
private class BlogController
|
||||
{
|
||||
[MyOther]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -19,12 +20,16 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
|
|||
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, model.Attributes.Count);
|
||||
Assert.Equal(5, model.Attributes.Count);
|
||||
|
||||
Assert.Single(model.Attributes, a => a is MyOtherAttribute);
|
||||
Assert.Single(model.Attributes, a => a is MyFilterAttribute);
|
||||
Assert.Single(model.Attributes, a => a is MyRouteConstraintAttribute);
|
||||
Assert.Single(model.Attributes, a => a is RouteAttribute);
|
||||
|
||||
var routes = model.Attributes.OfType<RouteAttribute>().ToList();
|
||||
Assert.Equal(2, routes.Count());
|
||||
Assert.Single(routes, r => r.Template.Equals("Blog"));
|
||||
Assert.Single(routes, r => r.Template.Equals("Microblog"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -91,14 +96,17 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
|
|||
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(model.AttributeRouteModel);
|
||||
Assert.Equal("Blog", model.AttributeRouteModel.Template);
|
||||
Assert.NotNull(model.AttributeRoutes);
|
||||
Assert.Equal(2, model.AttributeRoutes.Count); ;
|
||||
Assert.Single(model.AttributeRoutes, r => r.Template.Equals("Blog"));
|
||||
Assert.Single(model.AttributeRoutes, r => r.Template.Equals("Microblog"));
|
||||
}
|
||||
|
||||
[MyOther]
|
||||
[MyFilter]
|
||||
[MyRouteConstraint]
|
||||
[Route("Blog")]
|
||||
[Route("Microblog")]
|
||||
private class BlogController
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,163 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
result.RouteValues);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost/api/v1/Maps")]
|
||||
[InlineData("http://localhost/api/v2/Maps")]
|
||||
public async Task AttributeRoutedAction_MultipleRouteAttributes_WorksWithNameAndOrder(string url)
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Equal("Maps", result.Controller);
|
||||
Assert.Equal("Get", result.Action);
|
||||
|
||||
Assert.Equal(new string[]
|
||||
{
|
||||
"/api/v2/Maps",
|
||||
"/api/v1/Maps",
|
||||
"/api/v2/Maps"
|
||||
},
|
||||
result.ExpectedUrls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_MultipleRouteAttributes_WorksWithOverrideRoutes()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/api/v2/Maps";
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, url));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Equal("Maps", result.Controller);
|
||||
Assert.Equal("Post", result.Action);
|
||||
|
||||
Assert.Equal(new string[]
|
||||
{
|
||||
"/api/v2/Maps",
|
||||
"/api/v2/Maps"
|
||||
},
|
||||
result.ExpectedUrls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_MultipleRouteAttributes_RouteAttributeTemplatesIgnoredForOverrideActions()
|
||||
{
|
||||
// Arrange
|
||||
var url = "http://localhost/api/v1/Maps";
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.SendAsync(new HttpRequestMessage(new HttpMethod("POST"), url));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost/api/v1/Maps/5", "PUT")]
|
||||
[InlineData("http://localhost/api/v2/Maps/5", "PUT")]
|
||||
[InlineData("http://localhost/api/v1/Maps/PartialUpdate/5", "PATCH")]
|
||||
[InlineData("http://localhost/api/v2/Maps/PartialUpdate/5", "PATCH")]
|
||||
public async Task AttributeRoutedAction_MultipleRouteAttributes_CombinesWithMultipleHttpAttributes(
|
||||
string url,
|
||||
string method)
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.SendAsync(new HttpRequestMessage(new HttpMethod(method), url));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Equal("Maps", result.Controller);
|
||||
Assert.Equal("Update", result.Action);
|
||||
|
||||
Assert.Equal(new string[]
|
||||
{
|
||||
"/api/v2/Maps/PartialUpdate/5",
|
||||
"/api/v2/Maps/PartialUpdate/5"
|
||||
},
|
||||
result.ExpectedUrls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost/Banks/Get/5")]
|
||||
[InlineData("http://localhost/Bank/Get/5")]
|
||||
public async Task AttributeRoutedAction_MultipleHttpAttributesAndTokenReplacement(string url)
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
var expectedUrl = new Uri(url).AbsolutePath;
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Equal("Banks", result.Controller);
|
||||
Assert.Equal("Get", result.Action);
|
||||
|
||||
Assert.Equal(new string[]
|
||||
{
|
||||
"/Bank/Get/5",
|
||||
"/Bank/Get/5"
|
||||
},
|
||||
result.ExpectedUrls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost/api/v1/Maps/5", "PATCH")]
|
||||
[InlineData("http://localhost/api/v2/Maps/5", "PATCH")]
|
||||
[InlineData("http://localhost/api/v1/Maps/PartialUpdate/5", "PUT")]
|
||||
[InlineData("http://localhost/api/v2/Maps/PartialUpdate/5", "PUT")]
|
||||
public async Task AttributeRoutedAction_MultipleRouteAttributes_WithMultipleHttpAttributes_RespectsConstraints(
|
||||
string url,
|
||||
string method)
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
var expectedUrl = new Uri(url).AbsolutePath;
|
||||
|
||||
// Act
|
||||
var response = await client.SendAsync(new HttpRequestMessage(new HttpMethod(method), url));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
// The url would be /Store/ListProducts with conventional routes
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_IsNotReachableWithTraditionalRoute()
|
||||
|
|
@ -359,6 +516,57 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
Assert.Equal("UpdateEmployee", result.Action);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PUT")]
|
||||
[InlineData("PATCH")]
|
||||
public async Task AttributeRoutedAction_ControllerLevelRoute_WithAcceptVerbsAndRouteTemplate_IsReachable(string verb)
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var message = new HttpRequestMessage(new HttpMethod(verb), "http://localhost/api/Employee/Manager");
|
||||
var response = await client.SendAsync(message);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Contains("/api/Employee/Manager", result.ExpectedUrls);
|
||||
Assert.Equal("Employee", result.Controller);
|
||||
Assert.Equal("UpdateManager", result.Action);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PUT", "Bank")]
|
||||
[InlineData("PATCH", "Bank")]
|
||||
[InlineData("PUT", "Bank/Update")]
|
||||
[InlineData("PATCH", "Bank/Update")]
|
||||
public async Task AttributeRoutedAction_AcceptVerbsAndRouteTemplate_IsReachable(string verb, string path)
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.CreateClient();
|
||||
var expectedUrl = "/Bank";
|
||||
|
||||
// Act
|
||||
var message = new HttpRequestMessage(new HttpMethod(verb), "http://localhost/" + path);
|
||||
var response = await client.SendAsync(message);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Equal(new string[] { expectedUrl, expectedUrl }, result.ExpectedUrls);
|
||||
Assert.Equal("Banks", result.Controller);
|
||||
Assert.Equal("UpdateBank", result.Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_WithCustomHttpAttributes_IsReachable()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
using Microsoft.AspNet.Mvc;
|
||||
using System;
|
||||
|
||||
namespace RoutingWebSite
|
||||
{
|
||||
public class BanksController : Controller
|
||||
{
|
||||
private readonly TestResponseGenerator _generator;
|
||||
|
||||
public BanksController(TestResponseGenerator generator)
|
||||
{
|
||||
_generator = generator;
|
||||
}
|
||||
|
||||
[HttpGet("Banks/[action]/{id}")]
|
||||
[HttpGet("Bank/[action]/{id}")]
|
||||
public ActionResult Get(int id)
|
||||
{
|
||||
return _generator.Generate(
|
||||
Url.Action(),
|
||||
Url.RouteUrl(new { }));
|
||||
}
|
||||
|
||||
[AcceptVerbs("PUT", Route ="Bank")]
|
||||
[HttpPatch("Bank")]
|
||||
[AcceptVerbs("PUT", Route ="Bank/Update")]
|
||||
[HttpPatch("Bank/Update")]
|
||||
public ActionResult UpdateBank()
|
||||
{
|
||||
return _generator.Generate(
|
||||
Url.Action(),
|
||||
Url.RouteUrl(new { }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,12 @@ namespace RoutingWebSite
|
|||
return _generator.Generate("/api/Employee");
|
||||
}
|
||||
|
||||
[AcceptVerbs("PUT", "PATCH", Route = "Manager")]
|
||||
public IActionResult UpdateManager()
|
||||
{
|
||||
return _generator.Generate("/api/Employee/Manager");
|
||||
}
|
||||
|
||||
[HttpMerge("{id}")]
|
||||
public IActionResult MergeEmployee(int id)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
using Microsoft.AspNet.Mvc;
|
||||
using System;
|
||||
|
||||
namespace RoutingWebSite
|
||||
{
|
||||
[Route("api/v1/Maps", Name = "v1", Order = 1)]
|
||||
[Route("api/v2/Maps")]
|
||||
public class MapsController : Controller
|
||||
{
|
||||
private readonly TestResponseGenerator _generator;
|
||||
|
||||
public MapsController(TestResponseGenerator generator)
|
||||
{
|
||||
_generator = generator;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult Get()
|
||||
{
|
||||
// Multiple attribute routes with name and order.
|
||||
// We will always generate v2 routes except when
|
||||
// we explicitly use "v1" to generate a v1 route.
|
||||
return _generator.Generate(
|
||||
Url.Action(),
|
||||
Url.RouteUrl("v1"),
|
||||
Url.RouteUrl(new { }));
|
||||
}
|
||||
|
||||
[HttpPost("/api/v2/Maps")]
|
||||
public ActionResult Post()
|
||||
{
|
||||
return _generator.Generate(
|
||||
Url.Action(),
|
||||
Url.RouteUrl(new { }));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPatch("PartialUpdate/{id}")]
|
||||
public ActionResult Update(int id)
|
||||
{
|
||||
// We will generate "/api/v2/Maps/PartialUpdate/{id}"
|
||||
// in both cases, v1 routes will be discarded due to their
|
||||
// Order and for v2 routes PartialUpdate has higher precedence.
|
||||
// api/v1/Maps/{id} and api/v2/Maps/{id} will only match on PUT.
|
||||
// api/v1/Maps/PartialUpdate/{id} and api/v2/Maps/PartialUpdate/{id} will only match on PATCH.
|
||||
return _generator.Generate(
|
||||
Url.Action(),
|
||||
Url.RouteUrl(new { }));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue