Make DFA matcher the default

This commit is contained in:
Ryan Nowak 2018-07-17 20:28:51 -07:00
parent 400d243f42
commit 477296a3cc
15 changed files with 341 additions and 435 deletions

View File

@ -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)

View File

@ -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()
{

View File

@ -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)
{

View File

@ -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>();

View File

@ -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();
}
}
}

View File

@ -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>();
});
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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; }
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -30,7 +30,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
.AddOptions()
.AddRouting()
.AddDispatcher()
.AddTransient<DfaMatcherBuilder>()
.BuildServiceProvider();
var builder = services.GetRequiredService<DfaMatcherBuilder>();

View File

@ -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";

View File

@ -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];