Added support for route constraints in Dispatcher world

This commit is contained in:
Nate McMaster 2018-06-28 16:48:57 -07:00 committed by Kiran Challa
parent 576c14a1b5
commit 85e92ab3cc
25 changed files with 886 additions and 110 deletions

View File

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

View File

@ -34,13 +34,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
} }
return new MatcherEndpoint( return new MatcherEndpoint(
(next) => (context) => Task.CompletedTask, (next) => (context) => Task.CompletedTask,
template, template,
new RouteValueDictionary(), new RouteValueDictionary(),
new RouteValueDictionary(), new RouteValueDictionary(),
0, new List<MatchProcessorReference>(),
new EndpointMetadataCollection(metadata), 0,
template); EndpointMetadataCollection.Empty,
template);
} }
internal static int[] SampleRequests(int endpointCount, int count) 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matchers; using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -17,7 +19,13 @@ namespace DispatcherSample.Web
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddRouting(); services.AddTransient<EndsWithStringMatchProcessor>();
services.AddRouting(options =>
{
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor));
});
services.AddDispatcher(options => services.AddDispatcher(options =>
{ {
options.DataSources.Add(new DefaultEndpointDataSource(new[] options.DataSources.Add(new DefaultEndpointDataSource(new[]
@ -31,7 +39,13 @@ namespace DispatcherSample.Web
response.ContentLength = payloadLength; response.ContentLength = payloadLength;
return response.Body.WriteAsync(_homePayload, 0, 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) => new MatcherEndpoint((next) => (httpContext) =>
{ {
var response = httpContext.Response; var response = httpContext.Response;
@ -41,7 +55,41 @@ namespace DispatcherSample.Web
response.ContentLength = payloadLength; response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloWorldPayload, 0, 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 // No arguments - call the default constructor
if (argumentString == null) if (argumentString == null)

View File

@ -13,15 +13,18 @@ using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNetCore.Routing namespace Microsoft.AspNetCore.Routing
{ {
public class DefaultLinkGenerator : ILinkGenerator internal class DefaultLinkGenerator : ILinkGenerator
{ {
private readonly MatchProcessorFactory _matchProcessorFactory;
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool; private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
private readonly ILogger<DefaultLinkGenerator> _logger; private readonly ILogger<DefaultLinkGenerator> _logger;
public DefaultLinkGenerator( public DefaultLinkGenerator(
MatchProcessorFactory matchProcessorFactory,
ObjectPool<UriBuildingContext> uriBuildingContextPool, ObjectPool<UriBuildingContext> uriBuildingContextPool,
ILogger<DefaultLinkGenerator> logger) ILogger<DefaultLinkGenerator> logger)
{ {
_matchProcessorFactory = matchProcessorFactory;
_uriBuildingContextPool = uriBuildingContextPool; _uriBuildingContextPool = uriBuildingContextPool;
_logger = logger; _logger = logger;
} }
@ -61,7 +64,7 @@ namespace Microsoft.AspNetCore.Routing
foreach (var endpoint in matcherEndpoints) foreach (var endpoint in matcherEndpoints)
{ {
link = GetLink(endpoint.ParsedTemplate, endpoint.Defaults, explicitValues, ambientValues); link = GetLink(endpoint, explicitValues, ambientValues);
if (link != null) if (link != null)
{ {
return true; return true;
@ -72,27 +75,55 @@ namespace Microsoft.AspNetCore.Routing
} }
private string GetLink( private string GetLink(
RouteTemplate template, MatcherEndpoint endpoint,
RouteValueDictionary defaults,
RouteValueDictionary explicitValues, RouteValueDictionary explicitValues,
RouteValueDictionary ambientValues) RouteValueDictionary ambientValues)
{ {
var templateBinder = new TemplateBinder( var templateBinder = new TemplateBinder(
UrlEncoder.Default, UrlEncoder.Default,
_uriBuildingContextPool, _uriBuildingContextPool,
template, endpoint.ParsedTemplate,
defaults); endpoint.Defaults);
var values = templateBinder.GetValues(ambientValues, explicitValues); var templateValuesResult = templateBinder.GetValues(ambientValues, explicitValues);
if (values == null) if (templateValuesResult == null)
{ {
// We're missing one of the required values for this route. // We're missing one of the required values for this route.
return null; 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 System;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Tree; using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -28,6 +30,7 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(services)); throw new ArgumentNullException(nameof(services));
} }
services.TryAddSingleton<MatchProcessorFactory, DefaultMatchProcessorFactory>();
services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>(); services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>();
services.TryAddSingleton<ObjectPool<UriBuildingContext>>(s => 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Template;
@ -20,6 +21,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
string template, string template,
RouteValueDictionary defaults, RouteValueDictionary defaults,
RouteValueDictionary requiredValues, RouteValueDictionary requiredValues,
List<MatchProcessorReference> nonInlineMatchProcessorReferences,
int order, int order,
EndpointMetadataCollection metadata, EndpointMetadataCollection metadata,
string displayName) string displayName)
@ -44,6 +46,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
RequiredValues = requiredValues; RequiredValues = requiredValues;
var mergedDefaults = GetDefaults(ParsedTemplate, defaults); var mergedDefaults = GetDefaults(ParsedTemplate, defaults);
Defaults = mergedDefaults; Defaults = mergedDefaults;
var mergedReferences = MergeMatchProcessorReferences(ParsedTemplate, nonInlineMatchProcessorReferences);
MatchProcessorReferences = mergedReferences.AsReadOnly();
} }
public int Order { get; } public int Order { get; }
@ -57,6 +62,8 @@ namespace Microsoft.AspNetCore.Routing.Matchers
// Todo: needs review // Todo: needs review
public RouteTemplate ParsedTemplate { get; } public RouteTemplate ParsedTemplate { get; }
public IReadOnlyList<MatchProcessorReference> MatchProcessorReferences { get; }
// Merge inline and non inline defaults into one // Merge inline and non inline defaults into one
private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate, RouteValueDictionary nonInlineDefaults) private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate, RouteValueDictionary nonInlineDefaults)
{ {
@ -81,5 +88,33 @@ namespace Microsoft.AspNetCore.Routing.Matchers
return result; 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 internal class TreeMatcher : Matcher
{ {
private readonly IInlineConstraintResolver _constraintFactory; private readonly MatchProcessorFactory _matchProcessorFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly EndpointSelector _endpointSelector; private readonly EndpointSelector _endpointSelector;
private readonly DataSourceDependantCache<UrlMatchingTree[]> _cache; private readonly DataSourceDependantCache<UrlMatchingTree[]> _cache;
public TreeMatcher( public TreeMatcher(
IInlineConstraintResolver constraintFactory, MatchProcessorFactory matchProcessorFactory,
ILogger logger, ILogger logger,
EndpointDataSource dataSource, EndpointDataSource dataSource,
EndpointSelector endpointSelector) EndpointSelector endpointSelector)
{ {
if (constraintFactory == null) if (matchProcessorFactory == null)
{ {
throw new ArgumentNullException(nameof(constraintFactory)); throw new ArgumentNullException(nameof(matchProcessorFactory));
} }
if (logger == null) if (logger == null)
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new ArgumentNullException(nameof(dataSource)); throw new ArgumentNullException(nameof(dataSource));
} }
_constraintFactory = constraintFactory; _matchProcessorFactory = matchProcessorFactory;
_logger = logger; _logger = logger;
_endpointSelector = endpointSelector; _endpointSelector = endpointSelector;
_cache = new DataSourceDependantCache<UrlMatchingTree[]>(dataSource, CreateTrees); _cache = new DataSourceDependantCache<UrlMatchingTree[]>(dataSource, CreateTrees);
@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
foreach (var item in node.Matches) foreach (var item in node.Matches)
{ {
var entry = item.Entry; var entry = item.Entry;
var tagData = (InboundEntryTagData)entry.Tag;
var matcher = item.TemplateMatcher; var matcher = item.TemplateMatcher;
values.Clear(); values.Clear();
@ -89,12 +90,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Log.MatchedTemplate(_logger, httpContext, entry.RouteTemplate); Log.MatchedTemplate(_logger, httpContext, entry.RouteTemplate);
if (!MatchConstraints(httpContext, values, entry.Constraints)) if (!MatchConstraints(httpContext, values, tagData.MatchProcessors))
{ {
continue; continue;
} }
SelectEndpoint(httpContext, feature, (MatcherEndpoint[])entry.Tag); SelectEndpoint(httpContext, feature, tagData.Endpoints);
if (feature.Endpoint != null) if (feature.Endpoint != null)
{ {
@ -123,18 +124,14 @@ namespace Microsoft.AspNetCore.Routing.Matchers
private bool MatchConstraints( private bool MatchConstraints(
HttpContext httpContext, HttpContext httpContext,
RouteValueDictionary values, 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 (!processor.ProcessInbound(httpContext, values))
if (!constraint.Match(httpContext, new DummyRouter(), kvp.Key, values, RouteDirection.IncomingRequest))
{ {
values.TryGetValue(kvp.Key, out var value);
Log.ConstraintFailed(_logger, value, kvp.Key, kvp.Value);
return false; return false;
} }
} }
@ -223,7 +220,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
return trees.ToArray(); return trees.ToArray();
} }
private InboundRouteEntry MapInbound(RouteTemplate template, Endpoint[] endpoints, int order) private InboundRouteEntry MapInbound(RouteTemplate template, MatcherEndpoint[] endpoints, int order)
{ {
if (template == null) if (template == null)
{ {
@ -235,27 +232,24 @@ namespace Microsoft.AspNetCore.Routing.Matchers
Precedence = RoutePrecedence.ComputeInbound(template), Precedence = RoutePrecedence.ComputeInbound(template),
RouteTemplate = template, RouteTemplate = template,
Order = order, Order = order,
Tag = endpoints,
}; };
var constraintBuilder = new RouteConstraintBuilder(_constraintFactory, template.TemplateText); // Since all endpoints within a group are expected to have same template and same constraints,
foreach (var parameter in template.Parameters) // get the first endpoint which has the processor references
{ var endpoint = endpoints[0];
if (parameter.InlineConstraints != null)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var constraint in parameter.InlineConstraints) var matchProcessors = new List<MatchProcessor>();
{ foreach (var matchProcessorReference in endpoint.MatchProcessorReferences)
constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); {
} var matchProcessor = _matchProcessorFactory.Create(matchProcessorReference);
} matchProcessors.Add(matchProcessor);
} }
entry.Constraints = constraintBuilder.Build(); entry.Tag = new InboundEntryTagData()
{
Endpoints = endpoints,
MatchProcessors = matchProcessors,
};
entry.Defaults = new RouteValueDictionary(); entry.Defaults = new RouteValueDictionary();
foreach (var parameter in entry.RouteTemplate.Parameters) foreach (var parameter in entry.RouteTemplate.Parameters)
@ -350,5 +344,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
_matchedTemplate(logger, template.TemplateText, httpContext.Request.Path, null); _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 internal class TreeMatcherFactory : MatcherFactory
{ {
private readonly IInlineConstraintResolver _constraintFactory; private readonly MatchProcessorFactory _matchProcessorFactory;
private readonly ILogger<TreeMatcher> _logger; private readonly ILogger<TreeMatcher> _logger;
private readonly EndpointSelector _endpointSelector; private readonly EndpointSelector _endpointSelector;
public TreeMatcherFactory( public TreeMatcherFactory(
IInlineConstraintResolver constraintFactory, MatchProcessorFactory matchProcessorFactory,
ILogger<TreeMatcher> logger, ILogger<TreeMatcher> logger,
EndpointSelector endpointSelector) EndpointSelector endpointSelector)
{ {
if (constraintFactory == null) if (matchProcessorFactory == null)
{ {
throw new ArgumentNullException(nameof(constraintFactory)); throw new ArgumentNullException(nameof(matchProcessorFactory));
} }
if (logger == null) if (logger == null)
@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new ArgumentNullException(nameof(endpointSelector)); throw new ArgumentNullException(nameof(endpointSelector));
} }
_constraintFactory = constraintFactory; _matchProcessorFactory = matchProcessorFactory;
_logger = logger; _logger = logger;
_endpointSelector = endpointSelector; _endpointSelector = endpointSelector;
} }
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
throw new ArgumentNullException(nameof(dataSource)); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing.EndpointFinders; using Microsoft.AspNetCore.Routing.EndpointFinders;
using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matchers; using Microsoft.AspNetCore.Routing.Matchers;
@ -17,19 +16,16 @@ namespace Microsoft.AspNetCore.Routing
internal class RouteValuesBasedEndpointFinder : IEndpointFinder<RouteValuesBasedEndpointFinderContext> internal class RouteValuesBasedEndpointFinder : IEndpointFinder<RouteValuesBasedEndpointFinderContext>
{ {
private readonly CompositeEndpointDataSource _endpointDataSource; private readonly CompositeEndpointDataSource _endpointDataSource;
private readonly IInlineConstraintResolver _inlineConstraintResolver;
private readonly ObjectPool<UriBuildingContext> _objectPool; private readonly ObjectPool<UriBuildingContext> _objectPool;
private LinkGenerationDecisionTree _allMatchesLinkGenerationTree; private LinkGenerationDecisionTree _allMatchesLinkGenerationTree;
private IDictionary<string, LinkGenerationDecisionTree> _namedMatches; private IDictionary<string, LinkGenerationDecisionTree> _namedMatches;
public RouteValuesBasedEndpointFinder( public RouteValuesBasedEndpointFinder(
CompositeEndpointDataSource endpointDataSource, CompositeEndpointDataSource endpointDataSource,
ObjectPool<UriBuildingContext> objectPool, ObjectPool<UriBuildingContext> objectPool)
IInlineConstraintResolver inlineConstraintResolver)
{ {
_endpointDataSource = endpointDataSource; _endpointDataSource = endpointDataSource;
_objectPool = objectPool; _objectPool = objectPool;
_inlineConstraintResolver = inlineConstraintResolver;
BuildOutboundMatches(); BuildOutboundMatches();
} }
@ -110,28 +106,6 @@ namespace Microsoft.AspNetCore.Routing
Data = endpoint, Data = endpoint,
RouteName = routeNameMetadata?.Name, 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; entry.Defaults = endpoint.Defaults;
return entry; return entry;
} }
@ -146,21 +120,5 @@ namespace Microsoft.AspNetCore.Routing
} }
return result; 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); 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() public void Dispose()
{ {
_testServer.Dispose(); _testServer.Dispose();

View File

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

View File

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

View File

@ -1,6 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Http;
using Microsoft.AspNetCore.Routing.Matchers; using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.AspNetCore.Routing.TestObjects;
@ -9,9 +12,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Moq; using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Routing.EndpointConstraints 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -43,6 +44,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
template, template,
defaults, defaults,
new RouteValueDictionary(), new RouteValueDictionary(),
new List<MatchProcessorReference>(),
order ?? 0, order ?? 0,
EndpointMetadataCollection.Empty, EndpointMetadataCollection.Empty,
"endpoint: " + template); "endpoint: " + template);

View File

@ -1,12 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Routing.Matchers 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) private MatcherEndpoint CreateEndpoint(string template, int order, object defaultValues = null, EndpointMetadataCollection metadata = null)
{ {
var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues); 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) private TreeMatcher CreateTreeMatcher(EndpointDataSource endpointDataSource)
{ {
var compositeDataSource = new CompositeEndpointDataSource(new[] { 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( var endpointSelector = new EndpointSelector(
compositeDataSource, compositeDataSource,
new EndpointConstraintCache(compositeDataSource, new IEndpointConstraintProvider[] { new DefaultEndpointConstraintProvider() }), new EndpointConstraintCache(compositeDataSource, new IEndpointConstraintProvider[] { new DefaultEndpointConstraintProvider() }),