diff --git a/build/dependencies.props b/build/dependencies.props
index f20cec2237..9fc6186a0c 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -48,8 +48,8 @@
2.2.0-preview1-348162.2.0-preview1-348162.2.0-preview1-34816
- 2.2.0-preview1-34816
- 2.2.0-preview1-34816
+ 2.2.0-a-preview1-action-constraints-come-home-16802
+ 2.2.0-a-preview1-action-constraints-come-home-168022.2.0-preview1-348162.2.0-preview1-348162.2.0-preview1-34816
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs
index 7e047b1367..8a6efd7362 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs
@@ -10,8 +10,8 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
-using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.Net.Http.Headers;
using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;
@@ -26,8 +26,7 @@ namespace Microsoft.AspNetCore.Mvc
Attribute,
IResourceFilter,
IConsumesActionConstraint,
- IApiRequestMetadataProvider,
- IConsumesEndpointConstraint
+ IApiRequestMetadataProvider
{
public static readonly int ConsumesActionConstraintOrder = 200;
@@ -58,11 +57,6 @@ namespace Microsoft.AspNetCore.Mvc
///
int IActionConstraint.Order => ConsumesActionConstraintOrder;
- // The value used is a non default value so that it avoids getting mixed with other endpoint constraints
- // with default order.
- ///
- int IEndpointConstraint.Order => ConsumesActionConstraintOrder;
-
///
/// Gets or sets the supported request content types. Used to select an action when there would otherwise be
/// multiple matches.
@@ -192,83 +186,6 @@ namespace Microsoft.AspNetCore.Mvc
return true;
}
- ///
- public bool Accept(EndpointConstraintContext context)
- {
- // If this constraint is not closest to the endpoint, it will be skipped.
- if (!IsApplicable(context.CurrentCandidate.Endpoint))
- {
- // Since the constraint is to be skipped, returning true here
- // will let the current candidate ignore this constraint and will
- // be selected based on other constraints for this endpoint.
- return true;
- }
-
- var requestContentType = context.HttpContext.Request.ContentType;
-
- // If the request content type is null we need to act like pass through.
- // In case there is a single candidate with a constraint it should be selected.
- // If there are multiple endpoints with consumes endpoint constraints this should result in ambiguous exception
- // unless there is another endpoint without a consumes constraint.
- if (requestContentType == null)
- {
- var isEndpointWithoutConsumeConstraintPresent = context.Candidates.Any(
- candidate => candidate.Constraints == null ||
- !candidate.Constraints.Any(constraint => constraint is IConsumesEndpointConstraint));
-
- return !isEndpointWithoutConsumeConstraintPresent;
- }
-
- // Confirm the request's content type is more specific than (a media type this endpoint supports e.g. OK
- // if client sent "text/plain" data and this endpoint supports "text/*".
- if (IsSubsetOfAnyContentType(requestContentType))
- {
- return true;
- }
-
- var firstCandidate = context.Candidates[0];
- if (firstCandidate.Endpoint != context.CurrentCandidate.Endpoint)
- {
- // If the current candidate is not same as the first candidate,
- // we need not probe other candidates to see if they apply.
- // Only the first candidate is allowed to probe other candidates and based on the result select itself.
- return false;
- }
-
- // Run the matching logic for all IConsumesEndpointConstraints we can find, and see what matches.
- // 1). If we have a unique best match, then only that constraint should return true.
- // 2). If we have multiple matches, then all constraints that match will return true
- // , resulting in ambiguity(maybe).
- // 3). If we have no matches, then we choose the first constraint to return true.It will later return a 415
- foreach (var candidate in context.Candidates)
- {
- if (candidate.Endpoint == firstCandidate.Endpoint)
- {
- continue;
- }
-
- var tempContext = new EndpointConstraintContext()
- {
- Candidates = context.Candidates,
- HttpContext = context.HttpContext,
- CurrentCandidate = candidate
- };
-
- if (candidate.Constraints == null || candidate.Constraints.Count == 0 ||
- candidate.Constraints.Any(constraint => constraint is IConsumesEndpointConstraint &&
- constraint.Accept(tempContext)))
- {
- // There is someone later in the chain which can handle the request.
- // end the process here.
- return false;
- }
- }
-
- // There is no one later in the chain that can handle this content type return a false positive so that
- // later we can detect and return a 415.
- return true;
- }
-
private bool IsApplicable(ActionDescriptor actionDescriptor)
{
// If there are multiple IConsumeActionConstraints which are defined at the class and
@@ -280,17 +197,6 @@ namespace Microsoft.AspNetCore.Mvc
filter => filter.Filter is IConsumesActionConstraint).Filter == this;
}
- private bool IsApplicable(Endpoint endpoint)
- {
- // If there are multiple IConsumeActionConstraints which are defined at the class and
- // at the action level, the one closest to the action overrides the others. To ensure this
- // we take advantage of the fact that ConsumesAttribute is both an IActionFilter and an
- // IConsumeActionConstraint. Since filterdescriptor collection is ordered (the last filter is the one
- // closest to the action), we apply this constraint only if there is no IConsumeActionConstraint after this.
- return endpoint.Metadata.Last(
- metadata => metadata is IConsumesEndpointConstraint) == this;
- }
-
private MediaTypeCollection GetContentTypes(string firstArg, string[] args)
{
var completeArgs = new List();
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
index 3d50285d76..477a67ac0a 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
@@ -20,7 +20,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.DependencyInjection
@@ -173,8 +172,10 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton();
// Will be cached by the DefaultActionSelector
- services.TryAddEnumerable(
- ServiceDescriptor.Transient());
+ services.TryAddEnumerable(ServiceDescriptor.Transient());
+
+ // Policies for Endpoints
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
//
// Controller Factory
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs
index 3ba049a494..ff50a463b0 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs
@@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
_actionConstraintProviders = actionConstraintProviders.OrderBy(item => item.Order).ToArray();
}
- private InnerCache CurrentCache
+ internal InnerCache CurrentCache
{
get
{
@@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (current == null || current.Version != actionDescriptors.Version)
{
- current = new InnerCache(actionDescriptors.Version);
+ current = new InnerCache(actionDescriptors);
_currentCache = current;
}
@@ -165,20 +165,49 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return actionConstraints;
}
- private class InnerCache
+ internal class InnerCache
{
- public InnerCache(int version)
+ private readonly ActionDescriptorCollection _actions;
+ private bool? _hasActionConstraints;
+
+ public InnerCache(ActionDescriptorCollection actions)
{
- Version = version;
+ _actions = actions;
}
public ConcurrentDictionary Entries { get; } =
new ConcurrentDictionary();
- public int Version { get; }
+ public int Version => _actions.Version;
+
+ public bool HasActionConstraints
+ {
+ get
+ {
+ // This is a safe race-condition, since it always transitions from null to non-null.
+ // All writers will always get the same result.
+ if (_hasActionConstraints == null)
+ {
+ var found = false;
+ for (var i = 0; i < _actions.Items.Count; i++)
+ {
+ var action = _actions.Items[i];
+ if (action.ActionConstraints?.Count > 0)
+ {
+ found = true;
+ break;
+ }
+ }
+
+ _hasActionConstraints = found;
+ }
+
+ return _hasActionConstraints.Value;
+ }
+ }
}
- private struct CacheEntry
+ internal readonly struct CacheEntry
{
public CacheEntry(IReadOnlyList actionConstraints)
{
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/IConsumesActionConstraint.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/IConsumesActionConstraint.cs
index 2806bd496d..b004cbdcf3 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/IConsumesActionConstraint.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/IConsumesActionConstraint.cs
@@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc.ActionConstraints;
-using Microsoft.AspNetCore.Routing.EndpointConstraints;
namespace Microsoft.AspNetCore.Mvc.Internal
{
@@ -13,12 +12,4 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public interface IConsumesActionConstraint : IActionConstraint
{
}
-
- ///
- /// An constraint that identifies a type which can be used to select an action
- /// based on incoming request.
- ///
- public interface IConsumesEndpointConstraint : IEndpointConstraint
- {
- }
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs
index 649f5febfa..b16b9ed018 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs
@@ -5,14 +5,12 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
-using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Metadata;
using Microsoft.AspNetCore.Routing.Patterns;
@@ -338,17 +336,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
{
- // REVIEW: What is the best way to pick up endpoint constraints of an ActionDescriptor?
- // Currently they need to implement IActionConstraintMetadata
+ // We explicitly convert a few types of action constraints into MatcherPolicy+Metadata
+ // to better integrate with the DFA matcher.
+ //
+ // Other IActionConstraint data will trigger a back-compat path that can execute
+ // action constraints.
foreach (var actionConstraint in action.ActionConstraints)
{
- if (actionConstraint is IEndpointConstraintMetadata)
+ if (actionConstraint is HttpMethodActionConstraint httpMethodActionConstraint &&
+ !metadata.OfType().Any())
+ {
+ metadata.Add(new HttpMethodMetadata(httpMethodActionConstraint.HttpMethods));
+ }
+ else if (!metadata.Contains(actionConstraint))
{
// The constraint might have been added earlier, e.g. it is also a filter descriptor
- if (!metadata.Contains(actionConstraint))
- {
- metadata.Add(actionConstraint);
- }
+ metadata.Add(actionConstraint);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionConstraintMatcherPolicy.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionConstraintMatcherPolicy.cs
new file mode 100644
index 0000000000..60a1abe6b1
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/ActionConstraintMatcherPolicy.cs
@@ -0,0 +1,237 @@
+// Copyright (c) .NET Foundation. 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.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ActionConstraints;
+using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Matchers;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Mvc.Routing
+{
+ // This is a bridge that allows us to execute IActionConstraint instance when
+ // used with Matcher.
+ internal class ActionConstraintMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
+ {
+ private static readonly IReadOnlyList EmptyEndpoints = Array.Empty();
+
+ // We need to be able to run IActionConstraints on Endpoints that aren't associated
+ // with an action. This is a sentinel value we use when the endpoint isn't from MVC.
+ internal static readonly ActionDescriptor NonAction = new ActionDescriptor();
+
+ private readonly ActionConstraintCache _actionConstraintCache;
+
+ public ActionConstraintMatcherPolicy(ActionConstraintCache actionConstraintCache)
+ {
+ _actionConstraintCache = actionConstraintCache;
+ }
+
+ // Run really late.
+ public override int Order => 100000;
+
+ public void Apply(HttpContext httpContext, CandidateSet candidateSet)
+ {
+ // PERF: we can skip over action constraints if there aren't any app-wide.
+ //
+ // Running action constraints (or just checking for them) in a candidate set
+ // is somewhat expensive compared to other routing operations. This should only
+ // happen if user-code adds action constraints.
+ var actions = _actionConstraintCache.CurrentCache;
+ if (actions.HasActionConstraints)
+ {
+ ApplyActionConstraints(httpContext, candidateSet);
+ }
+ }
+
+ private void ApplyActionConstraints(
+ HttpContext httpContext,
+ CandidateSet candidateSet)
+ {
+ var finalMatches = EvaluateActionConstraints(httpContext, candidateSet);
+
+ // We've computed the set of actions that still apply (and their indices)
+ // First, mark everything as invalid, and then mark everything in the matching
+ // set as valid. This is O(2n) vs O(n**2)
+ for (var i = 0; i < candidateSet.Count; i++)
+ {
+ candidateSet[i].IsValidCandidate = false;
+ }
+
+ if (finalMatches != null)
+ {
+ for (var i = 0; i < finalMatches.Count; i++)
+ {
+ candidateSet[finalMatches[i].index].IsValidCandidate = true;
+ }
+ }
+ }
+
+ // This is almost the same as the code in ActionSelector, but we can't really share the logic
+ // because we need to track the index of each candidate - and, each candidate has its own route
+ // values.
+ private IReadOnlyList<(int index, ActionSelectorCandidate candidate)> EvaluateActionConstraints(
+ HttpContext httpContext,
+ CandidateSet candidateSet)
+ {
+ var items = new List<(int index, ActionSelectorCandidate candidate)>();
+
+ // We want to execute a group at a time (based on score) so keep track of the score that we've seen.
+ int? score = null;
+
+ // Perf: Avoid allocations
+ for (var i = 0; i < candidateSet.Count; i++)
+ {
+ ref var candidate = ref candidateSet[i];
+ if (candidate.IsValidCandidate)
+ {
+ if (score != null && score != candidate.Score)
+ {
+ // This is the end of a group.
+ var matches = EvaluateActionConstraintsCore(httpContext, candidateSet, items, startingOrder: null);
+ if (matches?.Count > 0)
+ {
+ return matches;
+ }
+
+ // If we didn't find matches, then reset.
+ items.Clear();
+ }
+
+ score = candidate.Score;
+
+ // If we get here, this is either the first endpoint or the we just (unsuccessfully)
+ // executed constraints for a group.
+ //
+ // So keep adding constraints.
+ var endpoint = candidate.Endpoint;
+ var actionDescriptor = endpoint.Metadata.GetMetadata();
+
+ IReadOnlyList constraints = Array.Empty();
+ if (actionDescriptor != null)
+ {
+ constraints = _actionConstraintCache.GetActionConstraints(httpContext, actionDescriptor);
+ }
+
+ // Capture the index. We need this later to look up the endpoint/route values.
+ items.Add((i, new ActionSelectorCandidate(actionDescriptor ?? NonAction, constraints)));
+ }
+ }
+
+ // Handle residue
+ return EvaluateActionConstraintsCore(httpContext, candidateSet, items, startingOrder: null);
+ }
+
+ private IReadOnlyList<(int index, ActionSelectorCandidate candidate)> EvaluateActionConstraintsCore(
+ HttpContext httpContext,
+ CandidateSet candidateSet,
+ IReadOnlyList<(int index, ActionSelectorCandidate candidate)> items,
+ int? startingOrder)
+ {
+ // Find the next group of constraints to process. This will be the lowest value of
+ // order that is higher than startingOrder.
+ int? order = null;
+
+ // Perf: Avoid allocations
+ for (var i = 0; i < items.Count; i++)
+ {
+ var item = items[i];
+ var constraints = item.candidate.Constraints;
+ if (constraints != null)
+ {
+ for (var j = 0; j < constraints.Count; j++)
+ {
+ var constraint = constraints[j];
+ if ((startingOrder == null || constraint.Order > startingOrder) &&
+ (order == null || constraint.Order < order))
+ {
+ order = constraint.Order;
+ }
+ }
+ }
+ }
+
+ // If we don't find a next then there's nothing left to do.
+ if (order == null)
+ {
+ return items;
+ }
+
+ // Since we have a constraint to process, bisect the set of endpoints into those with and without a
+ // constraint for the current order.
+ var endpointsWithConstraint = new List<(int index, ActionSelectorCandidate candidate)>();
+ var endpointsWithoutConstraint = new List<(int index, ActionSelectorCandidate candidate)>();
+
+ var constraintContext = new ActionConstraintContext();
+ constraintContext.Candidates = items.Select(i => i.candidate).ToArray();
+
+ // Perf: Avoid allocations
+ for (var i = 0; i < items.Count; i++)
+ {
+ var item = items[i];
+ var isMatch = true;
+ var foundMatchingConstraint = false;
+
+ var constraints = item.candidate.Constraints;
+ if (constraints != null)
+ {
+ constraintContext.CurrentCandidate = item.candidate;
+ for (var j = 0; j < constraints.Count; j++)
+ {
+ var constraint = constraints[j];
+ if (constraint.Order == order)
+ {
+ foundMatchingConstraint = true;
+
+ // Before we run the constraint, we need to initialize the route values.
+ // In global routing, the route values are per-endpoint.
+ constraintContext.RouteContext = new RouteContext(httpContext)
+ {
+ RouteData = new RouteData(candidateSet[item.index].Values),
+ };
+ if (!constraint.Accept(constraintContext))
+ {
+ isMatch = false;
+ break;
+ }
+ }
+ }
+ }
+
+ if (isMatch && foundMatchingConstraint)
+ {
+ endpointsWithConstraint.Add(item);
+ }
+ else if (isMatch)
+ {
+ endpointsWithoutConstraint.Add(item);
+ }
+ }
+
+ // If we have matches with constraints, those are better so try to keep processing those
+ if (endpointsWithConstraint.Count > 0)
+ {
+ var matches = EvaluateActionConstraintsCore(httpContext, candidateSet, endpointsWithConstraint, order);
+ if (matches?.Count > 0)
+ {
+ return matches;
+ }
+ }
+
+ // If the set of matches with constraints can't work, then process the set without constraints.
+ if (endpointsWithoutConstraint.Count == 0)
+ {
+ return null;
+ }
+ else
+ {
+ return EvaluateActionConstraintsCore(httpContext, candidateSet, endpointsWithoutConstraint, order);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/IConsumesMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/IConsumesMetadata.cs
new file mode 100644
index 0000000000..83e04931b7
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/IConsumesMetadata.cs
@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. 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.AspNetCore.Mvc.Routing
+{
+ internal interface IConsumesMetadata
+ {
+ IReadOnlyList ContentTypes { get; }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs
index 30514d5ac8..57a0bbddf1 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs
@@ -12,9 +12,6 @@ using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
-using Microsoft.AspNetCore.Routing.EndpointConstraints;
-using Microsoft.AspNetCore.Routing.Matchers;
-using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@@ -296,192 +293,6 @@ namespace Microsoft.AspNetCore.Mvc
Assert.True(constraint2.Accept(context));
}
- private MatcherEndpoint CreateEndpoint(params IEndpointConstraint[] constraints)
- {
- var endpointMetadata = new EndpointMetadataCollection(constraints);
-
- return new MatcherEndpoint(
- (r) => null,
- RoutePatternFactory.Parse("/"),
- new RouteValueDictionary(),
- 0,
- endpointMetadata,
- "");
- }
-
- [Theory]
- [InlineData("application/json")]
- [InlineData("application/json;Parameter1=12")]
- [InlineData("text/xml")]
- public void EndpointConstraint_Accept_MatchesForMachingRequestContentType(string contentType)
- {
- // Arrange
- var constraint = new ConsumesAttribute("application/json", "text/xml");
- var endpoint = CreateEndpoint(constraint);
-
- var context = new EndpointConstraintContext();
- context.Candidates = new List()
- {
- new EndpointSelectorCandidate(endpoint, new [] { constraint }),
- };
-
- context.CurrentCandidate = context.Candidates[0];
- context.HttpContext = CreateHttpContext(contentType: contentType);
-
- // Act & Assert
- Assert.True(constraint.Accept(context));
- }
-
- [Fact]
- public void EndpointConstraint_Accept_TheFirstCandidateReturnsFalse_IfALaterOneMatches()
- {
- // Arrange
- var constraint1 = new ConsumesAttribute("application/json", "text/xml");
- var endpoint1 = CreateEndpoint(constraint1);
-
- var constraint2 = new Mock();
- var endpoint2 = CreateEndpoint(constraint2.Object);
-
- constraint2.Setup(o => o.Accept(It.IsAny()))
- .Returns(true);
-
- var context = new EndpointConstraintContext();
- context.Candidates = new List()
- {
- new EndpointSelectorCandidate(endpoint1, new [] { constraint1 }),
- new EndpointSelectorCandidate(endpoint2, new [] { constraint2.Object }),
- };
-
- context.CurrentCandidate = context.Candidates[0];
- context.HttpContext = CreateHttpContext(contentType: "application/custom");
-
- // Act & Assert
- Assert.False(constraint1.Accept(context));
- }
-
- [Theory]
- [InlineData("application/custom")]
- [InlineData("")]
- [InlineData(null)]
- public void EndpointConstraint_Accept_ForNoMatchingCandidates_SelectsTheFirstCandidate(string contentType)
- {
- // Arrange
- var constraint1 = new ConsumesAttribute("application/json", "text/xml");
- var endpoint1 = CreateEndpoint(constraint1);
-
- var constraint2 = new Mock();
- var endpoint2 = CreateEndpoint(constraint2.Object);
-
- constraint2.Setup(o => o.Accept(It.IsAny()))
- .Returns(false);
-
- var context = new EndpointConstraintContext();
- context.Candidates = new List()
- {
- new EndpointSelectorCandidate(endpoint1, new [] { constraint1 }),
- new EndpointSelectorCandidate(endpoint2, new [] { constraint2.Object }),
- };
-
- context.CurrentCandidate = context.Candidates[0];
- context.HttpContext = CreateHttpContext(contentType: contentType);
-
- // Act & Assert
- Assert.True(constraint1.Accept(context));
- }
-
- [Theory]
- [InlineData("")]
- [InlineData(null)]
- public void EndpointConstraint_Accept_ForNoRequestType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
- {
- // Arrange
- var constraint1 = new ConsumesAttribute("application/json");
- var endpointWithConstraint = CreateEndpoint(constraint1);
-
- var constraint2 = new ConsumesAttribute("text/xml");
- var endpointWithConstraint2 = CreateEndpoint(constraint2);
-
- var endpointWithoutConstraint = CreateEndpoint();
-
- var context = new EndpointConstraintContext();
- context.Candidates = new List()
- {
- new EndpointSelectorCandidate(endpointWithConstraint, new [] { constraint1 }),
- new EndpointSelectorCandidate(endpointWithConstraint2, new [] { constraint2 }),
- new EndpointSelectorCandidate(endpointWithoutConstraint, new List()),
- };
-
- context.HttpContext = CreateHttpContext(contentType: contentType);
-
- // Act & Assert
- context.CurrentCandidate = context.Candidates[0];
- Assert.False(constraint1.Accept(context));
- context.CurrentCandidate = context.Candidates[1];
- Assert.False(constraint2.Accept(context));
- }
-
- [Theory]
- [InlineData("application/xml")]
- [InlineData("application/custom")]
- [InlineData("invalid/invalid")]
- public void EndpointConstraint_Accept_UnrecognizedMediaType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
- {
- // Arrange
- var endpointWithoutConstraint = CreateEndpoint();
- var constraint1 = new ConsumesAttribute("application/json");
- var endpointWithConstraint = CreateEndpoint(constraint1);
-
- var constraint2 = new ConsumesAttribute("text/xml");
- var endpointWithConstraint2 = CreateEndpoint(constraint2);
-
- var context = new EndpointConstraintContext();
- context.Candidates = new List()
- {
- new EndpointSelectorCandidate(endpointWithConstraint, new [] { constraint1 }),
- new EndpointSelectorCandidate(endpointWithConstraint2, new [] { constraint2 }),
- new EndpointSelectorCandidate(endpointWithoutConstraint, new List()),
- };
-
- context.HttpContext = CreateHttpContext(contentType: contentType);
-
- // Act & Assert
- context.CurrentCandidate = context.Candidates[0];
- Assert.False(constraint1.Accept(context));
-
- context.CurrentCandidate = context.Candidates[1];
- Assert.False(constraint2.Accept(context));
- }
-
- [Theory]
- [InlineData("")]
- [InlineData(null)]
- public void EndpointConstraint_Accept_ForNoRequestType_ReturnsTrueForAllConstraints(string contentType)
- {
- // Arrange
- var constraint1 = new ConsumesAttribute("application/json");
- var endpointWithConstraint = CreateEndpoint(constraint1);
-
- var constraint2 = new ConsumesAttribute("text/xml");
- var endpointWithConstraint2 = CreateEndpoint(constraint2);
-
- var endpointWithoutConstraint = CreateEndpoint();
-
- var context = new EndpointConstraintContext();
- context.Candidates = new List()
- {
- new EndpointSelectorCandidate(endpointWithConstraint, new [] { constraint1 }),
- new EndpointSelectorCandidate(endpointWithConstraint2, new [] { constraint2 }),
- };
-
- context.HttpContext = CreateHttpContext(contentType: contentType);
-
- // Act & Assert
- context.CurrentCandidate = context.Candidates[0];
- Assert.True(constraint1.Accept(context));
- context.CurrentCandidate = context.Candidates[1];
- Assert.True(constraint2.Accept(context));
- }
-
[Theory]
[InlineData("application/xml")]
[InlineData("application/custom")]
@@ -620,9 +431,5 @@ namespace Microsoft.AspNetCore.Mvc
public interface ITestActionConsumeConstraint : IConsumesActionConstraint, IResourceFilter
{
}
-
- public interface ITestEndpointConsumeConstraint : IConsumesEndpointConstraint, IResourceFilter
- {
- }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs
index 3e6c558518..9660812ec5 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs
@@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -320,6 +321,13 @@ namespace Microsoft.AspNetCore.Mvc
typeof(MiddlewareFilterBuilderStartupFilter)
}
},
+ {
+ typeof(MatcherPolicy),
+ new Type[]
+ {
+ typeof(ActionConstraintMatcherPolicy),
+ }
+ },
};
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionConstraintMatcherPolicyTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionConstraintMatcherPolicyTest.cs
new file mode 100644
index 0000000000..51a00d6ddd
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ActionConstraintMatcherPolicyTest.cs
@@ -0,0 +1,425 @@
+// Copyright (c) .NET Foundation. 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.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ActionConstraints;
+using Microsoft.AspNetCore.Mvc.ApplicationParts;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Matchers;
+using Microsoft.AspNetCore.Routing.Patterns;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.Routing
+{
+ public class ActionConstraintMatcherPolicyTest
+ {
+ [Fact]
+ public void Apply_CanBeAmbiguous()
+ {
+ // Arrange
+ var actions = new ActionDescriptor[]
+ {
+ new ActionDescriptor() { DisplayName = "A1" },
+ new ActionDescriptor() { DisplayName = "A2" },
+ };
+
+ var candidateSet = CreateCandidateSet(actions);
+
+ var selector = CreateSelector(actions);
+
+ // Act
+ selector.Apply(new DefaultHttpContext(), candidateSet);
+
+ // Assert
+ Assert.True(candidateSet[0].IsValidCandidate);
+ Assert.True(candidateSet[1].IsValidCandidate);
+ }
+
+ [Fact]
+ public void Apply_PrefersActionWithConstraints()
+ {
+ // Arrange
+ var actionWithConstraints = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new HttpMethodActionConstraint(new string[] { "POST" }),
+ },
+ Parameters = new List(),
+ };
+
+ var actionWithoutConstraints = new ActionDescriptor()
+ {
+ Parameters = new List(),
+ };
+
+ var actions = new ActionDescriptor[] { actionWithConstraints, actionWithoutConstraints };
+ var candidateSet = CreateCandidateSet(actions);
+
+ var selector = CreateSelector(actions);
+
+ var httpContext = CreateHttpContext("POST");
+
+ // Act
+ selector.Apply(httpContext, candidateSet);
+
+ // Assert
+ Assert.True(candidateSet[0].IsValidCandidate);
+ Assert.False(candidateSet[1].IsValidCandidate);
+ }
+
+ [Fact]
+ public void Apply_ConstraintsRejectAll()
+ {
+ // Arrange
+ var action1 = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = false, },
+ },
+ };
+
+ var action2 = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = false, },
+ },
+ };
+
+ var actions = new ActionDescriptor[] { action1, action2 };
+ var candidateSet = CreateCandidateSet(actions);
+
+ var selector = CreateSelector(actions);
+
+ var httpContext = CreateHttpContext("POST");
+
+ // Act
+ selector.Apply(httpContext, candidateSet);
+
+ // Assert
+ Assert.False(candidateSet[0].IsValidCandidate);
+ Assert.False(candidateSet[1].IsValidCandidate);
+ }
+
+ [Fact]
+ public void Apply_ConstraintsRejectAll_DifferentStages()
+ {
+ // Arrange
+ var action1 = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = false, Order = 0 },
+ new BooleanConstraint() { Pass = true, Order = 1 },
+ },
+ };
+
+ var action2 = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = true, Order = 0 },
+ new BooleanConstraint() { Pass = false, Order = 1 },
+ },
+ };
+
+ var actions = new ActionDescriptor[] { action1, action2 };
+ var candidateSet = CreateCandidateSet(actions);
+
+ var selector = CreateSelector(actions);
+ var httpContext = CreateHttpContext("POST");
+
+ // Act
+ selector.Apply(httpContext, candidateSet);
+
+ // Assert
+ Assert.False(candidateSet[0].IsValidCandidate);
+ Assert.False(candidateSet[1].IsValidCandidate);
+ }
+
+ // Due to ordering of stages, the first action will be better.
+ [Fact]
+ public void Apply_ConstraintsInOrder()
+ {
+ // Arrange
+ var best = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = true, Order = 0, },
+ },
+ };
+
+ var worst = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = true, Order = 1, },
+ },
+ };
+
+ var actions = new ActionDescriptor[] { best, worst };
+ var candidateSet = CreateCandidateSet(actions);
+
+ var selector = CreateSelector(actions);
+ var httpContext = CreateHttpContext("POST");
+
+ // Act
+ selector.Apply(httpContext, candidateSet);
+
+ // Assert
+ Assert.True(candidateSet[0].IsValidCandidate);
+ Assert.False(candidateSet[1].IsValidCandidate);
+ }
+
+ [Fact]
+ public void Apply_SkipsOverInvalidEndpoints()
+ {
+ // Arrange
+ var best = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = true, Order = 0, },
+ },
+ };
+
+ var another = new ActionDescriptor();
+
+ var worst = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = true, Order = 1, },
+ },
+ };
+
+ var actions = new ActionDescriptor[] { best, another, worst };
+ var candidateSet = CreateCandidateSet(actions);
+ candidateSet[0].IsValidCandidate = false;
+ candidateSet[1].IsValidCandidate = false;
+
+ var selector = CreateSelector(actions);
+ var httpContext = CreateHttpContext("POST");
+
+ // Act
+ selector.Apply(httpContext, candidateSet);
+
+ // Assert
+ Assert.False(candidateSet[0].IsValidCandidate);
+ Assert.False(candidateSet[1].IsValidCandidate);
+ Assert.True(candidateSet[2].IsValidCandidate);
+ }
+
+ [Fact]
+ public void Apply_IncludesNonMvcEndpoints()
+ {
+ // Arrange
+ var action1 = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = false, Order = 0, },
+ },
+ };
+
+ var action2 = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ new BooleanConstraint() { Pass = false, Order = 1, },
+ },
+ };
+
+ var actions = new ActionDescriptor[] { action1, null, action2 };
+ var candidateSet = CreateCandidateSet(actions);
+
+ var selector = CreateSelector(actions);
+ var httpContext = CreateHttpContext("POST");
+
+ // Act
+ selector.Apply(httpContext, candidateSet);
+
+ // Assert
+ Assert.False(candidateSet[0].IsValidCandidate);
+ Assert.True(candidateSet[1].IsValidCandidate);
+ Assert.False(candidateSet[2].IsValidCandidate);
+ }
+
+ // Due to ordering of stages, the first action will be better.
+ [Fact]
+ public void Apply_ConstraintsInOrder_MultipleStages()
+ {
+ // Arrange
+ var best = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ 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()
+ {
+ 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 candidateSet = CreateCandidateSet(actions);
+
+ var selector = CreateSelector(actions);
+
+ var httpContext = CreateHttpContext("POST");
+
+ // Act
+ selector.Apply(httpContext, candidateSet);
+
+ // Assert
+ Assert.True(candidateSet[0].IsValidCandidate);
+ Assert.False(candidateSet[1].IsValidCandidate);
+ }
+
+ [Fact]
+ public void Apply_Fallback_ToActionWithoutConstraints()
+ {
+ // Arrange
+ var nomatch1 = new ActionDescriptor()
+ {
+ ActionConstraints = new List()
+ {
+ 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()
+ {
+ 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 candidateSet = CreateCandidateSet(actions);
+
+ var selector = CreateSelector(actions);
+
+ var httpContext = CreateHttpContext("POST");
+
+ // Act
+ selector.Apply(httpContext, candidateSet);
+
+ // Assert
+ Assert.True(candidateSet[0].IsValidCandidate);
+ Assert.False(candidateSet[1].IsValidCandidate);
+ Assert.False(candidateSet[2].IsValidCandidate);
+ }
+
+ private ActionConstraintMatcherPolicy CreateSelector(ActionDescriptor[] actions)
+ {
+ // We need to actually provide some actions with some action constraints metadata
+ // or else the policy will No-op.
+ var actionDescriptorProvider = new Mock();
+ actionDescriptorProvider
+ .Setup(a => a.OnProvidersExecuted(It.IsAny()))
+ .Callback(c =>
+ {
+ for (var i = 0; i < actions.Length; i++)
+ {
+ c.Results.Add(actions[i]);
+ }
+ });
+
+ var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
+ new IActionDescriptorProvider[] { actionDescriptorProvider.Object, },
+ Enumerable.Empty());
+
+ var cache = new ActionConstraintCache(actionDescriptorCollectionProvider, new[]
+ {
+ new DefaultActionConstraintProvider(),
+ });
+
+ return new ActionConstraintMatcherPolicy(cache);
+ }
+
+ private static HttpContext CreateHttpContext(string httpMethod)
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = httpMethod;
+ return httpContext;
+ }
+
+ private static MatcherEndpoint CreateEndpoint(ActionDescriptor action)
+ {
+ var metadata = new List