Fix for #975 - Implementing IActionConstraint and ActionMethodSelectorAttribute

IActionConstraint follows a provider model similar to filters. The
attributes that go on actions/controllers can be simple metadata markers,
the 'real' constraint is provided by a set of configurable providers. In
general the simplest thing to do is to be both an
IActionConstraintMetadata and IActionConstraint, and then the default
provider will take care of you.

IActionConstraint now has stages based on the Order property. Each group
of constraints with the same Order will run together on the set of
actions. This process is repeated for each value of Order until we run out
of actions or run out of constraints.

The IActionConstraint interface is beefier than the equivalent in legacy
MVC. This is to support cooperative coding between sets of constraints
that know about each other. See the changes in the sample, which implement
webapi-style overloading.
This commit is contained in:
Ryan Nowak 2014-08-12 12:45:50 -07:00
parent 0d8a7368d9
commit 78a4e78358
34 changed files with 1415 additions and 618 deletions

View File

@ -1,4 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
using MvcSample.Web.Models;
namespace MvcSample.Web
@ -11,16 +16,24 @@ namespace MvcSample.Web
return Content("Get()");
}
[Overload]
public ActionResult Get(int id)
{
return Content("Get(id)");
}
[Overload]
public ActionResult Get(int id, string name)
{
return Content("Get(id, name)");
}
[Overload]
public ActionResult Get(string bleh)
{
return Content("Get(bleh)");
}
public ActionResult WithUser()
{
return Content("WithUser()");
@ -47,5 +60,76 @@ namespace MvcSample.Web
return result;
}
private class OverloadAttribute : Attribute, IActionConstraint
{
public int Order { get; } = Int32.MaxValue;
public bool Accept(ActionConstraintContext context)
{
var candidates = context.Candidates.Select(a => new
{
Action = a,
Parameters = GetOverloadableParameters(a.Action),
});
var valueProviderFactory = context.RouteContext.HttpContext.RequestServices
.GetService<ICompositeValueProviderFactory>();
var factoryContext = new ValueProviderFactoryContext(
context.RouteContext.HttpContext,
context.RouteContext.RouteData.Values);
var valueProvider = valueProviderFactory.GetValueProvider(factoryContext);
foreach (var group in candidates.GroupBy(c => c.Parameters.Count).OrderByDescending(g => g.Key))
{
var foundMatch = false;
foreach (var candidate in group)
{
var allFound = true;
foreach (var parameter in candidate.Parameters)
{
if (!(valueProvider.ContainsPrefixAsync(parameter.ParameterBindingInfo.Prefix).Result))
{
if (candidate.Action.Action == context.CurrentCandidate.Action)
{
return false;
}
allFound = false;
break;
}
}
if (allFound)
{
foundMatch = true;
}
}
if (foundMatch)
{
return group.Any(c => c.Action.Action == context.CurrentCandidate.Action);
}
}
return false;
}
private List<ParameterDescriptor> GetOverloadableParameters(ActionDescriptor action)
{
if (action.Parameters == null)
{
return new List<ParameterDescriptor>();
}
return action.Parameters.Where(
p =>
p.ParameterBindingInfo != null &&
!p.IsOptional &&
ValueProviderResult.CanConvertFromString(p.ParameterBindingInfo.ParameterType))
.ToList();
}
}
}
}

View File

@ -0,0 +1,30 @@
// 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.Collections.Generic;
using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Context for <see cref="IActionConstraint"/> execution.
/// </summary>
public class ActionConstraintContext
{
/// <summary>
/// The list of <see cref="ActionSelectorCandidate"/>. This includes all actions that are valid for the current
/// request, as well as their constraints.
/// </summary>
public IReadOnlyList<ActionSelectorCandidate> Candidates { get; set; }
/// <summary>
/// The current <see cref="ActionSelectorCandidate"/>.
/// </summary>
public ActionSelectorCandidate CurrentCandidate { get; set; }
/// <summary>
/// The <see cref="RouteContext"/>.
/// </summary>
public RouteContext RouteContext { get; set; }
}
}

View File

@ -0,0 +1,31 @@
// 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.
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Represents an <see cref="IActionConstraintMetadata"/> with or without a corresponding
/// <see cref="IActionConstraint"/>.
/// </summary>
public class ActionConstraintItem
{
/// <summary>
/// Creates a new <see cref="ActionConstraintItem"/>.
/// </summary>
/// <param name="metadata">The <see cref="IActionConstraintMetadata"/> instance.</param>
public ActionConstraintItem([NotNull] IActionConstraintMetadata metadata)
{
Metadata = metadata;
}
/// <summary>
/// The <see cref="IActionConstraint"/> associated with <see cref="Metadata"/>.
/// </summary>
public IActionConstraint Constraint { get; set; }
/// <summary>
/// The <see cref="IActionConstraintMetadata"/> instance.
/// </summary>
public IActionConstraintMetadata Metadata { get; private set; }
}
}

View File

@ -0,0 +1,36 @@
// 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.Collections.Generic;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Context for an action constraint provider.
/// </summary>
public class ActionConstraintProviderContext
{
/// <summary>
/// Creates a new <see cref="ActionConstraintProviderContext"/>.
/// </summary>
/// <param name="action">The <see cref="ActionDescriptor"/> for which constraints are being created.</param>
/// <param name="items">The list of <see cref="ActionConstraintItem"/> objects.</param>
public ActionConstraintProviderContext(
[NotNull] ActionDescriptor action,
[NotNull] IList<ActionConstraintItem> items)
{
Action = action;
Results = items;
}
/// <summary>
/// The <see cref="ActionDescriptor"/> for which constraints are being created.
/// </summary>
public ActionDescriptor Action { get; private set; }
/// <summary>
/// The list of <see cref="ActionConstraintItem"/> objects.
/// </summary>
public IList<ActionConstraintItem> Results { get; private set; }
}
}

View File

@ -0,0 +1,36 @@
// 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 Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Base class for attributes which can implement conditional logic to enable or disable an action
/// for a given request. See <see cref="IActionConstraint"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public abstract class ActionMethodSelectorAttribute : Attribute, IActionConstraint
{
/// <inheritdoc />
public int Order { get; set; }
/// <inheritdoc />
public bool Accept(ActionConstraintContext context)
{
return IsValidForRequest(context.RouteContext, context.CurrentCandidate.Action);
}
/// <summary>
/// Determines whether the action selection is valid for the specified route context.
/// </summary>
/// <param name="routeContext">The route context.</param>
/// <param name="action">Information about the action.</param>
/// <returns>
/// <see langword="true"/> if the action selection is valid for the specified context;
/// otherwise, <see langword="false"/>.
/// </returns>
public abstract bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action);
}
}

View File

@ -0,0 +1,36 @@
// 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.Collections.Generic;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// A candidate action for action selection.
/// </summary>
public class ActionSelectorCandidate
{
/// <summary>
/// Creates a new <see cref="ActionSelectorCandidate"/>.
/// </summary>
/// <param name="action">The <see cref="ActionDescriptor"/> representing a candidate for selection.</param>
/// <param name="constraints">
/// The list of <see cref="IActionConstraint"/> instances associated with <paramref name="action"/>.
/// </param>
public ActionSelectorCandidate([NotNull] ActionDescriptor action, IReadOnlyList<IActionConstraint> constraints)
{
Action = action;
Constraints = constraints;
}
/// <summary>
/// The <see cref="ActionDescriptor"/> representing a candiate for selection.
/// </summary>
public ActionDescriptor Action { get; private set; }
/// <summary>
/// The list of <see cref="IActionConstraint"/> instances associated with <see name="Action"/>.
/// </summary>
public IReadOnlyList<IActionConstraint> Constraints { get; private set; }
}
}

View File

@ -0,0 +1,67 @@
// 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 Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// A default implementation of <see cref="INestedProvider{ActionConstraintProviderContext}"/>.
/// </summary>
/// <remarks>
/// This provider is able to provide an <see cref="IActionConstraint"/> instance when the
/// <see cref="IActionConstraintMetadata"/> implements <see cref="IActionConstraint"/> or
/// <see cref="IActionConstraintFactory"/>/
/// </remarks>
public class DefaultActionConstraintProvider : INestedProvider<ActionConstraintProviderContext>
{
private readonly IServiceProvider _services;
/// <summary>
/// Creates a new <see cref="DefaultActionConstraintProvider"/>.
/// </summary>
/// <param name="services">The per-request services.</param>
public DefaultActionConstraintProvider(IServiceProvider services)
{
_services = services;
}
/// <inheritdoc />
public int Order { get; set; }
/// <inheritdoc />
public void Invoke([NotNull] ActionConstraintProviderContext context, [NotNull] Action callNext)
{
foreach (var item in context.Results)
{
ProvideConstraint(item);
}
callNext();
}
private void ProvideConstraint(ActionConstraintItem item)
{
// Don't overwrite anything that was done by a previous provider.
if (item.Constraint != null)
{
return;
}
var constraint = item.Metadata as IActionConstraint;
if (constraint != null)
{
item.Constraint = constraint;
return;
}
var factory = item.Metadata as IActionConstraintFactory;
if (factory != null)
{
item.Constraint = factory.CreateInstance(_services);
return;
}
}
}
}

View File

@ -0,0 +1,51 @@
// 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.
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Supports conditional logic to determine whether or not an associated action is valid to be selected
/// for the given request.
/// </summary>
/// <remarks>
/// Action constraints have the secondary effect of making an action with a constraint applied a better
/// match than one without.
///
/// Consider two actions, 'A' and 'B' with the same action and controller name. Action 'A' only allows the
/// HTTP POST method (via a constraint) and action 'B' has no constraints.
///
/// If an incoming request is a POST, then 'A' is considered the best match because it both matches and
/// has a constraint. If an incoming request uses any other verb, 'A' will not be valid for selection
/// due to it's constraint, so 'B' is the best match.
///
///
/// Action constraints are also grouped according to their order value. Any constraints with the same
/// group value are considered to be part of the same application policy, and will be executed in the
/// same stage.
///
/// Stages run in ascending order based on the value of <see cref="Order"/>. Given a set of actions which
/// are candidates for selection, the next stage to run is the lowest value of <see cref="Order"/> for any
/// constraint of any candidate which is greater than the order of the last stage.
///
/// Once the stage order is identified, each action has all of it's constraints in that stage executed.
/// If any constraint does not match, then that action is not a candidate for selection. If any actions
/// with constraints in the current state are still candidates, then those are the 'best' actions and this
/// process will repeat with the next stage on the set of 'best' actions. If after processing the
/// subsequent stages of the 'best' actions no candidates remain, this process will repeat on the set of
/// 'other' candidate actions from this stage (those without a constraint).
/// </remarks>
public interface IActionConstraint : IActionConstraintMetadata
{
/// <summary>
/// The constraint order.
/// </summary>
int Order { get; }
/// <summary>
/// Determines whether an action is a valid candidate for selection.
/// </summary>
/// <param name="context">The <see cref="ActionConstraintContext"/>.</param>
/// <returns>True if the action is valid for selection, otherwise false.</returns>
bool Accept(ActionConstraintContext context);
}
}

View File

@ -0,0 +1,27 @@
// 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;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// A factory for <see cref="IActionConstraint"/>.
/// </summary>
/// <remarks>
/// <see cref="IActionConstraintFactory"/> will be invoked by <see cref="DefaultActionConstraintProvider"/>
/// to create constraint instances for an action.
///
/// Place an attribute implementing this interface on a controller or action to insert an action
/// constraint created by a factory.
/// </remarks>
public interface IActionConstraintFactory : IActionConstraintMetadata
{
/// <summary>
/// Creates a new <see cref="IActionConstraint"/>.
/// </summary>
/// <param name="services">The per-request services.</param>
/// <returns>An <see cref="IActionConstraint"/>.</returns>
IActionConstraint CreateInstance(IServiceProvider services);
}
}

View File

@ -0,0 +1,12 @@
// 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.
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// A marker interface that identifies a type as metadata for an <see cref="IActionConstraint"/>.
/// </summary>
public interface IActionConstraintMetadata
{
}
}

View File

@ -23,9 +23,10 @@ namespace Microsoft.AspNet.Mvc
public Dictionary<string, object> RouteValueDefaults { get; private set; }
public List<HttpMethodConstraint> MethodConstraints { get; set; }
public List<IActionConstraint> DynamicConstraints { get; set; }
/// <summary>
/// The set of constraints for this action. Must all be satisfied for the action to be selected.
/// </summary>
public List<IActionConstraintMetadata> ActionConstraints { get; set; }
public List<ParameterDescriptor> Parameters { get; set; }

View File

@ -1,7 +1,6 @@
// 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.Collections.Generic;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
@ -9,8 +8,13 @@ namespace Microsoft.AspNet.Mvc
public class ActionInfo
{
public string ActionName { get; set; }
public string[] HttpMethods { get; set; }
public IRouteTemplateProvider AttributeRoute { get; set; }
public object[] Attributes { get; set; }
public bool RequireActionNameMatch { get; set; }
}
}

View File

@ -45,7 +45,8 @@ namespace Microsoft.AspNet.Mvc
return null;
}
var actionInfos = GetActionsForMethodsWithCustomAttributes(methodInfo, controllerTypeInfo);
var attributes = GetActionCustomAttributes(methodInfo);
var actionInfos = GetActionsForMethodsWithCustomAttributes(attributes, methodInfo, controllerTypeInfo);
if (actionInfos.Any())
{
return actionInfos;
@ -58,6 +59,7 @@ namespace Microsoft.AspNet.Mvc
new ActionInfo()
{
ActionName = methodInfo.Name,
Attributes = attributes.Attributes,
RequireActionNameMatch = true,
}
};
@ -89,21 +91,16 @@ namespace Microsoft.AspNet.Mvc
method.GetBaseDefinition().DeclaringType != typeof(object);
}
private bool HasCustomAttributes(MethodInfo methodInfo)
{
var actionAttributes = GetActionCustomAttributes(methodInfo);
return actionAttributes.Any();
}
private ActionAttributes GetActionCustomAttributes(MethodInfo methodInfo)
{
var attributes = methodInfo.GetCustomAttributes();
var attributes = methodInfo.GetCustomAttributes(inherit: true).OfType<object>().ToArray();
var actionNameAttribute = attributes.OfType<ActionNameAttribute>().FirstOrDefault();
var httpMethodConstraints = attributes.OfType<IActionHttpMethodProvider>();
var routeTemplates = attributes.OfType<IRouteTemplateProvider>();
return new ActionAttributes()
{
Attributes = attributes,
ActionNameAttribute = actionNameAttribute,
HttpMethodProviderAttributes = httpMethodConstraints,
RouteTemplateProviderAttributes = routeTemplates,
@ -111,16 +108,16 @@ namespace Microsoft.AspNet.Mvc
}
private IEnumerable<ActionInfo> GetActionsForMethodsWithCustomAttributes(
ActionAttributes actionAttributes,
MethodInfo methodInfo,
TypeInfo controller)
{
var hasControllerAttributeRoutes = HasValidControllerRouteTemplates(controller);
var actionAttributes = GetActionCustomAttributes(methodInfo);
// 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)
if (actionAttributes.HasSpecialAttribute() || hasControllerAttributeRoutes)
{
var actionNameAttribute = actionAttributes.ActionNameAttribute;
var actionName = actionNameAttribute != null ? actionNameAttribute.Name : methodInfo.Name;
@ -178,6 +175,7 @@ namespace Microsoft.AspNet.Mvc
{
HttpMethods = httpMethods,
ActionName = actionName,
Attributes = actionAttributes.Attributes,
RequireActionNameMatch = true,
};
}
@ -194,6 +192,7 @@ namespace Microsoft.AspNet.Mvc
{
actions.Add(new ActionInfo
{
Attributes = actionAttributes.Attributes,
ActionName = actionName,
HttpMethods = null,
RequireActionNameMatch = true,
@ -203,8 +202,14 @@ namespace Microsoft.AspNet.Mvc
foreach (var routeTemplateProvider in actionAttributes.RouteTemplateProviderAttributes)
{
// We want to exclude the attributes from the other route template providers;
var attributes = actionAttributes.Attributes
.Where(a => a == routeTemplateProvider || !(a is IRouteTemplateProvider))
.ToArray();
actions.Add(new ActionInfo()
{
Attributes = attributes,
ActionName = actionName,
HttpMethods = GetRouteTemplateHttpMethods(routeTemplateProvider),
RequireActionNameMatch = true,
@ -229,10 +234,13 @@ namespace Microsoft.AspNet.Mvc
private class ActionAttributes
{
public ActionNameAttribute ActionNameAttribute { get; set; }
public object[] Attributes { get; set; }
public IEnumerable<IActionHttpMethodProvider> HttpMethodProviderAttributes { get; set; }
public IEnumerable<IRouteTemplateProvider> RouteTemplateProviderAttributes { get; set; }
public bool Any()
public bool HasSpecialAttribute()
{
return ActionNameAttribute != null ||
HttpMethodProviderAttributes.Any() ||

View File

@ -9,6 +9,7 @@ using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Mvc
@ -17,15 +18,18 @@ namespace Microsoft.AspNet.Mvc
{
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
private readonly IActionSelectorDecisionTreeProvider _decisionTreeProvider;
private readonly INestedProviderManager<ActionConstraintProviderContext> _actionConstraintProvider;
private ILogger _logger;
public DefaultActionSelector(
[NotNull] IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider,
[NotNull] IActionSelectorDecisionTreeProvider decisionTreeProvider,
[NotNull] INestedProviderManager<ActionConstraintProviderContext> actionConstraintProvider,
[NotNull] ILoggerFactory loggerFactory)
{
_actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider;
_decisionTreeProvider = decisionTreeProvider;
_actionConstraintProvider = actionConstraintProvider;
_logger = loggerFactory.Create<DefaultActionSelector>();
}
@ -36,45 +40,36 @@ namespace Microsoft.AspNet.Mvc
var tree = _decisionTreeProvider.DecisionTree;
var matchingRouteConstraints = tree.Select(context.RouteData.Values);
var matchingRouteAndMethodConstraints =
matchingRouteConstraints.Where(ad =>
MatchMethodConstraints(ad, context)).ToList();
var matchingRouteAndMethodAndDynamicConstraints =
matchingRouteAndMethodConstraints.Where(ad =>
MatchDynamicConstraints(ad, context)).ToList();
var matching = matchingRouteAndMethodAndDynamicConstraints;
var matchesWithConstraints = new List<ActionDescriptor>();
foreach (var match in matching)
var candidates = new List<ActionSelectorCandidate>();
foreach (var action in matchingRouteConstraints)
{
if (match.DynamicConstraints != null && match.DynamicConstraints.Any() ||
match.MethodConstraints != null && match.MethodConstraints.Any())
var constraints = GetConstraints(action);
candidates.Add(new ActionSelectorCandidate(action, constraints));
}
var matchingActionConstraints =
EvaluateActionConstraints(context, candidates, startingOrder: null);
List<ActionDescriptor> matchingActions = null;
if (matchingActionConstraints != null)
{
matchingActions = new List<ActionDescriptor>(matchingActionConstraints.Count);
foreach (var candidate in matchingActionConstraints)
{
matchesWithConstraints.Add(match);
matchingActions.Add(candidate.Action);
}
}
// If any action that's applicable has constraints, this is considered better than
// an action without.
if (matchesWithConstraints.Any())
{
matching = matchesWithConstraints;
}
var finalMatches = SelectBestActions(matchingActions);
var finalMatches = SelectBestActions(matching);
if (finalMatches.Count == 0)
if (finalMatches == null || finalMatches.Count == 0)
{
if (_logger.IsEnabled(TraceType.Information))
{
_logger.WriteValues(new DefaultActionSelectorSelectAsyncValues()
{
ActionsMatchingRouteConstraints = matchingRouteConstraints,
ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints,
ActionsMatchingRouteAndMethodAndDynamicConstraints =
matchingRouteAndMethodAndDynamicConstraints,
ActionsMatchingActionConstraints = matchingActions,
FinalMatches = finalMatches,
});
}
@ -90,9 +85,7 @@ namespace Microsoft.AspNet.Mvc
_logger.WriteValues(new DefaultActionSelectorSelectAsyncValues()
{
ActionsMatchingRouteConstraints = matchingRouteConstraints,
ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints,
ActionsMatchingRouteAndMethodAndDynamicConstraints =
matchingRouteAndMethodAndDynamicConstraints,
ActionsMatchingActionConstraints = matchingActions,
FinalMatches = finalMatches,
SelectedAction = selectedAction
});
@ -107,9 +100,7 @@ namespace Microsoft.AspNet.Mvc
_logger.WriteValues(new DefaultActionSelectorSelectAsyncValues()
{
ActionsMatchingRouteConstraints = matchingRouteConstraints,
ActionsMatchingRouteAndMethodConstraints = matchingRouteAndMethodConstraints,
ActionsMatchingRouteAndMethodAndDynamicConstraints =
matchingRouteAndMethodAndDynamicConstraints,
ActionsMatchingActionConstraints = matchingActions,
FinalMatches = finalMatches,
});
}
@ -137,16 +128,96 @@ namespace Microsoft.AspNet.Mvc
return actions;
}
private bool MatchMethodConstraints(ActionDescriptor descriptor, RouteContext context)
private IReadOnlyList<ActionSelectorCandidate> EvaluateActionConstraints(
RouteContext context,
IReadOnlyList<ActionSelectorCandidate> candidates,
int? startingOrder)
{
return descriptor.MethodConstraints == null ||
descriptor.MethodConstraints.All(c => c.Accept(context));
}
// Find the next group of constraints to process. This will be the lowest value of
// order that is higher than startingOrder.
int? order = null;
foreach (var candidate in candidates)
{
if (candidate.Constraints != null)
{
foreach (var constraint in candidate.Constraints)
{
if ((startingOrder == null || constraint.Order > startingOrder) &&
(order == null || constraint.Order < order))
{
order = constraint.Order;
}
}
}
}
private bool MatchDynamicConstraints(ActionDescriptor descriptor, RouteContext context)
{
return descriptor.DynamicConstraints == null ||
descriptor.DynamicConstraints.All(c => c.Accept(context));
// If we don't find a 'next' then there's nothing left to do.
if (order == null)
{
return candidates;
}
// Since we have a constraint to process, bisect the set of actions into those with and without a
// constraint for the 'current order'.
var actionsWithConstraint = new List<ActionSelectorCandidate>();
var actionsWithoutConstraint = new List<ActionSelectorCandidate>();
var constraintContext = new ActionConstraintContext();
constraintContext.Candidates = candidates;
constraintContext.RouteContext = context;
foreach (var candidate in candidates)
{
var isMatch = true;
var foundMatchingConstraint = false;
if (candidate.Constraints != null)
{
constraintContext.CurrentCandidate = candidate;
foreach (var constraint in candidate.Constraints)
{
if (constraint.Order == order)
{
foundMatchingConstraint = true;
if (!constraint.Accept(constraintContext))
{
isMatch = false;
break;
}
}
}
}
if (isMatch && foundMatchingConstraint)
{
actionsWithConstraint.Add(candidate);
}
else if (isMatch)
{
actionsWithoutConstraint.Add(candidate);
}
}
// If we have matches with constraints, those are 'better' so try to keep processing those
if (actionsWithConstraint.Count > 0)
{
var matches = EvaluateActionConstraints(context, actionsWithConstraint, order);
if (matches?.Count > 0)
{
return matches;
}
}
// If the set of matches with constraints can't work, then process the set without constraints.
if (actionsWithoutConstraint.Count == 0)
{
return null;
}
else
{
return EvaluateActionConstraints(context, actionsWithoutConstraint, order);
}
}
// This method attempts to ensure that the route that's about to generate a link will generate a link
@ -184,5 +255,24 @@ namespace Microsoft.AspNet.Mvc
return descriptors.Items;
}
private IReadOnlyList<IActionConstraint> GetConstraints(ActionDescriptor action)
{
if (action.ActionConstraints == null || action.ActionConstraints.Count == 0)
{
return null;
}
var items = action.ActionConstraints.Select(c => new ActionConstraintItem(c)).ToList();
var context = new ActionConstraintProviderContext(action, items);
_actionConstraintProvider.Invoke(context);
return
context.Results
.Where(item => item.Constraint != null)
.Select(item => item.Constraint)
.ToList();
}
}
}

View File

@ -119,9 +119,9 @@ namespace Microsoft.AspNet.Mvc.Description
private IEnumerable<string> GetHttpMethods(ReflectedActionDescriptor action)
{
if (action.MethodConstraints != null && action.MethodConstraints.Count > 0)
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
{
return action.MethodConstraints.SelectMany(c => c.HttpMethods);
return action.ActionConstraints.OfType<HttpMethodConstraint>().SelectMany(c => c.HttpMethods);
}
else
{

View File

@ -5,22 +5,18 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc
{
public class HttpMethodConstraint : IActionConstraint
{
public static readonly int HttpMethodConstraintOrder = 100;
private readonly IReadOnlyList<string> _methods;
// Empty collection means any method will be accepted.
public HttpMethodConstraint(IEnumerable<string> httpMethods)
public HttpMethodConstraint([NotNull] IEnumerable<string> httpMethods)
{
if (httpMethods == null)
{
throw new ArgumentNullException("httpMethods");
}
var methods = new List<string>();
foreach (var method in httpMethods)
@ -44,19 +40,19 @@ namespace Microsoft.AspNet.Mvc
}
}
public bool Accept([NotNull] RouteContext context)
public int Order
{
if (context == null)
{
throw new ArgumentNullException("context");
}
get { return HttpMethodConstraintOrder; }
}
public bool Accept([NotNull] ActionConstraintContext context)
{
if (_methods.Count == 0)
{
return true;
}
var request = context.HttpContext.Request;
var request = context.RouteContext.HttpContext.Request;
return (HttpMethods.Any(m => m.Equals(request.Method, StringComparison.Ordinal)));
}

View File

@ -1,12 +0,0 @@
// 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 Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc
{
public interface IActionConstraint
{
bool Accept([NotNull] RouteContext context);
}
}

View File

@ -28,17 +28,13 @@ namespace Microsoft.AspNet.Mvc.Logging
public IReadOnlyList<ActionDescriptor> ActionsMatchingRouteConstraints { get; set; }
/// <summary>
/// The list of actions that matched all their route and method constraints, if any.
/// The list of actions that matched all their route constraints, and action constraints, if any.
/// </summary>
public IReadOnlyList<ActionDescriptor> ActionsMatchingRouteAndMethodConstraints { get; set; }
public IReadOnlyList<ActionDescriptor> ActionsMatchingActionConstraints { get; set; }
/// <summary>
/// The list of actions that matched all their route, method, and dynamic constraints, if any.
/// </summary>
public IReadOnlyList<ActionDescriptor> ActionsMatchingRouteAndMethodAndDynamicConstraints { get; set; }
/// <summary>
/// The list of actions that are the best matches. These match all constraints.
/// The list of actions that are the best matches. These match all constraints and any additional criteria
/// for disambiguation.
/// </summary>
public IReadOnlyList<ActionDescriptor> FinalMatches { get; set; }
@ -59,13 +55,10 @@ namespace Microsoft.AspNet.Mvc.Logging
builder.Append("\tActions matching route constraints: ");
StringBuilderHelpers.Append(builder, ActionsMatchingRouteConstraints, Formatter);
builder.AppendLine();
builder.Append("\tActions matching route and method constraints: ");
StringBuilderHelpers.Append(builder, ActionsMatchingRouteAndMethodConstraints, Formatter);
builder.Append("\tActions matching action constraints: ");
StringBuilderHelpers.Append(builder, ActionsMatchingActionConstraints, Formatter);
builder.AppendLine();
builder.Append("\tActions matching route, method, and dynamic constraints: ");
StringBuilderHelpers.Append(builder, ActionsMatchingRouteAndMethodAndDynamicConstraints, Formatter);
builder.AppendLine();
builder.Append("\tBest Matches: ");
builder.Append("\tFinal Matches: ");
StringBuilderHelpers.Append(builder, FinalMatches, Formatter);
builder.AppendLine();
builder.Append("\tSelected action: ");

View File

@ -72,11 +72,7 @@ namespace Microsoft.AspNet.Mvc
foreach (var controllerType in controllerTypes)
{
var controllerModel = new ReflectedControllerModel(controllerType)
{
Application = applicationModel,
};
var controllerModel = CreateControllerModel(applicationModel, controllerType);
applicationModel.Controllers.Add(controllerModel);
foreach (var methodInfo in controllerType.AsType().GetMethods())
@ -89,30 +85,14 @@ namespace Microsoft.AspNet.Mvc
foreach (var actionInfo in actionInfos)
{
var actionModel = new ReflectedActionModel(methodInfo)
{
ActionName = actionInfo.ActionName,
Controller = controllerModel,
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)
{
Action = actionModel,
});
}
var actionModel = CreateActionModel(controllerModel, methodInfo, actionInfo);
controllerModel.Actions.Add(actionModel);
foreach (var parameterInfo in methodInfo.GetParameters())
{
var parameterModel = CreateParameterModel(actionModel, parameterInfo);
actionModel.Parameters.Add(parameterModel);
}
}
}
}
@ -120,6 +100,109 @@ namespace Microsoft.AspNet.Mvc
return applicationModel;
}
private ReflectedControllerModel CreateControllerModel(
ReflectedApplicationModel applicationModel,
TypeInfo controllerType)
{
var controllerModel = new ReflectedControllerModel(controllerType)
{
Application = applicationModel,
};
controllerModel.ControllerName =
controllerType.Name.EndsWith("Controller", StringComparison.Ordinal) ?
controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length) :
controllerType.Name;
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToList() is List<object>
var attributes = controllerType.GetCustomAttributes(inherit: true).ToList();
controllerModel.Attributes.AddRange(attributes);
controllerModel.ActionConstraints.AddRange(attributes.OfType<IActionConstraintMetadata>());
controllerModel.Filters.AddRange(attributes.OfType<IFilter>());
controllerModel.RouteConstraints.AddRange(attributes.OfType<RouteConstraintAttribute>());
controllerModel.AttributeRoutes.AddRange(
attributes.OfType<IRouteTemplateProvider>().Select(rtp => new ReflectedAttributeRouteModel(rtp)));
var apiVisibility = attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
if (apiVisibility != null)
{
controllerModel.ApiExplorerIsVisible = !apiVisibility.IgnoreApi;
}
var apiGroupName = attributes.OfType<IApiDescriptionGroupNameProvider>().FirstOrDefault();
if (apiGroupName != null)
{
controllerModel.ApiExplorerGroupName = apiGroupName.GroupName;
}
return controllerModel;
}
private ReflectedActionModel CreateActionModel(
ReflectedControllerModel controllerModel,
MethodInfo methodInfo,
ActionInfo actionInfo)
{
var actionModel = new ReflectedActionModel(methodInfo)
{
ActionName = actionInfo.ActionName,
Controller = controllerModel,
IsActionNameMatchRequired = actionInfo.RequireActionNameMatch,
};
var attributes = actionInfo.Attributes;
actionModel.Attributes.AddRange(attributes);
actionModel.ActionConstraints.AddRange(attributes.OfType<IActionConstraintMetadata>());
actionModel.Filters.AddRange(attributes.OfType<IFilter>());
var apiVisibility = attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
if (apiVisibility != null)
{
actionModel.ApiExplorerIsVisible = !apiVisibility.IgnoreApi;
}
var apiGroupName = attributes.OfType<IApiDescriptionGroupNameProvider>().FirstOrDefault();
if (apiGroupName != null)
{
actionModel.ApiExplorerGroupName = apiGroupName.GroupName;
}
actionModel.HttpMethods.AddRange(actionInfo.HttpMethods ?? Enumerable.Empty<string>());
if (actionInfo.AttributeRoute != null)
{
actionModel.AttributeRouteModel = new ReflectedAttributeRouteModel(
actionInfo.AttributeRoute);
}
return actionModel;
}
private ReflectedParameterModel CreateParameterModel(
ReflectedActionModel actionModel,
ParameterInfo parameterInfo)
{
var parameterModel = new ReflectedParameterModel(parameterInfo)
{
Action = actionModel,
};
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToList() is List<object>
var attributes = parameterInfo.GetCustomAttributes(inherit: true).OfType<object>().ToList();
parameterModel.Attributes.AddRange(attributes);
parameterModel.ParameterName = parameterInfo.Name;
parameterModel.IsOptional = parameterInfo.HasDefaultValue;
return parameterModel;
}
public void ApplyConventions(ReflectedApplicationModel model)
{
// Conventions are applied from the outside-in to allow for scenarios where an action overrides
@ -176,7 +259,7 @@ namespace Microsoft.AspNet.Mvc
}
}
public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel application)
{
var actions = new List<ReflectedActionDescriptor>();
@ -188,7 +271,7 @@ namespace Microsoft.AspNet.Mvc
var routeTemplateErrors = new List<string>();
var attributeRoutingConfigurationErrors = new Dictionary<MethodInfo, string>();
foreach (var controller in model.Controllers)
foreach (var controller in application.Controllers)
{
var controllerDescriptor = new ControllerDescriptor()
{
@ -203,13 +286,14 @@ namespace Microsoft.AspNet.Mvc
// 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);
var actionDescriptors = CreateActionDescriptors(application, controller, action);
foreach (var actionDescriptor in actionDescriptors)
{
actionDescriptor.ControllerDescriptor = controllerDescriptor;
AddApiExplorerInfo(actionDescriptor, action, controller);
AddActionFilters(actionDescriptor, action.Filters, controller.Filters, model.Filters);
AddActionConstraints(actionDescriptor, action, controller);
AddRouteConstraints(actionDescriptor, controller, action);
AddControllerRouteConstraints(
actionDescriptor,
controller.RouteConstraints,
@ -324,37 +408,71 @@ namespace Microsoft.AspNet.Mvc
}
private static IList<ReflectedActionDescriptor> CreateActionDescriptors(
ReflectedActionModel action,
ReflectedApplicationModel application,
ReflectedControllerModel controller,
ControllerDescriptor controllerDescriptor)
ReflectedActionModel action)
{
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))
if (action.AttributeRouteModel != null &&
action.AttributeRouteModel.IsAbsoluteTemplate)
{
// We're overriding the attribute routes on the controller, so filter out any metadata
// from controller level routes.
var actionDescriptor = CreateActionDescriptor(
action,
controllerAttributeRoute: null);
actionDescriptors.Add(actionDescriptor);
// If we're using an attribute route on the controller, then filter out any additional
// metadata from the 'other' attribute routes.
var controllerFilters = controller.Filters
.Where(c => !(c is IRouteTemplateProvider));
AddActionFilters(actionDescriptor, action.Filters, controllerFilters, application.Filters);
var controllerConstraints = controller.ActionConstraints
.Where(c => !(c is IRouteTemplateProvider));
AddActionConstraints(actionDescriptor, action, controllerConstraints);
}
else if (controller.AttributeRoutes != null &&
controller.AttributeRoutes.Count > 0)
{
// We're using the attribute routes from the controller
foreach (var controllerAttributeRoute in controller.AttributeRoutes)
{
var actionDescriptor = CreateActionDescriptor(
action,
controllerAttributeRoute,
controllerDescriptor);
controllerAttributeRoute);
actionDescriptors.Add(actionDescriptor);
// If we're using an attribute route on the controller, then filter out any additional
// metadata from the 'other' attribute routes.
var controllerFilters = controller.Filters
.Where(c => c == controllerAttributeRoute?.Attribute || !(c is IRouteTemplateProvider));
AddActionFilters(actionDescriptor, action.Filters, controllerFilters, application.Filters);
var controllerConstraints = controller.ActionConstraints
.Where(c => c == controllerAttributeRoute?.Attribute || !(c is IRouteTemplateProvider));
AddActionConstraints(actionDescriptor, action, controllerConstraints);
}
}
else
{
actionDescriptors.Add(CreateActionDescriptor(
// No attribute routes on the controller
var actionDescriptor = CreateActionDescriptor(
action,
controllerAttributeRoute: null,
controllerDescriptor: controllerDescriptor));
controllerAttributeRoute: null);
actionDescriptors.Add(actionDescriptor);
// If there's no attribute route on the controller, then we can use all of the filters/constraints
// on the controller.
AddActionFilters(actionDescriptor, action.Filters, controller.Filters, application.Filters);
AddActionConstraints(actionDescriptor, action, controller.ActionConstraints);
}
return actionDescriptors;
@ -362,32 +480,13 @@ namespace Microsoft.AspNet.Mvc
private static ReflectedActionDescriptor CreateActionDescriptor(
ReflectedActionModel action,
ReflectedAttributeRouteModel controllerAttributeRoute,
ControllerDescriptor controllerDescriptor)
ReflectedAttributeRouteModel controllerAttributeRoute)
{
var parameterDescriptors = new List<ParameterDescriptor>();
foreach (var parameter in action.Parameters)
{
var isFromBody = parameter.Attributes.OfType<FromBodyAttribute>().Any();
var paramDescriptor = new ParameterDescriptor()
{
Name = parameter.ParameterName,
IsOptional = parameter.IsOptional
};
if (isFromBody)
{
paramDescriptor.BodyParameterInfo = new BodyParameterInfo(
parameter.ParameterInfo.ParameterType);
}
else
{
paramDescriptor.ParameterBindingInfo = new ParameterBindingInfo(
parameter.ParameterName,
parameter.ParameterInfo.ParameterType);
}
parameterDescriptors.Add(paramDescriptor);
var parameterDescriptor = CreateParameterDescriptor(parameter);
parameterDescriptors.Add(parameterDescriptor);
}
var attributeRouteInfo = CreateAttributeRouteInfo(
@ -397,7 +496,6 @@ namespace Microsoft.AspNet.Mvc
var actionDescriptor = new ReflectedActionDescriptor()
{
Name = action.ActionName,
ControllerDescriptor = controllerDescriptor,
MethodInfo = action.ActionMethod,
Parameters = parameterDescriptors,
RouteConstraints = new List<RouteDataActionConstraint>(),
@ -412,6 +510,30 @@ namespace Microsoft.AspNet.Mvc
return actionDescriptor;
}
private static ParameterDescriptor CreateParameterDescriptor(ReflectedParameterModel parameter)
{
var parameterDescriptor = new ParameterDescriptor()
{
Name = parameter.ParameterName,
IsOptional = parameter.IsOptional
};
var isFromBody = parameter.Attributes.OfType<FromBodyAttribute>().Any();
if (isFromBody)
{
parameterDescriptor.BodyParameterInfo = new BodyParameterInfo(
parameter.ParameterInfo.ParameterType);
}
else
{
parameterDescriptor.ParameterBindingInfo = new ParameterBindingInfo(
parameter.ParameterName,
parameter.ParameterInfo.ParameterType);
}
return parameterDescriptor;
}
private static void AddApiExplorerInfo(
ReflectedActionDescriptor actionDescriptor,
ReflectedActionModel action,
@ -469,17 +591,37 @@ namespace Microsoft.AspNet.Mvc
private static void AddActionConstraints(
ReflectedActionDescriptor actionDescriptor,
ReflectedActionModel action,
ReflectedControllerModel controller)
IEnumerable<IActionConstraintMetadata> controllerConstraints)
{
var constraints = new List<IActionConstraintMetadata>();
var httpMethods = action.HttpMethods;
if (httpMethods != null && httpMethods.Count > 0)
{
actionDescriptor.MethodConstraints = new List<HttpMethodConstraint>()
{
new HttpMethodConstraint(httpMethods)
};
constraints.Add(new HttpMethodConstraint(httpMethods));
}
if (action.ActionConstraints != null)
{
constraints.AddRange(action.ActionConstraints);
}
if (controllerConstraints != null)
{
constraints.AddRange(controllerConstraints);
}
if (constraints.Count > 0)
{
actionDescriptor.ActionConstraints = constraints;
}
}
public void AddRouteConstraints(
ReflectedActionDescriptor actionDescriptor,
ReflectedControllerModel controller,
ReflectedActionModel action)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"controller",
controller.ControllerName));

View File

@ -15,36 +15,15 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
ActionMethod = actionMethod;
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToList() is List<object>
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);
}
var apiExplorerNameAttribute = Attributes.OfType<IApiDescriptionGroupNameProvider>().FirstOrDefault();
if (apiExplorerNameAttribute != null)
{
ApiExplorerGroupName = apiExplorerNameAttribute.GroupName;
}
var apiExplorerVisibilityAttribute = Attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
if (apiExplorerVisibilityAttribute != null)
{
ApiExplorerIsVisible = !apiExplorerVisibilityAttribute.IgnoreApi;
}
Attributes = new List<object>();
ActionConstraints = new List<IActionConstraintMetadata>();
Filters = new List<IFilter>();
HttpMethods = new List<string>();
Parameters = new List<ReflectedParameterModel>();
}
public List<IActionConstraintMetadata> ActionConstraints { get; private set; }
public MethodInfo ActionMethod { get; private set; }
public string ActionName { get; set; }

View File

@ -19,11 +19,14 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
public ReflectedAttributeRouteModel([NotNull] IRouteTemplateProvider templateProvider)
{
Attribute = templateProvider;
Template = templateProvider.Template;
Order = templateProvider.Order;
Name = templateProvider.Name;
}
public IRouteTemplateProvider Attribute { get; private set; }
public string Template { get; set; }
public int? Order { get; set; }

View File

@ -3,10 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
@ -17,38 +14,15 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
ControllerType = controllerType;
Actions = new List<ReflectedActionModel>();
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToList() is List<object>
Attributes = ControllerType.GetCustomAttributes(inherit: true).OfType<object>().ToList();
Filters = Attributes
.OfType<IFilter>()
.ToList();
RouteConstraints = Attributes.OfType<RouteConstraintAttribute>().ToList();
AttributeRoutes = Attributes.OfType<IRouteTemplateProvider>()
.Select(rtp => new ReflectedAttributeRouteModel(rtp))
.ToList();
var apiExplorerNameAttribute = Attributes.OfType<IApiDescriptionGroupNameProvider>().FirstOrDefault();
if (apiExplorerNameAttribute != null)
{
ApiExplorerGroupName = apiExplorerNameAttribute.GroupName;
}
var apiExplorerVisibilityAttribute = Attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
if (apiExplorerVisibilityAttribute != null)
{
ApiExplorerIsVisible = !apiExplorerVisibilityAttribute.IgnoreApi;
}
ControllerName = controllerType.Name.EndsWith("Controller", StringComparison.Ordinal)
? controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length)
: controllerType.Name;
Attributes = new List<object>();
AttributeRoutes = new List<ReflectedAttributeRouteModel>();
ActionConstraints = new List<IActionConstraintMetadata>();
Filters = new List<IFilter>();
RouteConstraints = new List<RouteConstraintAttribute>();
}
public List<IActionConstraintMetadata> ActionConstraints { get; private set; }
public List<ReflectedActionModel> Actions { get; private set; }
public ReflectedApplicationModel Application { get; set; }
@ -66,14 +40,14 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
public List<ReflectedAttributeRouteModel> AttributeRoutes { get; private set; }
/// <summary>
/// If <c>true</c>, <see cref="ApiDescription"/> objects will be created for actions defined by this
/// controller.
/// If <c>true</c>, <see cref="Description.ApiDescription"/> objects will be created for actions defined by
/// this controller.
/// </summary>
public bool? ApiExplorerIsVisible { get; set; }
/// <summary>
/// The value for <see cref="ApiDescription.GroupName"/> of <see cref="ApiDescription"/> objects created
/// for actions defined by this controller.
/// The value for <see cref="Description.ApiDescription.GroupName"/> of
/// <see cref="Description.ApiDescription"/> objects created for actions defined by this controller.
/// </summary>
public string ApiExplorerGroupName { get; set; }
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
@ -13,12 +12,7 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
ParameterInfo = parameterInfo;
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToList() is List<object>
Attributes = parameterInfo.GetCustomAttributes(inherit: true).OfType<object>().ToList();
ParameterName = parameterInfo.Name;
IsOptional = ParameterInfo.HasDefaultValue;
Attributes = new List<object>();
}
public ReflectedActionModel Action { get; set; }

View File

@ -11,7 +11,7 @@ using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc
{
public class RouteDataActionConstraint : IActionConstraint
public class RouteDataActionConstraint
{
private IEqualityComparer _comparer;

View File

@ -39,6 +39,11 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Singleton<IActionSelectorDecisionTreeProvider, ActionSelectorDecisionTreeProvider>();
yield return describe.Scoped<IActionSelector, DefaultActionSelector>();
// This provider needs access to the per-request services, but might be used many times for a given
// request.
yield return describe.Scoped<INestedProvider<ActionConstraintProviderContext>,
DefaultActionConstraintProvider>();
yield return describe.Transient<IActionInvokerFactory, ActionInvokerFactory>();
yield return describe.Transient<IControllerAssemblyProvider, DefaultControllerAssemblyProvider>();
yield return describe.Transient<IActionDiscoveryConventions, DefaultActionDiscoveryConventions>();

View File

@ -178,9 +178,16 @@ namespace Microsoft.AspNet.Mvc
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
var actionConstraintProvider = new NestedProviderManager<ActionConstraintProviderContext>(
new INestedProvider<ActionConstraintProviderContext>[]
{
new DefaultActionConstraintProvider(serviceContainer),
});
var defaultActionSelector = new DefaultActionSelector(
actionCollectionDescriptorProvider,
decisionTreeProvider,
actionConstraintProvider,
NullLoggerFactory.Instance);
return await defaultActionSelector.SelectAsync(context);

View File

@ -82,9 +82,16 @@ namespace Microsoft.AspNet.Mvc
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
var actionConstraintProvider = new NestedProviderManager<ActionConstraintProviderContext>(
new INestedProvider<ActionConstraintProviderContext>[]
{
new DefaultActionConstraintProvider(serviceContainer),
});
var defaultActionSelector = new DefaultActionSelector(
actionCollectionDescriptorProvider,
decisionTreeProvider,
actionConstraintProvider,
NullLoggerFactory.Instance);
return await defaultActionSelector.SelectAsync(context);

View File

@ -166,6 +166,7 @@ namespace Microsoft.AspNet.Mvc
Assert.True(action.RequireActionNameMatch);
Assert.Null(action.HttpMethods);
Assert.Null(action.AttributeRoute);
Assert.Empty(action.Attributes);
}
[Fact]
@ -185,6 +186,7 @@ namespace Microsoft.AspNet.Mvc
Assert.True(action.RequireActionNameMatch);
Assert.Equal(new[] { "PUT", "PATCH" }, action.HttpMethods);
Assert.Null(action.AttributeRoute);
Assert.IsType<CustomHttpMethodsAttribute>(Assert.Single(action.Attributes));
}
[Fact]
@ -206,6 +208,8 @@ namespace Microsoft.AspNet.Mvc
var httpMethod = Assert.Single(action.HttpMethods);
Assert.Equal("DELETE", httpMethod);
Assert.Null(action.AttributeRoute);
Assert.IsType<HttpDeleteAttribute>(Assert.Single(action.Attributes));
}
[Fact]
@ -225,6 +229,10 @@ namespace Microsoft.AspNet.Mvc
Assert.True(action.RequireActionNameMatch);
Assert.Equal(new[] { "GET", "POST" }, action.HttpMethods.OrderBy(m => m, StringComparer.Ordinal));
Assert.Null(action.AttributeRoute);
Assert.Equal(2, action.Attributes.Length);
Assert.Single(action.Attributes, a => a is HttpGetAttribute);
Assert.Single(action.Attributes, a => a is HttpPostAttribute);
}
[Fact]
@ -244,6 +252,11 @@ namespace Microsoft.AspNet.Mvc
Assert.True(action.RequireActionNameMatch);
Assert.Equal(new[] { "GET", "POST", "PUT" }, action.HttpMethods.OrderBy(m => m, StringComparer.Ordinal));
Assert.Null(action.AttributeRoute);
Assert.Equal(3, action.Attributes.Length);
Assert.Single(action.Attributes, a => a is HttpPutAttribute);
Assert.Single(action.Attributes, a => a is HttpGetAttribute);
Assert.Single(action.Attributes, a => a is AcceptVerbsAttribute);
}
[Fact]
@ -268,6 +281,8 @@ namespace Microsoft.AspNet.Mvc
Assert.NotNull(action.AttributeRoute);
Assert.Equal("Change", action.AttributeRoute.Template);
Assert.IsType<HttpPostAttribute>(Assert.Single(action.Attributes));
}
[Fact]
@ -291,6 +306,8 @@ namespace Microsoft.AspNet.Mvc
Assert.NotNull(action.AttributeRoute);
Assert.Equal("Update", action.AttributeRoute.Template);
Assert.IsType<RouteAttribute>(Assert.Single(action.Attributes));
}
[Fact]
@ -314,6 +331,8 @@ namespace Microsoft.AspNet.Mvc
Assert.NotNull(action.AttributeRoute);
Assert.Equal("ListAll", action.AttributeRoute.Template);
Assert.IsType<AcceptVerbsAttribute>(Assert.Single(action.Attributes));
}
[Fact]
@ -341,10 +360,12 @@ namespace Microsoft.AspNet.Mvc
var list = Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("List"));
var listMethod = Assert.Single(list.HttpMethods);
Assert.Equal("POST", listMethod);
Assert.IsType<HttpPostAttribute>(Assert.Single(list.Attributes));
var all = Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("All"));
var allMethod = Assert.Single(all.HttpMethods);
Assert.Equal("GET", allMethod);
Assert.IsType<HttpGetAttribute>(Assert.Single(all.Attributes));
}
[Fact]
@ -367,6 +388,8 @@ namespace Microsoft.AspNet.Mvc
Assert.Null(action.HttpMethods);
Assert.Null(action.AttributeRoute);
Assert.Empty(action.Attributes);
}
[Theory]
@ -390,6 +413,8 @@ namespace Microsoft.AspNet.Mvc
Assert.Null(action.HttpMethods);
Assert.Null(action.AttributeRoute);
Assert.Empty(action.Attributes);
}
[Theory]
@ -416,6 +441,8 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal("GET", httpMethod);
Assert.NotNull(action.AttributeRoute);
Assert.IsType<HttpGetAttribute>(Assert.Single(action.Attributes));
}
Assert.Single(actionInfos, ai => ai.AttributeRoute.Template.Equals("List"));
@ -745,22 +772,22 @@ namespace Microsoft.AspNet.Mvc.DefaultActionDiscoveryConventionsControllers
[HttpPut]
[AcceptVerbs("GET", "POST")]
public void List() { }
}
// Keep it private and nested to avoid polluting the namespace.
private class CustomHttpMethodsAttribute : Attribute, IActionHttpMethodProvider
public class CustomHttpMethodsAttribute : Attribute, IActionHttpMethodProvider
{
private readonly string[] _methods;
public CustomHttpMethodsAttribute(params string[] methods)
{
private readonly string[] _methods;
_methods = methods;
}
public CustomHttpMethodsAttribute(params string[] methods)
public IEnumerable<string> HttpMethods
{
get
{
_methods = methods;
}
public IEnumerable<string> HttpMethods
{
get
{
return _methods;
}
return _methods;
}
}
}

View File

@ -3,12 +3,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Mvc.Logging;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.NestedProviders;
using Microsoft.Framework.Logging;
using Moq;
using Xunit;
@ -52,8 +55,7 @@ namespace Microsoft.AspNet.Mvc
var values = Assert.IsType<DefaultActionSelectorSelectAsyncValues>(write.State);
Assert.Equal("DefaultActionSelector.SelectAsync", values.Name);
Assert.Empty(values.ActionsMatchingRouteConstraints);
Assert.Empty(values.ActionsMatchingRouteAndMethodConstraints);
Assert.Empty(values.ActionsMatchingRouteAndMethodAndDynamicConstraints);
Assert.Empty(values.ActionsMatchingActionConstraints);
Assert.Empty(values.FinalMatches);
Assert.Null(values.SelectedAction);
}
@ -67,7 +69,7 @@ namespace Microsoft.AspNet.Mvc
var matched = new ActionDescriptor()
{
MethodConstraints = new List<HttpMethodConstraint>()
ActionConstraints = new List<IActionConstraintMetadata>()
{
new HttpMethodConstraint(new string[] { "POST" }),
},
@ -107,7 +109,7 @@ namespace Microsoft.AspNet.Mvc
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>(new[] { matched }, values.ActionsMatchingActionConstraints);
Assert.Equal(matched, Assert.Single(values.FinalMatches));
Assert.Equal(matched, values.SelectedAction);
}
@ -155,7 +157,7 @@ namespace Microsoft.AspNet.Mvc
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.ActionsMatchingActionConstraints);
Assert.Equal<ActionDescriptor>(actions, values.FinalMatches);
Assert.Null(values.SelectedAction);
}
@ -200,7 +202,7 @@ namespace Microsoft.AspNet.Mvc
// Arrange
var actionWithConstraints = new ActionDescriptor()
{
MethodConstraints = new List<HttpMethodConstraint>()
ActionConstraints = new List<IActionConstraintMetadata>()
{
new HttpMethodConstraint(new string[] { "POST" }),
},
@ -224,6 +226,268 @@ namespace Microsoft.AspNet.Mvc
Assert.Same(action, actionWithConstraints);
}
[Fact]
public async Task SelectAsync_ConstraintsRejectAll()
{
// Arrange
var action1 = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = false, },
},
};
var action2 = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = false, },
},
};
var actions = new ActionDescriptor[] { action1, action2 };
var selector = CreateSelector(actions);
var context = CreateRouteContext("POST");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Null(action);
}
[Fact]
public async Task SelectAsync_ConstraintsRejectAll_DifferentStages()
{
// Arrange
var action1 = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = false, Order = 0 },
new BooleanConstraint() { Pass = true, Order = 1 },
},
};
var action2 = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = true, Order = 0 },
new BooleanConstraint() { Pass = false, Order = 1 },
},
};
var actions = new ActionDescriptor[] { action1, action2 };
var selector = CreateSelector(actions);
var context = CreateRouteContext("POST");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Null(action);
}
[Fact]
public async Task SelectAsync_ActionConstraintFactory()
{
// Arrange
var actionWithConstraints = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new ConstraintFactory()
{
Constraint = new BooleanConstraint() { Pass = true },
},
}
};
var actionWithoutConstraints = new ActionDescriptor()
{
Parameters = new List<ParameterDescriptor>(),
};
var actions = new ActionDescriptor[] { actionWithConstraints, actionWithoutConstraints };
var selector = CreateSelector(actions);
var context = CreateRouteContext("POST");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Same(action, actionWithConstraints);
}
[Fact]
public async Task SelectAsync_ActionConstraintFactory_ReturnsNull()
{
// Arrange
var nullConstraint = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new ConstraintFactory()
{
},
}
};
var actions = new ActionDescriptor[] { nullConstraint };
var selector = CreateSelector(actions);
var context = CreateRouteContext("POST");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Same(action, nullConstraint);
}
// There's a custom constraint provider registered that only understands BooleanConstraintMarker
[Fact]
public async Task SelectAsync_CustomProvider()
{
// Arrange
var actionWithConstraints = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraintMarker() { Pass = true },
}
};
var actionWithoutConstraints = new ActionDescriptor()
{
Parameters = new List<ParameterDescriptor>(),
};
var actions = new ActionDescriptor[] { actionWithConstraints, actionWithoutConstraints, };
var selector = CreateSelector(actions);
var context = CreateRouteContext("POST");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Same(action, actionWithConstraints);
}
// Due to ordering of stages, the first action will be better.
[Fact]
public async Task SelectAsync_ConstraintsInOrder()
{
// Arrange
var best = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = true, Order = 0, },
},
};
var worst = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = true, Order = 1, },
},
};
var actions = new ActionDescriptor[] { best, worst };
var selector = CreateSelector(actions);
var context = CreateRouteContext("POST");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Same(action, best);
}
// Due to ordering of stages, the first action will be better.
[Fact]
public async Task SelectAsync_ConstraintsInOrder_MultipleStages()
{
// Arrange
var best = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = true, Order = 2, },
},
};
var worst = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = true, Order = 3, },
},
};
var actions = new ActionDescriptor[] { best, worst };
var selector = CreateSelector(actions);
var context = CreateRouteContext("POST");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Same(action, best);
}
[Fact]
public async Task SelectAsync_Fallback_ToActionWithoutConstraints()
{
// Arrange
var nomatch1 = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = false, Order = 2, },
},
};
var nomatch2 = new ActionDescriptor()
{
ActionConstraints = new List<IActionConstraintMetadata>()
{
new BooleanConstraint() { Pass = true, Order = 0, },
new BooleanConstraint() { Pass = true, Order = 1, },
new BooleanConstraint() { Pass = false, Order = 3, },
},
};
var best = new ActionDescriptor();
var actions = new ActionDescriptor[] { best, nomatch1, nomatch2 };
var selector = CreateSelector(actions);
var context = CreateRouteContext("POST");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Same(action, best);
}
[Fact]
public async Task SelectAsync_WithCatchAll_PrefersNonCatchAll()
{
@ -389,7 +653,18 @@ namespace Microsoft.AspNet.Mvc
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionProvider.Object);
return new DefaultActionSelector(actionProvider.Object, decisionTreeProvider, loggerFactory);
var actionConstraintProvider = new NestedProviderManager<ActionConstraintProviderContext>(
new INestedProvider<ActionConstraintProviderContext>[]
{
new DefaultActionConstraintProvider(new ServiceContainer()),
new BooleanConstraintProvider(),
});
return new DefaultActionSelector(
actionProvider.Object,
decisionTreeProvider,
actionConstraintProvider,
loggerFactory);
}
private static VirtualPathContext CreateContext(object routeValues)
@ -454,5 +729,52 @@ namespace Microsoft.AspNet.Mvc
return actionDescriptor;
}
private class BooleanConstraint : IActionConstraint
{
public bool Pass { get; set; }
public int Order { get; set; }
public bool Accept([NotNull]ActionConstraintContext context)
{
return Pass;
}
}
private class ConstraintFactory : IActionConstraintFactory
{
public IActionConstraint Constraint { get; set; }
public IActionConstraint CreateInstance(IServiceProvider services)
{
return Constraint;
}
}
private class BooleanConstraintMarker : IActionConstraintMetadata
{
public bool Pass { get; set; }
}
private class BooleanConstraintProvider : INestedProvider<ActionConstraintProviderContext>
{
public int Order { get; set; }
public void Invoke(ActionConstraintProviderContext context, Action callNext)
{
foreach (var item in context.Results)
{
var marker = item.Metadata as BooleanConstraintMarker;
if (marker != null)
{
Assert.Null(item.Constraint);
item.Constraint = new BooleanConstraint() { Pass = marker.Pass };
}
}
callNext();
}
}
}
}

View File

@ -91,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.Description
{
// Arrange
var action = CreateActionDescriptor();
action.MethodConstraints = new List<HttpMethodConstraint>()
action.ActionConstraints = new List<IActionConstraintMetadata>()
{
new HttpMethodConstraint(new string[] { "PUT", "POST" }),
new HttpMethodConstraint(new string[] { "GET" }),

View File

@ -73,8 +73,8 @@ namespace Microsoft.AspNet.Mvc.Test
// Assert
Assert.Equal("OnlyPost", descriptor.Name);
Assert.Single(descriptor.MethodConstraints);
Assert.Equal(new string[] { "POST" }, descriptor.MethodConstraints[0].HttpMethods);
var constraint = Assert.IsType<HttpMethodConstraint>(Assert.Single(descriptor.ActionConstraints));
Assert.Equal(new string[] { "POST" }, constraint.HttpMethods);
}
[Fact]
@ -646,8 +646,8 @@ namespace Microsoft.AspNet.Mvc.Test
{
Assert.Equal("MultiRouteAttributes", action.ControllerName);
Assert.NotNull(action.MethodConstraints);
var methodConstraint = Assert.Single(action.MethodConstraints);
Assert.NotNull(action.ActionConstraints);
var methodConstraint = Assert.IsType<HttpMethodConstraint>(Assert.Single(action.ActionConstraints));
Assert.NotNull(methodConstraint.HttpMethods);
Assert.Equal(new[] { "POST" }, methodConstraint.HttpMethods);
@ -671,8 +671,8 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal("MultiRouteAttributes", action.ControllerName);
Assert.NotNull(action.MethodConstraints);
var methodConstraint = Assert.Single(action.MethodConstraints);
Assert.NotNull(action.ActionConstraints);
var methodConstraint = Assert.IsType<HttpMethodConstraint>(Assert.Single(action.ActionConstraints));
Assert.NotNull(methodConstraint.HttpMethods);
Assert.Equal(new[] { "PUT" }, methodConstraint.HttpMethods);
@ -702,23 +702,25 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.NotNull(action.AttributeRouteInfo.Template);
}
var constrainedActions = actions.Where(a => a.MethodConstraints != null);
var constrainedActions = actions.Where(a => a.ActionConstraints != null);
Assert.Equal(4, constrainedActions.Count());
// Actions generated by AcceptVerbs
var postActions = constrainedActions.Where(a => a.MethodConstraints.Single().HttpMethods.Single() == "POST");
var postActions = constrainedActions.Where(
a => a.ActionConstraints.OfType<HttpMethodConstraint>().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");
var putActions = constrainedActions.Where(
a => a.ActionConstraints.OfType<HttpMethodConstraint>().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);
var unconstrainedActions = actions.Where(a => a.ActionConstraints == 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"));
@ -766,7 +768,10 @@ namespace Microsoft.AspNet.Mvc.Test
{
var action = Assert.Single(
actions,
a => a.MethodConstraints.SelectMany(c => c.HttpMethods).Contains(method));
a => a.ActionConstraints
.OfType<HttpMethodConstraint>()
.SelectMany(c => c.HttpMethods)
.Contains(method));
Assert.NotNull(action.AttributeRouteInfo);
Assert.Equal("Products/list", action.AttributeRouteInfo.Template);
@ -827,7 +832,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.NotNull(action.AttributeRouteInfo);
Assert.Equal("Products/Index", action.AttributeRouteInfo.Template);
Assert.Null(action.MethodConstraints);
Assert.Null(action.ActionConstraints);
}
[Fact]
@ -1180,6 +1185,149 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(4, sequence);
}
[Fact]
public void BuildModel_SplitsConstraintsBasedOnRoute()
{
// Arrange
var provider = GetProvider(typeof(MultipleRouteProviderOnActionController).GetTypeInfo());
// Act
var model = provider.BuildModel();
// Assert
var actions = Assert.Single(model.Controllers).Actions;
Assert.Equal(2, actions.Count());
var action = Assert.Single(actions, a => a.AttributeRouteModel.Template == "R1");
Assert.Equal(2, action.Attributes.Count);
Assert.Single(action.Attributes, a => a is RouteAndConstraintAttribute);
Assert.Single(action.Attributes, a => a is ConstraintAttribute);
Assert.Equal(2, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => a is RouteAndConstraintAttribute);
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
action = Assert.Single(actions, a => a.AttributeRouteModel.Template == "R2");
Assert.Equal(2, action.Attributes.Count);
Assert.Single(action.Attributes, a => a is RouteAndConstraintAttribute);
Assert.Single(action.Attributes, a => a is ConstraintAttribute);
Assert.Equal(2, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => a is RouteAndConstraintAttribute);
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
}
[Fact]
public void GetDescriptors_SplitsConstraintsBasedOnRoute()
{
// Arrange
var provider = GetProvider(typeof(MultipleRouteProviderOnActionController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors();
// Assert
Assert.Equal(2, actions.Count());
var action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "R1");
Assert.Equal(2, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => a is RouteAndConstraintAttribute);
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "R2");
Assert.Equal(2, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => a is RouteAndConstraintAttribute);
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
}
[Fact]
public void GetDescriptors_SplitsConstraintsBasedOnControllerRoute()
{
// Arrange
var actionName = nameof(MultipleRouteProviderOnActionAndControllerController.Edit);
var provider = GetProvider(typeof(MultipleRouteProviderOnActionAndControllerController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors().Where(a => a.Name == actionName);
// Assert
Assert.Equal(2, actions.Count());
var action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C1/A1");
Assert.Equal(3, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C1");
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A1");
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C2/A1");
Assert.Equal(3, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C2");
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A1");
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
}
[Fact]
public void GetDescriptors_SplitsConstraintsBasedOnControllerRoute_MultipleRoutesOnAction()
{
// Arrange
var actionName = nameof(MultipleRouteProviderOnActionAndControllerController.Delete);
var provider = GetProvider(typeof(MultipleRouteProviderOnActionAndControllerController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors().Where(a => a.Name == actionName);
// Assert
Assert.Equal(4, actions.Count());
var action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C1/A3");
Assert.Equal(3, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C1");
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A3");
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C2/A3");
Assert.Equal(3, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C2");
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A3");
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C1/A4");
Assert.Equal(3, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C1");
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A4");
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "C2/A4");
Assert.Equal(3, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "C2");
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "A4");
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
}
// This method overrides the route from the controller, and so doesn't inherit its metadata.
[Fact]
public void GetDescriptors_SplitsConstraintsBasedOnControllerRoute_Override()
{
// Arrange
var actionName = nameof(MultipleRouteProviderOnActionAndControllerController.Create);
var provider = GetProvider(typeof(MultipleRouteProviderOnActionAndControllerController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors().Where(a => a.Name == actionName);
// Assert
Assert.Equal(1, actions.Count());
var action = Assert.Single(actions, a => a.AttributeRouteInfo.Template == "A2");
Assert.Equal(2, action.ActionConstraints.Count);
Assert.Single(action.ActionConstraints, a => (a as RouteAndConstraintAttribute)?.Template == "~/A2");
Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute);
}
private ReflectedActionDescriptorProvider GetProvider(
TypeInfo controllerTypeInfo,
IEnumerable<IFilter> filters = null)
@ -1658,5 +1806,49 @@ namespace Microsoft.AspNet.Mvc.Test
{
public void Create(int productId) { }
}
private class MultipleRouteProviderOnActionController
{
[Constraint]
[RouteAndConstraint("R1")]
[RouteAndConstraint("R2")]
public void Edit() { }
}
[Constraint]
[RouteAndConstraint("C1")]
[RouteAndConstraint("C2")]
private class MultipleRouteProviderOnActionAndControllerController
{
[RouteAndConstraint("A1")]
public void Edit() { }
[RouteAndConstraint("~/A2")]
public void Create() { }
[RouteAndConstraint("A3")]
[RouteAndConstraint("A4")]
public void Delete() { }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
private class RouteAndConstraintAttribute : Attribute, IActionConstraintMetadata, IRouteTemplateProvider
{
public RouteAndConstraintAttribute(string template)
{
Template = template;
}
public string Name { get; set; }
public int? Order { get; set; }
public string Template { get; private set; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
private class ConstraintAttribute : Attribute, IActionConstraintMetadata
{
}
}
}

View File

@ -1,93 +0,0 @@
// 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 Xunit;
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
{
public class ReflectedActionModelTests
{
[Fact]
public void ReflectedActionModel_PopulatesAttributes()
{
// Arrange
var actionMethod = typeof(BlogController).GetMethod("Edit");
// Act
var model = new ReflectedActionModel(actionMethod);
// Assert
Assert.Equal(3, model.Attributes.Count);
Assert.Single(model.Attributes, a => a is MyFilterAttribute);
Assert.Single(model.Attributes, a => a is MyOtherAttribute);
Assert.Single(model.Attributes, a => a is HttpGetAttribute);
}
[Fact]
public void ReflectedActionModel_PopulatesFilters()
{
// Arrange
var actionMethod = typeof(BlogController).GetMethod("Edit");
// Act
var model = new ReflectedActionModel(actionMethod);
// Assert
Assert.Single(model.Filters);
Assert.IsType<MyFilterAttribute>(model.Filters[0]);
}
[Fact]
public void ReflectedActionModel_PopulatesApiExplorerInfo()
{
// Arrange
var actionMethod = typeof(BlogController).GetMethod("Create");
// Act
var model = new ReflectedActionModel(actionMethod);
// Assert
Assert.Equal(false, model.ApiExplorerIsVisible);
Assert.Equal("Blog", model.ApiExplorerGroupName);
}
[Fact]
public void ReflectedActionModel_PopulatesApiExplorerInfo_NoAttribute()
{
// Arrange
var actionMethod = typeof(BlogController).GetMethod("Edit");
// Act
var model = new ReflectedActionModel(actionMethod);
// Assert
Assert.Null(model.ApiExplorerIsVisible);
Assert.Null(model.ApiExplorerGroupName);
}
private class BlogController
{
[MyOther]
[MyFilter]
[HttpGet("Edit")]
public void Edit()
{
}
[ApiExplorerSettings(IgnoreApi = true, GroupName = "Blog")]
public void Create()
{
}
}
private class MyFilterAttribute : Attribute, IFilter
{
}
private class MyOtherAttribute : Attribute
{
}
}
}

View File

@ -1,188 +0,0 @@
// 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.Linq;
using System.Reflection;
using Xunit;
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
{
public class ReflectedControllerModelTests
{
[Fact]
public void ReflectedControllerModel_PopulatesAttributes()
{
// Arrange
var controllerType = typeof(BlogController);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Equal(6, 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 ApiExplorerSettingsAttribute);
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]
public void ReflectedControllerModel_PopulatesFilters()
{
// Arrange
var controllerType = typeof(BlogController);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Single(model.Filters);
Assert.IsType<MyFilterAttribute>(model.Filters[0]);
}
[Fact]
public void ReflectedControllerModel_PopulatesRouteConstraintAttributes()
{
// Arrange
var controllerType = typeof(BlogController);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Single(model.RouteConstraints);
Assert.IsType<MyRouteConstraintAttribute>(model.RouteConstraints[0]);
}
[Fact]
public void ReflectedControllerModel_ComputesControllerName()
{
// Arrange
var controllerType = typeof(BlogController);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Equal("Blog", model.ControllerName);
}
[Fact]
public void ReflectedControllerModel_ComputesControllerName_WithoutSuffix()
{
// Arrange
var controllerType = typeof(Store);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Equal("Store", model.ControllerName);
}
[Fact]
public void ReflectedControllerModel_PopulatesAttributeRouteInfo()
{
// Arrange
var controllerType = typeof(BlogController);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
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"));
}
[Fact]
public void ReflectedControllerModel_PopulatesApiExplorerInfo()
{
// Arrange
var controllerType = typeof(BlogController);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Equal(true, model.ApiExplorerIsVisible);
Assert.Equal("Blog", model.ApiExplorerGroupName);
}
[Fact]
public void ReflectedControllerModel_PopulatesApiExplorerInfo_Inherited()
{
// Arrange
var controllerType = typeof(DerivedController);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Equal(true, model.ApiExplorerIsVisible);
Assert.Equal("API", model.ApiExplorerGroupName);
}
[Fact]
public void ReflectedControllerModel_PopulatesApiExplorerInfo_NoAttribute()
{
// Arrange
var controllerType = typeof(Store);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Null(model.ApiExplorerIsVisible);
Assert.Null(model.ApiExplorerGroupName);
}
[MyOther]
[MyFilter]
[MyRouteConstraint]
[Route("Blog")]
[Route("Microblog")]
[ApiExplorerSettings(GroupName = "Blog")]
private class BlogController
{
}
private class Store
{
}
private class DerivedController : BaseController
{
}
[ApiExplorerSettings(GroupName = "API")]
private class BaseController
{
}
private class MyRouteConstraintAttribute : RouteConstraintAttribute
{
public MyRouteConstraintAttribute()
: base("MyRouteConstraint", "MyRouteConstraint", false)
{
}
}
private class MyFilterAttribute : Attribute, IFilter
{
}
private class MyOtherAttribute : Attribute
{
}
}
}

View File

@ -1,64 +0,0 @@
// 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 Xunit;
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
{
public class ReflectedParameterModelTests
{
[Fact]
public void ReflectedParameterModel_PopulatesAttributes()
{
// Arrange
var parameterInfo = typeof(BlogController).GetMethod("Edit").GetParameters()[0];
// Act
var model = new ReflectedParameterModel(parameterInfo);
// Assert
Assert.Equal(1, model.Attributes.Count);
Assert.Single(model.Attributes, a => a is MyOtherAttribute);
}
[Fact]
public void ReflectedParameterModel_PopulatesParameterName()
{
// Arrange
var parameterInfo = typeof(BlogController).GetMethod("Edit").GetParameters()[0];
// Act
var model = new ReflectedParameterModel(parameterInfo);
// Assert
Assert.Equal("name", model.ParameterName);
}
[Theory]
[InlineData(0, false)]
[InlineData(1, true)]
public void ReflectedParameterModel_PopulatesIsOptional(int parameterIndex, bool expected)
{
// Arrange
var parameterInfo = typeof(BlogController).GetMethod("Edit").GetParameters()[parameterIndex];
// Act
var model = new ReflectedParameterModel(parameterInfo);
// Assert
Assert.Equal(expected, model.IsOptional);
}
private class BlogController
{
public void Edit([MyOther] string name, int age = 17)
{
}
}
private class MyOtherAttribute : Attribute
{
}
}
}