Added support for route constraints in Dispatcher world

This commit is contained in:
Kiran Challa 2018-07-13 14:18:54 -07:00
parent 1c7f53ae39
commit 42708bec91
25 changed files with 886 additions and 110 deletions

View File

@ -34,6 +34,7 @@ namespace Benchmarks
template: "/plaintext",
defaults: new RouteValueDictionary(),
requiredValues: new RouteValueDictionary(),
nonInlineMatchProcessorReferences: null,
order: 0,
metadata: EndpointMetadataCollection.Empty,
displayName: "Plaintext"),

View File

@ -34,13 +34,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
}
return new MatcherEndpoint(
(next) => (context) => Task.CompletedTask,
template,
new RouteValueDictionary(),
new RouteValueDictionary(),
0,
new EndpointMetadataCollection(metadata),
template);
(next) => (context) => Task.CompletedTask,
template,
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
EndpointMetadataCollection.Empty,
template);
}
internal static int[] SampleRequests(int endpointCount, int count)

View File

@ -0,0 +1,40 @@
// 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.Globalization;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.Logging;
namespace DispatcherSample.Web
{
internal class EndsWithStringMatchProcessor : MatchProcessorBase
{
private readonly ILogger<EndsWithStringMatchProcessor> _logger;
public EndsWithStringMatchProcessor(ILogger<EndsWithStringMatchProcessor> logger)
{
_logger = logger;
}
public override bool Process(object value)
{
if (value == null)
{
return false;
}
var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
var endsWith = valueString.EndsWith(ConstraintArgument, StringComparison.OrdinalIgnoreCase);
if (!endsWith)
{
_logger.LogDebug(
$"Parameter '{ParameterName}' with value '{valueString}' does not end with '{ConstraintArgument}'.");
}
return endsWith;
}
}
}

View File

@ -2,8 +2,10 @@
// 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.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.DependencyInjection;
@ -17,7 +19,13 @@ namespace DispatcherSample.Web
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddTransient<EndsWithStringMatchProcessor>();
services.AddRouting(options =>
{
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor));
});
services.AddDispatcher(options =>
{
options.DataSources.Add(new DefaultEndpointDataSource(new[]
@ -31,7 +39,13 @@ namespace DispatcherSample.Web
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_homePayload, 0, payloadLength);
},
"/", new RouteValueDictionary(), new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, "Home"),
"/",
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
EndpointMetadataCollection.Empty,
"Home"),
new MatcherEndpoint((next) => (httpContext) =>
{
var response = httpContext.Response;
@ -41,7 +55,41 @@ namespace DispatcherSample.Web
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
},
"/plaintext", new RouteValueDictionary(), new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, "Plaintext"),
"/plaintext",
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
EndpointMetadataCollection.Empty,
"Plaintext"),
new MatcherEndpoint((next) => (httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("WithConstraints");
},
"/withconstraints/{id:endsWith(_001)}",
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
EndpointMetadataCollection.Empty,
"withconstraints"),
new MatcherEndpoint((next) => (httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("withoptionalconstraints");
},
"/withoptionalconstraints/{id:endsWith(_001)?}",
new RouteValueDictionary(),
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
EndpointMetadataCollection.Empty,
"withoptionalconstraints"),
}));
});
}

View File

@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Routing
}
}
private static IRouteConstraint CreateConstraint(Type constraintType, string argumentString)
internal static IRouteConstraint CreateConstraint(Type constraintType, string argumentString)
{
// No arguments - call the default constructor
if (argumentString == null)

View File

@ -13,15 +13,18 @@ using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNetCore.Routing
{
public class DefaultLinkGenerator : ILinkGenerator
internal class DefaultLinkGenerator : ILinkGenerator
{
private readonly MatchProcessorFactory _matchProcessorFactory;
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
private readonly ILogger<DefaultLinkGenerator> _logger;
public DefaultLinkGenerator(
MatchProcessorFactory matchProcessorFactory,
ObjectPool<UriBuildingContext> uriBuildingContextPool,
ILogger<DefaultLinkGenerator> logger)
{
_matchProcessorFactory = matchProcessorFactory;
_uriBuildingContextPool = uriBuildingContextPool;
_logger = logger;
}
@ -61,7 +64,7 @@ namespace Microsoft.AspNetCore.Routing
foreach (var endpoint in matcherEndpoints)
{
link = GetLink(endpoint.ParsedTemplate, endpoint.Defaults, explicitValues, ambientValues);
link = GetLink(endpoint, explicitValues, ambientValues);
if (link != null)
{
return true;
@ -72,27 +75,55 @@ namespace Microsoft.AspNetCore.Routing
}
private string GetLink(
RouteTemplate template,
RouteValueDictionary defaults,
MatcherEndpoint endpoint,
RouteValueDictionary explicitValues,
RouteValueDictionary ambientValues)
{
var templateBinder = new TemplateBinder(
UrlEncoder.Default,
_uriBuildingContextPool,
template,
defaults);
endpoint.ParsedTemplate,
endpoint.Defaults);
var values = templateBinder.GetValues(ambientValues, explicitValues);
if (values == null)
var templateValuesResult = templateBinder.GetValues(ambientValues, explicitValues);
if (templateValuesResult == null)
{
// We're missing one of the required values for this route.
return null;
}
//TODO: route constraint matching here
if (!Match(endpoint, templateValuesResult.CombinedValues))
{
return null;
}
return templateBinder.BindValues(values.AcceptedValues);
return templateBinder.BindValues(templateValuesResult.AcceptedValues);
}
private bool Match(MatcherEndpoint endpoint, RouteValueDictionary routeValues)
{
if (routeValues == null)
{
throw new ArgumentNullException(nameof(routeValues));
}
for (var i = 0; i < endpoint.MatchProcessorReferences.Count; i++)
{
var matchProcessorReference = endpoint.MatchProcessorReferences[i];
var parameter = endpoint.ParsedTemplate.GetParameter(matchProcessorReference.ParameterName);
if (parameter.IsOptional && !routeValues.ContainsKey(parameter.Name))
{
continue;
}
var matchProcessor = _matchProcessorFactory.Create(matchProcessorReference);
if (!matchProcessor.ProcessOutbound(httpContext: null, routeValues))
{
return false;
}
}
return true;
}
}
}

View File

@ -3,7 +3,9 @@
using System;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
@ -28,6 +30,7 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(services));
}
services.TryAddSingleton<MatchProcessorFactory, DefaultMatchProcessorFactory>();
services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>();
services.TryAddSingleton<ObjectPool<UriBuildingContext>>(s =>
{

View File

@ -0,0 +1,26 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNetCore.Routing
{
internal class NullRouter : IRouter
{
public static readonly NullRouter Instance = new NullRouter();
private NullRouter()
{
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return null;
}
public Task RouteAsync(RouteContext context)
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,156 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class DefaultMatchProcessorFactory : MatchProcessorFactory
{
private readonly RouteOptions _options;
private readonly ILogger<DefaultMatchProcessorFactory> _logger;
private readonly IServiceProvider _serviceProvider;
public DefaultMatchProcessorFactory(
IOptions<RouteOptions> options,
ILogger<DefaultMatchProcessorFactory> logger,
IServiceProvider serviceProvider)
{
_options = options.Value;
_logger = logger;
_serviceProvider = serviceProvider;
}
public override MatchProcessor Create(MatchProcessorReference matchProcessorReference)
{
if (matchProcessorReference == null)
{
throw new ArgumentNullException(nameof(matchProcessorReference));
}
if (matchProcessorReference.MatchProcessor != null)
{
return matchProcessorReference.MatchProcessor;
}
// Example:
// {productId:regex(\d+)}
//
// ParameterName: productId
// ConstraintText: regex(\d+)
// ConstraintName: regex
// ConstraintArgument: \d+
(var constraintName, var constraintArgument) = Parse(matchProcessorReference.ConstraintText);
if (!_options.ConstraintMap.TryGetValue(constraintName, out var constraintType))
{
throw new InvalidOperationException(
$"No constraint has been registered with name '{constraintName}'.");
}
var processor = ResolveMatchProcessor(
matchProcessorReference.ParameterName,
matchProcessorReference.Optional,
constraintType,
constraintArgument);
if (processor != null)
{
return processor;
}
if (!typeof(IRouteConstraint).IsAssignableFrom(constraintType))
{
throw new InvalidOperationException(
Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint(
constraintType, constraintName, typeof(IRouteConstraint).Name));
}
try
{
return CreateMatchProcessorFromRouteConstraint(
matchProcessorReference.ParameterName,
constraintType,
constraintArgument);
}
catch (RouteCreationException)
{
throw;
}
catch (Exception exception)
{
throw new InvalidOperationException(
$"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.",
exception);
}
}
private MatchProcessor CreateMatchProcessorFromRouteConstraint(
string parameterName,
Type constraintType,
string constraintArgument)
{
var routeConstraint = DefaultInlineConstraintResolver.CreateConstraint(constraintType, constraintArgument);
return (new MatchProcessorReference(parameterName, routeConstraint)).MatchProcessor;
}
private MatchProcessor ResolveMatchProcessor(
string parameterName,
bool optional,
Type constraintType,
string constraintArgument)
{
if (constraintType == null)
{
throw new ArgumentNullException(nameof(constraintType));
}
if (!typeof(MatchProcessor).IsAssignableFrom(constraintType))
{
// Since a constraint type could be of type IRouteConstraint, do not throw
return null;
}
var registeredProcessor = _serviceProvider.GetRequiredService(constraintType);
if (registeredProcessor is MatchProcessor matchProcessor)
{
if (optional)
{
matchProcessor = new OptionalMatchProcessor(matchProcessor);
}
matchProcessor.Initialize(parameterName, constraintArgument);
return matchProcessor;
}
else
{
throw new InvalidOperationException(
$"Registered constraint type '{constraintType}' is not of type '{typeof(MatchProcessor)}'.");
}
}
private (string constraintName, string constraintArgument) Parse(string constraintText)
{
string constraintName;
string constraintArgument;
var indexOfFirstOpenParens = constraintText.IndexOf('(');
if (indexOfFirstOpenParens >= 0 && constraintText.EndsWith(")", StringComparison.Ordinal))
{
constraintName = constraintText.Substring(0, indexOfFirstOpenParens);
constraintArgument = constraintText.Substring(
indexOfFirstOpenParens + 1,
constraintText.Length - indexOfFirstOpenParens - 2);
}
else
{
constraintName = constraintText;
constraintArgument = null;
}
return (constraintName, constraintArgument);
}
}
}

View File

@ -0,0 +1,18 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public abstract class MatchProcessor
{
public virtual void Initialize(string parameterName, string constraintArgument)
{
}
public abstract bool ProcessInbound(HttpContext httpContext, RouteValueDictionary values);
public abstract bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values);
}
}

View File

@ -0,0 +1,42 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public abstract class MatchProcessorBase : MatchProcessor
{
public string ParameterName { get; private set; }
public string ConstraintArgument { get; private set; }
public override void Initialize(string parameterName, string constraintArgument)
{
ParameterName = parameterName;
ConstraintArgument = constraintArgument;
}
public abstract bool Process(object value);
public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary values)
{
return Process(values);
}
public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values)
{
return Process(values);
}
private bool Process(RouteValueDictionary values)
{
if (!values.TryGetValue(ParameterName, out var value) || value == null)
{
return false;
}
return Process(value);
}
}
}

View File

@ -0,0 +1,10 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal abstract class MatchProcessorFactory
{
public abstract MatchProcessor Create(MatchProcessorReference matchProcessorReference);
}
}

View File

@ -0,0 +1,86 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public sealed class MatchProcessorReference
{
// Example:
// api/products/{productId:regex(\d+)}
//
// ParameterName = productId
// ConstraintText = regex(\d+)
// ConstraintArgument = \d+
public MatchProcessorReference(string parameterName, string constraintText)
{
ParameterName = parameterName;
ConstraintText = constraintText;
}
public MatchProcessorReference(string parameterName, bool optional, string constraintText)
{
ParameterName = parameterName;
Optional = optional;
ConstraintText = constraintText;
}
public MatchProcessorReference(string parameterName, MatchProcessor matchProcessor)
{
ParameterName = parameterName;
MatchProcessor = matchProcessor;
}
internal MatchProcessor MatchProcessor { get; private set; }
internal string ConstraintText { get; private set; }
internal string ParameterName { get; private set; }
internal bool Optional { get; private set; }
public MatchProcessorReference(string parameterName, IRouteConstraint routeConstraint)
: this(parameterName, new RouteConstraintMatchProcessorAdapter(parameterName, routeConstraint))
{
}
private class RouteConstraintMatchProcessorAdapter : MatchProcessor
{
public string ParameterName { get; private set; }
public IRouteConstraint RouteConstraint { get; }
public RouteConstraintMatchProcessorAdapter(string parameterName, IRouteConstraint routeConstraint)
{
ParameterName = parameterName;
RouteConstraint = routeConstraint;
}
public override void Initialize(string parameterName, string constraintArgument)
{
}
public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary routeValues)
{
return RouteConstraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
routeValues,
RouteDirection.IncomingRequest);
}
public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values)
{
return RouteConstraint.Match(
httpContext,
NullRouter.Instance,
ParameterName,
values,
RouteDirection.UrlGeneration);
}
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Template;
@ -20,6 +21,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
string template,
RouteValueDictionary defaults,
RouteValueDictionary requiredValues,
List<MatchProcessorReference> nonInlineMatchProcessorReferences,
int order,
EndpointMetadataCollection metadata,
string displayName)
@ -44,6 +46,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
RequiredValues = requiredValues;
var mergedDefaults = GetDefaults(ParsedTemplate, defaults);
Defaults = mergedDefaults;
var mergedReferences = MergeMatchProcessorReferences(ParsedTemplate, nonInlineMatchProcessorReferences);
MatchProcessorReferences = mergedReferences.AsReadOnly();
}
public int Order { get; }
@ -57,6 +62,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
// Todo: needs review
public RouteTemplate ParsedTemplate { get; }
public IReadOnlyList<MatchProcessorReference> MatchProcessorReferences { get; }
// Merge inline and non inline defaults into one
private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate, RouteValueDictionary nonInlineDefaults)
{
@ -81,5 +88,33 @@ namespace Microsoft.AspNetCore.Routing.Matchers
return result;
}
private List<MatchProcessorReference> MergeMatchProcessorReferences(
RouteTemplate parsedTemplate,
List<MatchProcessorReference> nonInlineReferences)
{
var matchProcessorReferences = new List<MatchProcessorReference>();
if (nonInlineReferences != null)
{
matchProcessorReferences.AddRange(nonInlineReferences);
}
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.InlineConstraints != null)
{
foreach (var constraint in parameter.InlineConstraints)
{
matchProcessorReferences.Add(
new MatchProcessorReference(
parameter.Name,
optional: parameter.IsOptional,
constraintText: constraint.Constraint));
}
}
}
return matchProcessorReferences;
}
}
}

View File

@ -0,0 +1,44 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class OptionalMatchProcessor : MatchProcessor
{
private readonly MatchProcessor _innerMatchProcessor;
public OptionalMatchProcessor(MatchProcessor innerMatchProcessor)
{
_innerMatchProcessor = innerMatchProcessor;
}
public string ParameterName { get; private set; }
public override void Initialize(string parameterName, string constraintArgument)
{
ParameterName = parameterName;
_innerMatchProcessor.Initialize(parameterName, constraintArgument);
}
public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary values)
{
return Process(httpContext, values);
}
public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values)
{
return Process(httpContext, values);
}
private bool Process(HttpContext httpContext, RouteValueDictionary values)
{
if (values.TryGetValue(ParameterName, out var value))
{
return _innerMatchProcessor.ProcessInbound(httpContext, values);
}
return true;
}
}
}

View File

@ -17,20 +17,20 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class TreeMatcher : Matcher
{
private readonly IInlineConstraintResolver _constraintFactory;
private readonly MatchProcessorFactory _matchProcessorFactory;
private readonly ILogger _logger;
private readonly EndpointSelector _endpointSelector;
private readonly DataSourceDependantCache<UrlMatchingTree[]> _cache;
public TreeMatcher(
IInlineConstraintResolver constraintFactory,
MatchProcessorFactory matchProcessorFactory,
ILogger logger,
EndpointDataSource dataSource,
EndpointSelector endpointSelector)
{
if (constraintFactory == null)
if (matchProcessorFactory == null)
{
throw new ArgumentNullException(nameof(constraintFactory));
throw new ArgumentNullException(nameof(matchProcessorFactory));
}
if (logger == null)
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new ArgumentNullException(nameof(dataSource));
}
_constraintFactory = constraintFactory;
_matchProcessorFactory = matchProcessorFactory;
_logger = logger;
_endpointSelector = endpointSelector;
_cache = new DataSourceDependantCache<UrlMatchingTree[]>(dataSource, CreateTrees);
@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
foreach (var item in node.Matches)
{
var entry = item.Entry;
var tagData = (InboundEntryTagData)entry.Tag;
var matcher = item.TemplateMatcher;
values.Clear();
@ -89,12 +90,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Log.MatchedTemplate(_logger, httpContext, entry.RouteTemplate);
if (!MatchConstraints(httpContext, values, entry.Constraints))
if (!MatchConstraints(httpContext, values, tagData.MatchProcessors))
{
continue;
}
SelectEndpoint(httpContext, feature, (MatcherEndpoint[])entry.Tag);
SelectEndpoint(httpContext, feature, tagData.Endpoints);
if (feature.Endpoint != null)
{
@ -123,18 +124,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
private bool MatchConstraints(
HttpContext httpContext,
RouteValueDictionary values,
IDictionary<string, IRouteConstraint> constraints)
IList<MatchProcessor> matchProcessors)
{
if (constraints != null)
if (matchProcessors != null)
{
foreach (var kvp in constraints)
foreach (var processor in matchProcessors)
{
var constraint = kvp.Value;
if (!constraint.Match(httpContext, new DummyRouter(), kvp.Key, values, RouteDirection.IncomingRequest))
if (!processor.ProcessInbound(httpContext, values))
{
values.TryGetValue(kvp.Key, out var value);
Log.ConstraintFailed(_logger, value, kvp.Key, kvp.Value);
return false;
}
}
@ -223,7 +220,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
return trees.ToArray();
}
private InboundRouteEntry MapInbound(RouteTemplate template, Endpoint[] endpoints, int order)
private InboundRouteEntry MapInbound(RouteTemplate template, MatcherEndpoint[] endpoints, int order)
{
if (template == null)
{
@ -235,27 +232,24 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Precedence = RoutePrecedence.ComputeInbound(template),
RouteTemplate = template,
Order = order,
Tag = endpoints,
};
var constraintBuilder = new RouteConstraintBuilder(_constraintFactory, template.TemplateText);
foreach (var parameter in template.Parameters)
{
if (parameter.InlineConstraints != null)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
// Since all endpoints within a group are expected to have same template and same constraints,
// get the first endpoint which has the processor references
var endpoint = endpoints[0];
foreach (var constraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint);
}
}
var matchProcessors = new List<MatchProcessor>();
foreach (var matchProcessorReference in endpoint.MatchProcessorReferences)
{
var matchProcessor = _matchProcessorFactory.Create(matchProcessorReference);
matchProcessors.Add(matchProcessor);
}
entry.Constraints = constraintBuilder.Build();
entry.Tag = new InboundEntryTagData()
{
Endpoints = endpoints,
MatchProcessors = matchProcessors,
};
entry.Defaults = new RouteValueDictionary();
foreach (var parameter in entry.RouteTemplate.Parameters)
@ -350,5 +344,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
_matchedTemplate(logger, template.TemplateText, httpContext.Request.Path, null);
}
}
private class InboundEntryTagData
{
public MatcherEndpoint[] Endpoints { get; set; }
public List<MatchProcessor> MatchProcessors { get; set; }
}
}
}

View File

@ -9,18 +9,18 @@ namespace Microsoft.AspNetCore.Routing.Matchers
{
internal class TreeMatcherFactory : MatcherFactory
{
private readonly IInlineConstraintResolver _constraintFactory;
private readonly MatchProcessorFactory _matchProcessorFactory;
private readonly ILogger<TreeMatcher> _logger;
private readonly EndpointSelector _endpointSelector;
public TreeMatcherFactory(
IInlineConstraintResolver constraintFactory,
MatchProcessorFactory matchProcessorFactory,
ILogger<TreeMatcher> logger,
EndpointSelector endpointSelector)
{
if (constraintFactory == null)
if (matchProcessorFactory == null)
{
throw new ArgumentNullException(nameof(constraintFactory));
throw new ArgumentNullException(nameof(matchProcessorFactory));
}
if (logger == null)
@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new ArgumentNullException(nameof(endpointSelector));
}
_constraintFactory = constraintFactory;
_matchProcessorFactory = matchProcessorFactory;
_logger = logger;
_endpointSelector = endpointSelector;
}
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new ArgumentNullException(nameof(dataSource));
}
return new TreeMatcher(_constraintFactory, _logger, dataSource, _endpointSelector);
return new TreeMatcher(_matchProcessorFactory, _logger, dataSource, _endpointSelector);
}
}
}

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.EndpointFinders;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers;
@ -17,19 +16,16 @@ namespace Microsoft.AspNetCore.Routing
internal class RouteValuesBasedEndpointFinder : IEndpointFinder<RouteValuesBasedEndpointFinderContext>
{
private readonly CompositeEndpointDataSource _endpointDataSource;
private readonly IInlineConstraintResolver _inlineConstraintResolver;
private readonly ObjectPool<UriBuildingContext> _objectPool;
private LinkGenerationDecisionTree _allMatchesLinkGenerationTree;
private IDictionary<string, LinkGenerationDecisionTree> _namedMatches;
public RouteValuesBasedEndpointFinder(
CompositeEndpointDataSource endpointDataSource,
ObjectPool<UriBuildingContext> objectPool,
IInlineConstraintResolver inlineConstraintResolver)
ObjectPool<UriBuildingContext> objectPool)
{
_endpointDataSource = endpointDataSource;
_objectPool = objectPool;
_inlineConstraintResolver = inlineConstraintResolver;
BuildOutboundMatches();
}
@ -110,28 +106,6 @@ namespace Microsoft.AspNetCore.Routing
Data = endpoint,
RouteName = routeNameMetadata?.Name,
};
// TODO: review. These route constriants should be constructed when the endpoint
// is built. This way they can be checked for validity on app startup too
var constraintBuilder = new RouteConstraintBuilder(
_inlineConstraintResolver,
endpoint.ParsedTemplate.TemplateText);
foreach (var parameter in endpoint.ParsedTemplate.Parameters)
{
if (parameter.InlineConstraints != null)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var constraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint);
}
}
}
entry.Constraints = constraintBuilder.Build();
entry.Defaults = endpoint.Defaults;
return entry;
}
@ -146,21 +120,5 @@ namespace Microsoft.AspNetCore.Routing
}
return result;
}
// Used only to hook up link generation, and it doesn't need to do anything.
private class NullRouter : IRouter
{
public static readonly NullRouter Instance = new NullRouter();
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return null;
}
public Task RouteAsync(RouteContext context)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -61,6 +61,74 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
Assert.Equal(expectedContent, actualContent);
}
[Fact]
public async Task MatchesEndpoint_WithSuccessfulConstraintMatch()
{
// Arrange
var expectedContent = "WithConstraints";
// Act
var response = await _client.GetAsync("/withconstraints/555_001");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
var actualContent = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedContent, actualContent);
}
[Fact]
public async Task DoesNotMatchEndpoint_IfConstraintMatchFails()
{
// Arrange & Act
var response = await _client.GetAsync("/withconstraints/555");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task MatchesEndpoint_WithSuccessful_OptionalConstraintMatch()
{
// Arrange
var expectedContent = "withoptionalconstraints";
// Act
var response = await _client.GetAsync("/withoptionalconstraints/555_001");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
var actualContent = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedContent, actualContent);
}
[Fact]
public async Task MatchesEndpoint_WithSuccessful_OptionalConstraintMatch_NoValueForParameter()
{
// Arrange
var expectedContent = "withoptionalconstraints";
// Act
var response = await _client.GetAsync("/withoptionalconstraints");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
var actualContent = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedContent, actualContent);
}
[Fact]
public async Task DoesNotMatchEndpoint_IfOptionalConstraintMatchFails()
{
// Arrange & Act
var response = await _client.GetAsync("/withoptionalconstraints/555");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
public void Dispose()
{
_testServer.Dispose();

View File

@ -8,7 +8,9 @@ using Microsoft.AspNetCore.Routing.EndpointFinders;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
@ -788,6 +790,7 @@ namespace Microsoft.AspNetCore.Routing
template,
defaults,
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
0,
EndpointMetadataCollection.Empty,
null);
@ -796,8 +799,12 @@ namespace Microsoft.AspNetCore.Routing
private ILinkGenerator CreateLinkGenerator()
{
return new DefaultLinkGenerator(
new DefaultMatchProcessorFactory(
Options.Create(new RouteOptions()),
NullLogger<DefaultMatchProcessorFactory>.Instance,
Mock.Of<IServiceProvider>()),
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
Mock.Of<ILogger<DefaultLinkGenerator>>());
NullLogger<DefaultLinkGenerator>.Instance);
}
}
}

View File

@ -4,9 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;

View File

@ -1,6 +1,9 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.TestObjects;
@ -9,9 +12,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints

View File

@ -0,0 +1,190 @@
// 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.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
{
public class DefaultMatchProcessorFactoryTest
{
[Fact]
public void Create_ThrowsException_IfNoConstraintOrMatchProcessor_FoundInMap()
{
// Arrange
var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", @"notpresent(\d+)");
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(
() => factory.Create(matchProcessorReference));
}
[Fact]
public void Create_CreatesMatchProcessor_FromConstraintText_AndRouteConstraint()
{
// Arrange
var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", "int");
// Act 1
var processor = factory.Create(matchProcessorReference);
// Assert 1
Assert.NotNull(processor);
// Act 2
var isMatch = processor.ProcessInbound(
new DefaultHttpContext(),
new RouteValueDictionary(new { id = 10 }));
// Assert 2
Assert.True(isMatch);
// Act 2
isMatch = processor.ProcessInbound(
new DefaultHttpContext(),
new RouteValueDictionary(new { id = "foo" }));
// Assert 2
Assert.False(isMatch);
}
[Fact]
public void Create_CreatesMatchProcessor_FromConstraintText_AndCustomMatchProcessor()
{
// Arrange
var options = new RouteOptions();
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor));
var services = new ServiceCollection();
services.AddTransient<EndsWithStringMatchProcessor>();
var factory = GetMatchProcessorFactory(options, services);
var matchProcessorReference = new MatchProcessorReference("id", "endsWith(_001)");
// Act 1
var processor = factory.Create(matchProcessorReference);
// Assert 1
Assert.NotNull(processor);
// Act 2
var isMatch = processor.ProcessInbound(
new DefaultHttpContext(),
new RouteValueDictionary(new { id = "555_001" }));
// Assert 2
Assert.True(isMatch);
// Act 2
isMatch = processor.ProcessInbound(
new DefaultHttpContext(),
new RouteValueDictionary(new { id = "444" }));
// Assert 2
Assert.False(isMatch);
}
[Fact]
public void Create_ReturnsMatchProcessor_IfAvailable()
{
// Arrange
var factory = GetMatchProcessorFactory();
var matchProcessorReference = new MatchProcessorReference("id", Mock.Of<MatchProcessor>());
var expected = matchProcessorReference.MatchProcessor;
// Act
var processor = factory.Create(matchProcessorReference);
// Assert
Assert.Same(expected, processor);
}
[Fact]
public void Create_ReturnsMatchProcessor_WithSuppliedRouteConstraint()
{
// Arrange
var factory = GetMatchProcessorFactory();
var constraint = TestRouteConstraint.Create();
var matchProcessorReference = new MatchProcessorReference("id", constraint);
var processor = factory.Create(matchProcessorReference);
var expectedHttpContext = new DefaultHttpContext();
var expectedValues = new RouteValueDictionary();
// Act
processor.ProcessInbound(expectedHttpContext, expectedValues);
// Assert
Assert.Same(expectedHttpContext, constraint.HttpContext);
Assert.Same(expectedValues, constraint.Values);
Assert.Equal("id", constraint.RouteKey);
Assert.Equal(RouteDirection.IncomingRequest, constraint.RouteDirection);
Assert.Same(NullRouter.Instance, constraint.Route);
}
private DefaultMatchProcessorFactory GetMatchProcessorFactory(
RouteOptions options = null,
ServiceCollection services = null)
{
if (options == null)
{
options = new RouteOptions();
}
if (services == null)
{
services = new ServiceCollection();
}
return new DefaultMatchProcessorFactory(
Options.Create(options),
NullLogger<DefaultMatchProcessorFactory>.Instance,
services.BuildServiceProvider());
}
private class TestRouteConstraint : IRouteConstraint
{
private TestRouteConstraint() { }
public HttpContext HttpContext { get; private set; }
public IRouter Route { get; private set; }
public string RouteKey { get; private set; }
public RouteValueDictionary Values { get; private set; }
public RouteDirection RouteDirection { get; private set; }
public static TestRouteConstraint Create()
{
return new TestRouteConstraint();
}
public bool Match(
HttpContext httpContext,
IRouter route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
HttpContext = httpContext;
Route = route;
RouteKey = routeKey;
Values = values;
RouteDirection = routeDirection;
return false;
}
}
private class EndsWithStringMatchProcessor : MatchProcessorBase
{
public override bool Process(object value)
{
var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
return valueString.EndsWith(ConstraintArgument);
}
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
@ -43,6 +44,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
template,
defaults,
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
order ?? 0,
EndpointMetadataCollection.Empty,
"endpoint: " + template);

View File

@ -1,12 +1,14 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers
@ -16,13 +18,23 @@ namespace Microsoft.AspNetCore.Routing.Matchers
private MatcherEndpoint CreateEndpoint(string template, int order, object defaultValues = null, EndpointMetadataCollection metadata = null)
{
var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues);
return new MatcherEndpoint((next) => null, template, defaults, new RouteValueDictionary(), order, metadata ?? EndpointMetadataCollection.Empty, template);
return new MatcherEndpoint(
(next) => null,
template, defaults,
new RouteValueDictionary(),
new List<MatchProcessorReference>(),
order,
metadata ?? EndpointMetadataCollection.Empty,
template);
}
private TreeMatcher CreateTreeMatcher(EndpointDataSource endpointDataSource)
{
var compositeDataSource = new CompositeEndpointDataSource(new[] { endpointDataSource });
var defaultInlineConstraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()));
var defaultInlineConstraintResolver = new DefaultMatchProcessorFactory(
Options.Create(new RouteOptions()),
NullLogger<DefaultMatchProcessorFactory>.Instance,
Mock.Of<IServiceProvider>());
var endpointSelector = new EndpointSelector(
compositeDataSource,
new EndpointConstraintCache(compositeDataSource, new IEndpointConstraintProvider[] { new DefaultEndpointConstraintProvider() }),