diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs index f13c36e9d2..fb12d507a7 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/MatcherBenchmarkBase.cs @@ -30,8 +30,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers private protected DfaMatcherBuilder CreateDfaMatcherBuilder() { - var services = CreateServices(); - return ActivatorUtilities.CreateInstance(services); + return CreateServices().GetRequiredService(); } private protected static MatcherEndpoint CreateEndpoint(string template, string httpMethod = null) diff --git a/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs index 75e5360d47..a0009a7091 100644 --- a/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs @@ -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() { diff --git a/src/Microsoft.AspNetCore.Routing/Internal/DataSourceDependantCache.cs b/src/Microsoft.AspNetCore.Routing/DataSourceDependentCache.cs similarity index 91% rename from src/Microsoft.AspNetCore.Routing/Internal/DataSourceDependantCache.cs rename to src/Microsoft.AspNetCore.Routing/DataSourceDependentCache.cs index 4be08e1402..46ecba5fd2 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/DataSourceDependantCache.cs +++ b/src/Microsoft.AspNetCore.Routing/DataSourceDependentCache.cs @@ -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 where T : class + internal class DataSourceDependentCache where T : class { private readonly EndpointDataSource _dataSource; private readonly Func, T> _initializeCore; @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Routing.Internal private bool _initialized; private T _value; - public DataSourceDependantCache(EndpointDataSource dataSource, Func, T> initialize) + public DataSourceDependentCache(EndpointDataSource dataSource, Func, T> initialize) { if (dataSource == null) { diff --git a/src/Microsoft.AspNetCore.Routing/DependencyInjection/DispatcherServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/DependencyInjection/DispatcherServiceCollectionExtensions.cs index b17150e784..8fb75c8351 100644 --- a/src/Microsoft.AspNetCore.Routing/DependencyInjection/DispatcherServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/DependencyInjection/DispatcherServiceCollectionExtensions.cs @@ -36,7 +36,8 @@ namespace Microsoft.Extensions.DependencyInjection // Default matcher implementation // services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddTransient(); // Link generation related services services.TryAddSingleton, NameBasedEndpointFinder>(); diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DataSourceDependentMatcher.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DataSourceDependentMatcher.cs new file mode 100644 index 0000000000..07f9361a16 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DataSourceDependentMatcher.cs @@ -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 _matcherBuilderFactory; + private readonly DataSourceDependentCache _cache; + + public DataSourceDependentMatcher( + EndpointDataSource dataSource, + Func matcherBuilderFactory) + { + _matcherBuilderFactory = matcherBuilderFactory; + + _cache = new DataSourceDependentCache(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 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(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherFactory.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherFactory.cs new file mode 100644 index 0000000000..2e5dc8a3f2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DfaMatcherFactory.cs @@ -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(); + }); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/FastPathTokenizer.cs b/src/Microsoft.AspNetCore.Routing/Matchers/FastPathTokenizer.cs index 98ed984795..f561ae0ee2 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/FastPathTokenizer.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/FastPathTokenizer.cs @@ -26,6 +26,12 @@ namespace Microsoft.AspNetCore.Routing.Matchers // computed based on the string length. public static int Tokenize(string path, Span 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; diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs index f6cca216ee..a9e3a6d3b8 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherBuilderEntry.cs @@ -14,14 +14,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers { Endpoint = endpoint; - HttpMethod = endpoint.Metadata.OfType().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; diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/TreeMatcher.cs b/src/Microsoft.AspNetCore.Routing/Matchers/TreeMatcher.cs deleted file mode 100644 index 72fc5c9f6c..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Matchers/TreeMatcher.cs +++ /dev/null @@ -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 _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(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 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 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 endpoints) - { - var groups = new Dictionary>(); - - 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(); - groups.Add(new Key(order, endpoint.Template), group); - } - - group.Add(endpoint); - } - - var entries = new List(); - 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(); - 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(); - 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 - { - 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 _matchSuccess = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1, "MatchSuccess"), - "Request matched endpoint '{EndpointName}' for request path '{Path}'."); - - private static readonly Action _matchFailed = LoggerMessage.Define( - LogLevel.Debug, - new EventId(2, "MatchFailed"), - "No endpoints matched request path '{Path}'."); - - private static readonly Action, Exception> _matchAmbiguous = LoggerMessage.Define>( - LogLevel.Error, - new EventId(3, "MatchAmbiguous"), - "Request matched multiple endpoints for request path '{Path}'. Matching endpoints: {AmbiguousEndpoints}"); - - private static readonly Action _constraintFailed = LoggerMessage.Define( - LogLevel.Debug, - new EventId(4, "ContraintFailed"), - "Route value '{RouteValue}' with key '{RouteKey}' did not match the constraint '{RouteConstraint}'."); - - private static readonly Action _matchedTemplate = LoggerMessage.Define( - 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 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 MatchProcessors { get; set; } - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/TreeMatcherFactory.cs b/src/Microsoft.AspNetCore.Routing/Matchers/TreeMatcherFactory.cs deleted file mode 100644 index 39171909a2..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Matchers/TreeMatcherFactory.cs +++ /dev/null @@ -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 _logger; - private readonly EndpointSelector _endpointSelector; - - public TreeMatcherFactory( - MatchProcessorFactory matchProcessorFactory, - ILogger 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); - } - } -} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DataSourceDependentCacheTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DataSourceDependentCacheTest.cs new file mode 100644 index 0000000000..33da589661 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/DataSourceDependentCacheTest.cs @@ -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(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(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(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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DataSourceDependentMatcherTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DataSourceDependentMatcherTest.cs new file mode 100644 index 0000000000..e165f65cb5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DataSourceDependentMatcherTest.cs @@ -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(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(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(matcher.CurrentMatcher); + Assert.Empty(inner.Endpoints); + } + + [Fact] + public void Cache_Reinitializes_WhenDataSourceChanges() + { + // Arrange + var count = 0; + + var dataSource = new DynamicEndpointDataSource(); + var cache = new DataSourceDependentCache(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 Create = () => new TestMatcherBuilder(); + + private List Endpoints { get; } = new List(); + + public override void AddEndpoint(MatcherEndpoint endpoint) + { + Endpoints.Add(endpoint); + } + + public override Matcher Build() + { + return new TestMatcher() { Endpoints = Endpoints, }; + } + } + + private class TestMatcher : Matcher + { + public IReadOnlyList Endpoints { get; set; } + + public override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherConformanceTest.cs index d8e05d3ae8..5d3893c653 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherConformanceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherConformanceTest.cs @@ -30,7 +30,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers .AddOptions() .AddRouting() .AddDispatcher() - .AddTransient() .BuildServiceProvider(); var builder = services.GetRequiredService(); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeMatcherTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs similarity index 76% rename from test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeMatcherTests.cs rename to test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs index 30639f9e57..7157147960 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeMatcherTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherTest.cs @@ -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(), 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()); - 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(); + return Assert.IsType(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"; diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/FastPathTokenizerTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/FastPathTokenizerTest.cs index 325e1c5489..121ed45b0e 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/FastPathTokenizerTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/FastPathTokenizerTest.cs @@ -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 segments = stackalloc PathSegment[1]; + + // Act + var count = FastPathTokenizer.Tokenize("", segments); + + // Assert + Assert.Equal(0, count); + } + + [Fact] + public void Tokenize_RootPath() { // Arrange Span segments = stackalloc PathSegment[1];