Make DFA matcher the default
This commit is contained in:
parent
400d243f42
commit
477296a3cc
|
|
@ -30,8 +30,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
|
||||
private protected DfaMatcherBuilder CreateDfaMatcherBuilder()
|
||||
{
|
||||
var services = CreateServices();
|
||||
return ActivatorUtilities.CreateInstance<DfaMatcherBuilder>(services);
|
||||
return CreateServices().GetRequiredService<DfaMatcherBuilder>();
|
||||
}
|
||||
|
||||
private protected static MatcherEndpoint CreateEndpoint(string template, string httpMethod = null)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
}
|
||||
|
||||
// Note: we can't use DataSourceDependantCache here because we also need to handle a list of change
|
||||
// Note: we can't use DataSourceDependentCache here because we also need to handle a list of change
|
||||
// tokens, which is a complication most of our code doesn't have.
|
||||
private void Initialize()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Internal
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal class DataSourceDependantCache<T> where T : class
|
||||
internal class DataSourceDependentCache<T> where T : class
|
||||
{
|
||||
private readonly EndpointDataSource _dataSource;
|
||||
private readonly Func<IReadOnlyList<Endpoint>, T> _initializeCore;
|
||||
|
|
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
private bool _initialized;
|
||||
private T _value;
|
||||
|
||||
public DataSourceDependantCache(EndpointDataSource dataSource, Func<IReadOnlyList<Endpoint>, T> initialize)
|
||||
public DataSourceDependentCache(EndpointDataSource dataSource, Func<IReadOnlyList<Endpoint>, T> initialize)
|
||||
{
|
||||
if (dataSource == null)
|
||||
{
|
||||
|
|
@ -36,7 +36,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
// Default matcher implementation
|
||||
//
|
||||
services.TryAddSingleton<MatchProcessorFactory, DefaultMatchProcessorFactory>();
|
||||
services.TryAddSingleton<MatcherFactory, TreeMatcherFactory>();
|
||||
services.TryAddSingleton<MatcherFactory, DfaMatcherFactory>();
|
||||
services.TryAddTransient<DfaMatcherBuilder>();
|
||||
|
||||
// Link generation related services
|
||||
services.TryAddSingleton<IEndpointFinder<string>, NameBasedEndpointFinder>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class DataSourceDependentMatcher : Matcher
|
||||
{
|
||||
private readonly Func<MatcherBuilder> _matcherBuilderFactory;
|
||||
private readonly DataSourceDependentCache<Matcher> _cache;
|
||||
|
||||
public DataSourceDependentMatcher(
|
||||
EndpointDataSource dataSource,
|
||||
Func<MatcherBuilder> matcherBuilderFactory)
|
||||
{
|
||||
_matcherBuilderFactory = matcherBuilderFactory;
|
||||
|
||||
_cache = new DataSourceDependentCache<Matcher>(dataSource, CreateMatcher);
|
||||
_cache.EnsureInitialized();
|
||||
}
|
||||
|
||||
// Used in tests
|
||||
internal Matcher CurrentMatcher => _cache.Value;
|
||||
|
||||
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
|
||||
{
|
||||
return CurrentMatcher.MatchAsync(httpContext, feature);
|
||||
}
|
||||
|
||||
private Matcher CreateMatcher(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
var builder = _matcherBuilderFactory();
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
// By design we only look at MatcherEndpoint here. It's possible to
|
||||
// register other endpoint types, which are non-routable, and it's
|
||||
// ok that we won't route to them.
|
||||
var endpoint = endpoints[i] as MatcherEndpoint;
|
||||
if (endpoint != null)
|
||||
{
|
||||
builder.AddEndpoint(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class DfaMatcherFactory : MatcherFactory
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
// Using the service provider here so we can avoid coupling to the dependencies
|
||||
// of DfaMatcherBuilder.
|
||||
public DfaMatcherFactory(IServiceProvider services)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public override Matcher CreateMatcher(EndpointDataSource dataSource)
|
||||
{
|
||||
if (dataSource == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
return new DataSourceDependentMatcher(dataSource, () =>
|
||||
{
|
||||
return _services.GetRequiredService<DfaMatcherBuilder>();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
// computed based on the string length.
|
||||
public static int Tokenize(string path, Span<PathSegment> segments)
|
||||
{
|
||||
// This can happen in test scenarios.
|
||||
if (path == string.Empty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
int start = 1; // Paths always start with a leading /
|
||||
int end;
|
||||
|
|
|
|||
|
|
@ -14,14 +14,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
{
|
||||
Endpoint = endpoint;
|
||||
|
||||
HttpMethod = endpoint.Metadata.OfType<HttpMethodEndpointConstraint>().FirstOrDefault()?.HttpMethods.Single();
|
||||
Precedence = RoutePrecedence.ComputeInbound(endpoint.ParsedTemplate);
|
||||
}
|
||||
|
||||
public MatcherEndpoint Endpoint { get; }
|
||||
|
||||
public string HttpMethod { get; }
|
||||
|
||||
public int Order => Endpoint.Order;
|
||||
|
||||
public RouteTemplate Pattern => Endpoint.ParsedTemplate;
|
||||
|
|
|
|||
|
|
@ -1,354 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class TreeMatcher : Matcher
|
||||
{
|
||||
private readonly MatchProcessorFactory _matchProcessorFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly EndpointSelector _endpointSelector;
|
||||
private readonly DataSourceDependantCache<UrlMatchingTree[]> _cache;
|
||||
|
||||
public TreeMatcher(
|
||||
MatchProcessorFactory matchProcessorFactory,
|
||||
ILogger logger,
|
||||
EndpointDataSource dataSource,
|
||||
EndpointSelector endpointSelector)
|
||||
{
|
||||
if (matchProcessorFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(matchProcessorFactory));
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
if (dataSource == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
_matchProcessorFactory = matchProcessorFactory;
|
||||
_logger = logger;
|
||||
_endpointSelector = endpointSelector;
|
||||
_cache = new DataSourceDependantCache<UrlMatchingTree[]>(dataSource, CreateTrees);
|
||||
_cache.EnsureInitialized();
|
||||
}
|
||||
|
||||
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (feature == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(feature));
|
||||
}
|
||||
|
||||
var values = new RouteValueDictionary();
|
||||
feature.Values = values;
|
||||
|
||||
var cache = _cache.Value;
|
||||
for (var i = 0; i < cache.Length; i++)
|
||||
{
|
||||
var tree = cache[i];
|
||||
var tokenizer = new PathTokenizer(httpContext.Request.Path);
|
||||
|
||||
var treenumerator = new TreeEnumerator(tree.Root, tokenizer);
|
||||
|
||||
while (treenumerator.MoveNext())
|
||||
{
|
||||
var node = treenumerator.Current;
|
||||
foreach (var item in node.Matches)
|
||||
{
|
||||
var entry = item.Entry;
|
||||
var tagData = (InboundEntryTagData)entry.Tag;
|
||||
var matcher = item.TemplateMatcher;
|
||||
|
||||
values.Clear();
|
||||
if (!matcher.TryMatch(httpContext.Request.Path, values))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.MatchedTemplate(_logger, httpContext, entry.RouteTemplate);
|
||||
|
||||
if (!MatchConstraints(httpContext, values, tagData.MatchProcessors))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SelectEndpoint(httpContext, feature, tagData.Endpoints);
|
||||
|
||||
if (feature.Endpoint != null)
|
||||
{
|
||||
if (feature.Endpoint is MatcherEndpoint endpoint)
|
||||
{
|
||||
foreach (var kvp in endpoint.Defaults)
|
||||
{
|
||||
if (!feature.Values.ContainsKey(kvp.Key))
|
||||
{
|
||||
feature.Values[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Found a matching endpoint
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No match found
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool MatchConstraints(
|
||||
HttpContext httpContext,
|
||||
RouteValueDictionary values,
|
||||
IList<MatchProcessor> matchProcessors)
|
||||
{
|
||||
if (matchProcessors != null)
|
||||
{
|
||||
foreach (var processor in matchProcessors)
|
||||
{
|
||||
if (!processor.ProcessInbound(httpContext, values))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private class DummyRouter : IRouter
|
||||
{
|
||||
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task RouteAsync(RouteContext context)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectEndpoint(HttpContext httpContext, IEndpointFeature feature, IReadOnlyList<MatcherEndpoint> endpoints)
|
||||
{
|
||||
var endpoint = (MatcherEndpoint)_endpointSelector.SelectBestCandidate(httpContext, endpoints);
|
||||
|
||||
if (endpoint == null)
|
||||
{
|
||||
Log.MatchFailed(_logger, httpContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.MatchSuccess(_logger, httpContext, endpoint);
|
||||
|
||||
feature.Endpoint = endpoint;
|
||||
feature.Invoker = endpoint.Invoker;
|
||||
}
|
||||
}
|
||||
|
||||
private UrlMatchingTree[] CreateTrees(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
var groups = new Dictionary<Key, List<MatcherEndpoint>>();
|
||||
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var endpoint = endpoints[i] as MatcherEndpoint;
|
||||
if (endpoint == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var order = endpoint.Order;
|
||||
if (!groups.TryGetValue(new Key(order, endpoint.Template), out var group))
|
||||
{
|
||||
group = new List<MatcherEndpoint>();
|
||||
groups.Add(new Key(order, endpoint.Template), group);
|
||||
}
|
||||
|
||||
group.Add(endpoint);
|
||||
}
|
||||
|
||||
var entries = new List<InboundRouteEntry>();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var template = TemplateParser.Parse(group.Key.Template);
|
||||
var entryExists = entries.Any(item => item.RouteTemplate.TemplateText == template.TemplateText && item.Order == group.Key.Order);
|
||||
if (!entryExists)
|
||||
{
|
||||
entries.Add(MapInbound(template, group.Value.ToArray(), group.Key.Order));
|
||||
}
|
||||
}
|
||||
|
||||
var trees = new List<UrlMatchingTree>();
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
|
||||
while (trees.Count <= entry.Order)
|
||||
{
|
||||
trees.Add(new UrlMatchingTree(entry.Order));
|
||||
}
|
||||
|
||||
var tree = trees[entry.Order];
|
||||
tree.AddEntry(entry);
|
||||
}
|
||||
|
||||
return trees.ToArray();
|
||||
}
|
||||
|
||||
private InboundRouteEntry MapInbound(RouteTemplate template, MatcherEndpoint[] endpoints, int order)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(template));
|
||||
}
|
||||
|
||||
var entry = new InboundRouteEntry()
|
||||
{
|
||||
Precedence = RoutePrecedence.ComputeInbound(template),
|
||||
RouteTemplate = template,
|
||||
Order = order,
|
||||
};
|
||||
|
||||
// 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];
|
||||
|
||||
var matchProcessors = new List<MatchProcessor>();
|
||||
foreach (var matchProcessorReference in endpoint.MatchProcessorReferences)
|
||||
{
|
||||
var matchProcessor = _matchProcessorFactory.Create(matchProcessorReference);
|
||||
matchProcessors.Add(matchProcessor);
|
||||
}
|
||||
|
||||
entry.Tag = new InboundEntryTagData()
|
||||
{
|
||||
Endpoints = endpoints,
|
||||
MatchProcessors = matchProcessors,
|
||||
};
|
||||
|
||||
entry.Defaults = new RouteValueDictionary();
|
||||
foreach (var parameter in entry.RouteTemplate.Parameters)
|
||||
{
|
||||
if (parameter.DefaultValue != null)
|
||||
{
|
||||
entry.Defaults.Add(parameter.Name, parameter.DefaultValue);
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private readonly struct Key : IEquatable<Key>
|
||||
{
|
||||
public readonly int Order;
|
||||
public readonly string Template;
|
||||
|
||||
public Key(int order, string routePattern)
|
||||
{
|
||||
Order = order;
|
||||
Template = routePattern;
|
||||
}
|
||||
|
||||
public bool Equals(Key other)
|
||||
{
|
||||
return Order == other.Order && string.Equals(Template, other.Template, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is Key ? Equals((Key)obj) : false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCodeCombiner();
|
||||
hash.Add(Order);
|
||||
hash.Add(Template, StringComparer.OrdinalIgnoreCase);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string, PathString, Exception> _matchSuccess = LoggerMessage.Define<string, PathString>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1, "MatchSuccess"),
|
||||
"Request matched endpoint '{EndpointName}' for request path '{Path}'.");
|
||||
|
||||
private static readonly Action<ILogger, PathString, Exception> _matchFailed = LoggerMessage.Define<PathString>(
|
||||
LogLevel.Debug,
|
||||
new EventId(2, "MatchFailed"),
|
||||
"No endpoints matched request path '{Path}'.");
|
||||
|
||||
private static readonly Action<ILogger, PathString, IEnumerable<string>, Exception> _matchAmbiguous = LoggerMessage.Define<PathString, IEnumerable<string>>(
|
||||
LogLevel.Error,
|
||||
new EventId(3, "MatchAmbiguous"),
|
||||
"Request matched multiple endpoints for request path '{Path}'. Matching endpoints: {AmbiguousEndpoints}");
|
||||
|
||||
private static readonly Action<ILogger, object, string, IRouteConstraint, Exception> _constraintFailed = LoggerMessage.Define<object, string, IRouteConstraint>(
|
||||
LogLevel.Debug,
|
||||
new EventId(4, "ContraintFailed"),
|
||||
"Route value '{RouteValue}' with key '{RouteKey}' did not match the constraint '{RouteConstraint}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, PathString, Exception> _matchedTemplate = LoggerMessage.Define<string, PathString>(
|
||||
LogLevel.Debug,
|
||||
new EventId(5, "MatchedTemplate"),
|
||||
"Request matched the route pattern '{RouteTemplate}' for request path '{Path}'.");
|
||||
|
||||
public static void MatchSuccess(ILogger logger, HttpContext httpContext, Endpoint endpoint)
|
||||
{
|
||||
_matchSuccess(logger, endpoint.DisplayName, httpContext.Request.Path, null);
|
||||
}
|
||||
|
||||
public static void MatchFailed(ILogger logger, HttpContext httpContext)
|
||||
{
|
||||
_matchFailed(logger, httpContext.Request.Path, null);
|
||||
}
|
||||
|
||||
public static void MatchAmbiguous(ILogger logger, HttpContext httpContext, IEnumerable<Endpoint> endpoints)
|
||||
{
|
||||
_matchAmbiguous(logger, httpContext.Request.Path, endpoints.Select(e => e.DisplayName), null);
|
||||
}
|
||||
|
||||
public static void ConstraintFailed(ILogger logger, object routeValue, string routeKey, IRouteConstraint routeConstraint)
|
||||
{
|
||||
_constraintFailed(logger, routeValue, routeKey, routeConstraint, null);
|
||||
}
|
||||
|
||||
public static void MatchedTemplate(ILogger logger, HttpContext httpContext, RouteTemplate template)
|
||||
{
|
||||
_matchedTemplate(logger, template.TemplateText, httpContext.Request.Path, null);
|
||||
}
|
||||
}
|
||||
|
||||
private class InboundEntryTagData
|
||||
{
|
||||
public MatcherEndpoint[] Endpoints { get; set; }
|
||||
public List<MatchProcessor> MatchProcessors { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
// 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.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class TreeMatcherFactory : MatcherFactory
|
||||
{
|
||||
private readonly MatchProcessorFactory _matchProcessorFactory;
|
||||
private readonly ILogger<TreeMatcher> _logger;
|
||||
private readonly EndpointSelector _endpointSelector;
|
||||
|
||||
public TreeMatcherFactory(
|
||||
MatchProcessorFactory matchProcessorFactory,
|
||||
ILogger<TreeMatcher> logger,
|
||||
EndpointSelector endpointSelector)
|
||||
{
|
||||
if (matchProcessorFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(matchProcessorFactory));
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
if (endpointSelector == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpointSelector));
|
||||
}
|
||||
|
||||
_matchProcessorFactory = matchProcessorFactory;
|
||||
_logger = logger;
|
||||
_endpointSelector = endpointSelector;
|
||||
}
|
||||
|
||||
public override Matcher CreateMatcher(EndpointDataSource dataSource)
|
||||
{
|
||||
if (dataSource == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
return new TreeMatcher(_matchProcessorFactory, _logger, dataSource, _endpointSelector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// 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.Text;
|
||||
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class DataSourceDependentCacheTest
|
||||
{
|
||||
[Fact]
|
||||
public void Cache_Initializes_WhenEnsureInitializedCalled()
|
||||
{
|
||||
// Arrange
|
||||
var called = false;
|
||||
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var cache = new DataSourceDependentCache<string>(dataSource, (endpoints) =>
|
||||
{
|
||||
called = true;
|
||||
return "hello, world!";
|
||||
});
|
||||
|
||||
// Act
|
||||
cache.EnsureInitialized();
|
||||
|
||||
// Assert
|
||||
Assert.True(called);
|
||||
Assert.Equal("hello, world!", cache.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cache_DoesNotInitialize_WhenValueCalled()
|
||||
{
|
||||
// Arrange
|
||||
var called = false;
|
||||
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var cache = new DataSourceDependentCache<string>(dataSource, (endpoints) =>
|
||||
{
|
||||
called = true;
|
||||
return "hello, world!";
|
||||
});
|
||||
|
||||
// Act
|
||||
GC.KeepAlive(cache.Value);
|
||||
|
||||
// Assert
|
||||
Assert.False(called);
|
||||
Assert.Null(cache.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cache_Reinitializes_WhenDataSourceChanges()
|
||||
{
|
||||
// Arrange
|
||||
var count = 0;
|
||||
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var cache = new DataSourceDependentCache<string>(dataSource, (endpoints) =>
|
||||
{
|
||||
count++;
|
||||
return $"hello, {count}!";
|
||||
});
|
||||
|
||||
cache.EnsureInitialized();
|
||||
Assert.Equal("hello, 1!", cache.Value);
|
||||
|
||||
// Act
|
||||
dataSource.AddEndpoint(null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, count);
|
||||
Assert.Equal("hello, 2!", cache.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// 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.TestObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
public class DataSourceDependentMatcherTest
|
||||
{
|
||||
[Fact]
|
||||
public void Matcher_Initializes_InConstructor()
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
|
||||
// Act
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, TestMatcherBuilder.Create);
|
||||
|
||||
// Assert
|
||||
var inner = Assert.IsType<TestMatcher>(matcher.CurrentMatcher);
|
||||
Assert.Empty(inner.Endpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matcher_Reinitializes_WhenDataSourceChanges()
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, TestMatcherBuilder.Create);
|
||||
|
||||
var endpoint = new MatcherEndpoint(
|
||||
MatcherEndpoint.EmptyInvoker,
|
||||
"a/b/c",
|
||||
new RouteValueDictionary(),
|
||||
new RouteValueDictionary(),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"test");
|
||||
|
||||
// Act
|
||||
dataSource.AddEndpoint(endpoint);
|
||||
|
||||
// Assert
|
||||
var inner = Assert.IsType<TestMatcher>(matcher.CurrentMatcher);
|
||||
Assert.Collection(
|
||||
inner.Endpoints,
|
||||
e => Assert.Same(endpoint, e));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matcher_Ignores_NonMatcherEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var endpoint = new TestEndpoint(EndpointMetadataCollection.Empty, "test");
|
||||
dataSource.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, TestMatcherBuilder.Create);
|
||||
|
||||
// Assert
|
||||
var inner = Assert.IsType<TestMatcher>(matcher.CurrentMatcher);
|
||||
Assert.Empty(inner.Endpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cache_Reinitializes_WhenDataSourceChanges()
|
||||
{
|
||||
// Arrange
|
||||
var count = 0;
|
||||
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var cache = new DataSourceDependentCache<string>(dataSource, (endpoints) =>
|
||||
{
|
||||
count++;
|
||||
return $"hello, {count}!";
|
||||
});
|
||||
|
||||
cache.EnsureInitialized();
|
||||
Assert.Equal("hello, 1!", cache.Value);
|
||||
|
||||
// Act
|
||||
dataSource.AddEndpoint(null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, count);
|
||||
Assert.Equal("hello, 2!", cache.Value);
|
||||
}
|
||||
|
||||
private class TestMatcherBuilder : MatcherBuilder
|
||||
{
|
||||
public static Func<MatcherBuilder> Create = () => new TestMatcherBuilder();
|
||||
|
||||
private List<MatcherEndpoint> Endpoints { get; } = new List<MatcherEndpoint>();
|
||||
|
||||
public override void AddEndpoint(MatcherEndpoint endpoint)
|
||||
{
|
||||
Endpoints.Add(endpoint);
|
||||
}
|
||||
|
||||
public override Matcher Build()
|
||||
{
|
||||
return new TestMatcher() { Endpoints = Endpoints, };
|
||||
}
|
||||
}
|
||||
|
||||
private class TestMatcher : Matcher
|
||||
{
|
||||
public IReadOnlyList<MatcherEndpoint> Endpoints { get; set; }
|
||||
|
||||
public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
.AddOptions()
|
||||
.AddRouting()
|
||||
.AddDispatcher()
|
||||
.AddTransient<DfaMatcherBuilder>()
|
||||
.BuildServiceProvider();
|
||||
|
||||
var builder = services.GetRequiredService<DfaMatcherBuilder>();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
|
|
@ -13,14 +14,16 @@ using Xunit;
|
|||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
public class TreeMatcherTests
|
||||
// Many of these are integration tests that exercise the system end to end,
|
||||
// so we're reusing the services here.
|
||||
public class DfaMatcherTest
|
||||
{
|
||||
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,
|
||||
template,
|
||||
new RouteValueDictionary(defaultValues),
|
||||
new RouteValueDictionary(),
|
||||
new List<MatchProcessorReference>(),
|
||||
order,
|
||||
|
|
@ -28,18 +31,17 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
template);
|
||||
}
|
||||
|
||||
private TreeMatcher CreateTreeMatcher(EndpointDataSource endpointDataSource)
|
||||
private Matcher CreateDfaMatcher(EndpointDataSource dataSource)
|
||||
{
|
||||
var compositeDataSource = new CompositeEndpointDataSource(new[] { endpointDataSource });
|
||||
var defaultInlineConstraintResolver = new DefaultMatchProcessorFactory(
|
||||
Options.Create(new RouteOptions()),
|
||||
Mock.Of<IServiceProvider>());
|
||||
var endpointSelector = new EndpointSelector(
|
||||
compositeDataSource,
|
||||
new EndpointConstraintCache(compositeDataSource, new IEndpointConstraintProvider[] { new DefaultEndpointConstraintProvider() }),
|
||||
NullLoggerFactory.Instance);
|
||||
var services = new ServiceCollection()
|
||||
.AddLogging()
|
||||
.AddOptions()
|
||||
.AddRouting()
|
||||
.AddDispatcher()
|
||||
.BuildServiceProvider();
|
||||
|
||||
return new TreeMatcher(defaultInlineConstraintResolver, NullLogger.Instance, endpointDataSource, endpointSelector);
|
||||
var factory = services.GetRequiredService<MatcherFactory>();
|
||||
return Assert.IsType<DataSourceDependentMatcher>(factory.CreateMatcher(dataSource));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -51,7 +53,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
CreateEndpoint("/{p:int}", 0)
|
||||
});
|
||||
|
||||
var treeMatcher = CreateTreeMatcher(endpointDataSource);
|
||||
var treeMatcher = CreateDfaMatcher(endpointDataSource);
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Path = "/1";
|
||||
|
|
@ -74,7 +76,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
CreateEndpoint("/{p:int}", 0)
|
||||
});
|
||||
|
||||
var treeMatcher = CreateTreeMatcher(endpointDataSource);
|
||||
var treeMatcher = CreateDfaMatcher(endpointDataSource);
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Path = "/One";
|
||||
|
|
@ -101,7 +103,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
lowerOrderEndpoint
|
||||
});
|
||||
|
||||
var treeMatcher = CreateTreeMatcher(endpointDataSource);
|
||||
var treeMatcher = CreateDfaMatcher(endpointDataSource);
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Path = "/Teams";
|
||||
|
|
@ -131,7 +133,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
endpointWithConstraint
|
||||
});
|
||||
|
||||
var treeMatcher = CreateTreeMatcher(endpointDataSource);
|
||||
var treeMatcher = CreateDfaMatcher(endpointDataSource);
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = "POST";
|
||||
|
|
@ -8,8 +8,23 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
|||
{
|
||||
public class FastPathTokenizerTest
|
||||
{
|
||||
[Fact] // Note: tokenizing a truly empty string is undefined.
|
||||
public void Tokenize_EmptyPath()
|
||||
// Generally this will only happen in tests when the HttpContext hasn't been
|
||||
// initialized. We still don't want to crash in this case.
|
||||
[Fact]
|
||||
public void Tokenize_EmptyString()
|
||||
{
|
||||
// Arrange
|
||||
Span<PathSegment> segments = stackalloc PathSegment[1];
|
||||
|
||||
// Act
|
||||
var count = FastPathTokenizer.Tokenize("", segments);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tokenize_RootPath()
|
||||
{
|
||||
// Arrange
|
||||
Span<PathSegment> segments = stackalloc PathSegment[1];
|
||||
|
|
|
|||
Loading…
Reference in New Issue