diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TreeRouterMatcher.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TreeRouterMatcher.cs index bdce271..4346eb7 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TreeRouterMatcher.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/TreeRouterMatcher.cs @@ -41,7 +41,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers if (context.Handler != null) { - httpContext.Features.Set(feature); await context.Handler(httpContext); } } diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpoint.cs b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpoint.cs index 6941597..d371afc 100644 --- a/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpoint.cs +++ b/src/Microsoft.AspNetCore.Routing/Matchers/MatcherEndpoint.cs @@ -3,12 +3,18 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matchers { public sealed class MatcherEndpoint : Endpoint { + internal static readonly Func EmptyInvoker = (next) => + { + return (context) => Task.CompletedTask; + }; + public MatcherEndpoint( Func invoker, string template, diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DispatcherAssert.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DispatcherAssert.cs new file mode 100644 index 0000000..b6c7bdd --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DispatcherAssert.cs @@ -0,0 +1,54 @@ +// 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.Linq; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + internal static class DispatcherAssert + { + public static void AssertMatch(IEndpointFeature feature, Endpoint expected) + { + AssertMatch(feature, expected, new RouteValueDictionary()); + } + + public static void AssertMatch(IEndpointFeature feature, Endpoint expected, RouteValueDictionary values) + { + if (feature.Endpoint == null) + { + throw new XunitException($"Was expected to match '{expected.DisplayName}' but did not match."); + } + + if (!object.ReferenceEquals(expected, feature.Endpoint)) + { + throw new XunitException( + $"Was expected to match '{expected.DisplayName}' but matched " + + $"'{feature.Endpoint.DisplayName}' with values: {FormatRouteValues(feature.Values)}."); + } + + if (values.Count != feature.Values.Count || + !values.OrderBy(kvp => kvp.Key).SequenceEqual(feature.Values.OrderBy(kvp => kvp.Key))) + { + throw new XunitException( + $"Was expected to match '{expected.DisplayName}' with values {FormatRouteValues(values)} but matched " + + $"values: {FormatRouteValues(feature.Values)}."); + } + } + + public static void AssertNotMatch(IEndpointFeature feature) + { + if (feature.Endpoint != null) + { + throw new XunitException( + $"Was expected not to match '{feature.Endpoint.DisplayName}' " + + $"but matched with values: {FormatRouteValues(feature.Values)}."); + } + } + + private static string FormatRouteValues(RouteValueDictionary values) + { + return "{" + string.Join(", ", values.Select(kvp => $"{kvp.Key} = '{kvp.Value}'")) + "}"; + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs new file mode 100644 index 0000000..a0b2fca --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs @@ -0,0 +1,84 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public abstract class MatcherConformanceTest + { + internal abstract Matcher CreateMatcher(MatcherEndpoint endpoint); + + [Fact] + public virtual async Task Match_SingleLiteralSegment_Success() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/simple"); + var (httpContext, feature) = CreateContext("/simple"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + DispatcherAssert.AssertMatch(feature, endpoint); + } + + [Theory] + [InlineData("simple")] + [InlineData("/simple")] + [InlineData("~/simple")] + public virtual async Task Match_Sanitizies_TemplatePrefix(string template) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var (httpContext, feature) = CreateContext("/simple"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + DispatcherAssert.AssertMatch(feature, endpoint); + } + + internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(string path) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "TEST"; + httpContext.Request.Path = path; + httpContext.RequestServices = CreateServices(); + + var feature = new EndpointFeature(); + httpContext.Features.Set(feature); + + return (httpContext, feature); + } + + // The older routing implementations retrieve services when they first execute. + internal static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddLogging(); + return services.BuildServiceProvider(); + } + + internal static MatcherEndpoint CreateEndpoint(string template) + { + return new MatcherEndpoint( + MatcherEndpoint.EmptyInvoker, + template, + null, + 0, + EndpointMetadataCollection.Empty, "endpoint: " + template); + } + + internal (Matcher matcher, MatcherEndpoint endpoint) CreateMatcher(string template) + { + var endpoint = CreateEndpoint(template); + return (CreateMatcher(endpoint), endpoint); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcher.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcher.cs new file mode 100644 index 0000000..1cb70e6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcher.cs @@ -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 System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Tree; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + // This is an adapter to use TreeRouter in the conformance tests + internal class TreeRouterMatcher : Matcher + { + private readonly TreeRouter _inner; + + internal TreeRouterMatcher(TreeRouter inner) + { + _inner = inner; + } + + public async override Task MatchAsync(HttpContext httpContext, IEndpointFeature feature) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (feature == null) + { + throw new ArgumentNullException(nameof(feature)); + } + + var context = new RouteContext(httpContext); + await _inner.RouteAsync(context); + + if (context.Handler != null) + { + httpContext.Features.Get().Values = context.RouteData.Values; + await context.Handler(httpContext); + } + } + } +} + diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherConformanceTest.cs new file mode 100644 index 0000000..45f04ad --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherConformanceTest.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.Threading.Tasks; +using Microsoft.AspNetCore.Routing.Internal; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.AspNetCore.Routing.Tree; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class TreeRouterMatcherConformanceTest : MatcherConformanceTest + { + internal override Matcher CreateMatcher(MatcherEndpoint endpoint) + { + var builder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), + new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()))); + + var handler = new RouteHandler(c => + { + var feature = c.Features.Get(); + feature.Endpoint = endpoint; + feature.Invoker = MatcherEndpoint.EmptyInvoker; + + return Task.CompletedTask; + }); + + builder.MapInbound(handler, TemplateParser.Parse(endpoint.Template), "default", 0); + + return new TreeRouterMatcher(builder.Build()); + } + } +}