Added support for route constraints in Dispatcher world
This commit is contained in:
parent
1c7f53ae39
commit
42708bec91
|
|
@ -34,6 +34,7 @@ namespace Benchmarks
|
|||
template: "/plaintext",
|
||||
defaults: new RouteValueDictionary(),
|
||||
requiredValues: new RouteValueDictionary(),
|
||||
nonInlineMatchProcessorReferences: null,
|
||||
order: 0,
|
||||
metadata: EndpointMetadataCollection.Empty,
|
||||
displayName: "Plaintext"),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() }),
|
||||
|
|
|
|||
Loading…
Reference in New Issue