From 736b49294d88a226f129cf8ee071d91af2c618fe Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 19 Oct 2017 08:20:15 -0700 Subject: [PATCH] Add Template abstraction This change adds the Template as a top level abstraction. URL templating is now a two-stage process. First you use a 'key' to look up a Template, then you use the Template to create the URL. This change also has some cleanup of the way RoutePatternBinder gets instantiated. I added a factory service so that most of the complex things can be made internal to Dispatcher. Now it's much easier to constuct and use. These impacts some pubternal APIs that we already broke, but makes them actually nice :) Also cleaned up some tests and fixed one that was broken and not running. --- .../DispatcherBenchmark.cs | 5 +- .../RoutingBenchmark.cs | 4 +- samples/DispatcherSample/Startup.cs | 12 +- .../AddressGroup.cs | 10 -- .../Template.cs | 27 +++++ .../BufferValue.cs | 2 +- .../DefaultTemplateFactory.cs | 49 ++++++++ .../DispatcherServiceCollectionExtensions.cs | 17 +-- .../ITemplateAddress.cs | 2 +- .../ITemplateFactoryComponent.cs | 9 ++ .../RoutePatternBinder.cs | 10 +- .../RoutePatternBinderFactory.cs | 96 +++++++++++++++ .../RoutePatternTemplate.cs | 66 +++++++++++ .../RoutePatternTemplateFactory.cs | 51 ++++++++ .../SegmentState.cs | 2 +- .../TemplateAddress.cs | 4 +- .../TemplateAddressSelector.cs | 7 +- .../TemplateFactory.cs | 15 +++ .../TemplateFactoryOfT.cs | 10 ++ .../UriBuilderContextPooledObjectPolicy.cs | 21 ---- .../UriBuildingContext.cs | 2 +- .../Dispatcher/RouteTemplateUrlGenerator.cs | 58 --------- src/Microsoft.AspNetCore.Routing/RouteBase.cs | 5 +- .../Template/TemplateBinder.cs | 24 +--- .../Tree/TreeRouteBuilder.cs | 28 +---- .../Tree/TreeRouter.cs | 26 +--- .../ApiAppStartup.cs | 1 - .../DefaultTemplateFactoryTest.cs | 64 ++++++++++ ...nderTests.cs => RoutePatternBinderTest.cs} | 112 ++++++++---------- ...herTests.cs => RoutePatternMatcherTest.cs} | 2 +- .../RoutePatternTemplateTest.cs | 66 +++++++++++ .../DateTimeRouteConstraintTests.cs | 43 ++++--- .../Template/TemplateBinderTests.cs | 43 +++---- .../Tree/TreeRouteBuilderTest.cs | 7 +- .../Tree/TreeRouterTest.cs | 15 ++- 35 files changed, 602 insertions(+), 313 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressGroup.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/Template.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/DefaultTemplateFactory.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/ITemplateFactoryComponent.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinderFactory.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplate.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplateFactory.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/TemplateFactory.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/TemplateFactoryOfT.cs delete mode 100644 src/Microsoft.AspNetCore.Dispatcher/UriBuilderContextPooledObjectPolicy.cs delete mode 100644 src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs create mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/DefaultTemplateFactoryTest.cs rename test/Microsoft.AspNetCore.Dispatcher.Test/{TemplateBinderTests.cs => RoutePatternBinderTest.cs} (97%) rename test/Microsoft.AspNetCore.Dispatcher.Test/{RoutePatternMatcherTests.cs => RoutePatternMatcherTest.cs} (99%) create mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternTemplateTest.cs diff --git a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs index 56952ddb24..a8232e0b02 100644 --- a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs @@ -2,13 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; using System.Text.Encodings.Web; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; @@ -30,8 +28,7 @@ namespace Microsoft.AspNetCore.Dispatcher.Performance var treeBuilder = new TreeRouteBuilder( NullLoggerFactory.Instance, - UrlEncoder.Default, - new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), + new RoutePatternBinderFactory(UrlEncoder.Default, new DefaultObjectPoolProvider()), new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()))); treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets"), "default", 0); diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs index 2a9298c66f..fedeb5b2e4 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; using System.Text.Encodings.Web; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -30,8 +29,7 @@ namespace Microsoft.AspNetCore.Routing.Performance var treeBuilder = new TreeRouteBuilder( NullLoggerFactory.Instance, - UrlEncoder.Default, - new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), + new RoutePatternBinderFactory(UrlEncoder.Default, new DefaultObjectPoolProvider()), new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()))); treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets"), "default", 0); diff --git a/samples/DispatcherSample/Startup.cs b/samples/DispatcherSample/Startup.cs index da026af1a2..139867abe5 100644 --- a/samples/DispatcherSample/Startup.cs +++ b/samples/DispatcherSample/Startup.cs @@ -21,7 +21,6 @@ namespace DispatcherSample // This is a temporary layering issue, don't worry about :) services.AddRouting(); - services.AddSingleton(); services.AddSingleton(); // Imagine this was done by MVC or another framework. @@ -86,15 +85,16 @@ namespace DispatcherSample public static Task Home_Index(HttpContext httpContext) { - var url = httpContext.RequestServices.GetService(); + var templateFactory = httpContext.RequestServices.GetRequiredService(); + return httpContext.Response.WriteAsync( $"" + $"" + $"

Some links you can visit

" + - $"

Home:Index()

" + - $"

Home:About()

" + - $"

Admin:Index()

" + - $"

Admin:GetUsers()/Admin:EditUsers()

" + + $"

Home:Index()

" + + $"

Home:About()

" + + $"

Admin:Index()

" + + $"

Admin:GetUsers()/Admin:EditUsers()

" + $"" + $""); } diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressGroup.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressGroup.cs deleted file mode 100644 index 3162e47b9c..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressGroup.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.AspNetCore.Dispatcher.Abstractions -{ - class AddressGroup - { - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Template.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Template.cs new file mode 100644 index 0000000000..94b56d402c --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Template.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public abstract class Template + { + public virtual string GetUrl() + { + return GetUrl(null, new DispatcherValueCollection()); + } + + public virtual string GetUrl(HttpContext httpContext) + { + return GetUrl(httpContext, new DispatcherValueCollection()); + } + + public virtual string GetUrl(DispatcherValueCollection values) + { + return GetUrl(null, values); + } + + public abstract string GetUrl(HttpContext httpContext, DispatcherValueCollection values); + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/BufferValue.cs b/src/Microsoft.AspNetCore.Dispatcher/BufferValue.cs index 40ad3b6047..c4f0031c96 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/BufferValue.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/BufferValue.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.Dispatcher { - public struct BufferValue + internal struct BufferValue { public BufferValue(string value, bool requiresEncoding) { diff --git a/src/Microsoft.AspNetCore.Dispatcher/DefaultTemplateFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/DefaultTemplateFactory.cs new file mode 100644 index 0000000000..e984e08683 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/DefaultTemplateFactory.cs @@ -0,0 +1,49 @@ +// 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; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public class DefaultTemplateFactory : TemplateFactory + { + private readonly ITemplateFactoryComponent[] _components; + + public DefaultTemplateFactory(IEnumerable components) + { + if (components == null) + { + throw new ArgumentNullException(nameof(components)); + } + + _components = components.ToArray(); + } + + public override Template GetTemplateFromKey(TKey key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + for (var i = 0; i < _components.Length; i++) + { + var component = _components[i] as TemplateFactory; + if (component == null) + { + continue; + } + + var template = component.GetTemplate(key); + if (template != null) + { + return template; + } + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherServiceCollectionExtensions.cs index 45c6c8a18b..58444b4357 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherServiceCollectionExtensions.cs @@ -25,17 +25,18 @@ namespace Microsoft.Extensions.DependencyInjection // Adds a default dispatcher which will collect all data sources and endpoint selectors from DI. services.TryAddEnumerable(ServiceDescriptor.Transient, DefaultDispatcherConfigureOptions>()); - services.AddSingleton(); - services.AddSingleton(); + // + // Addresses + Templates + // + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); // - // Infrastructure + // Misc Infrastructure // - services.AddSingleton>(s => - { - var provider = s.GetRequiredService(); - return provider.Create(new UriBuilderContextPooledObjectPolicy()); - }); + services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/Microsoft.AspNetCore.Dispatcher/ITemplateAddress.cs b/src/Microsoft.AspNetCore.Dispatcher/ITemplateAddress.cs index dc4697b29e..71fd9e4b1d 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/ITemplateAddress.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/ITemplateAddress.cs @@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Dispatcher { string Template { get; } - DispatcherValueCollection Values { get; } + DispatcherValueCollection Defaults { get; } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/ITemplateFactoryComponent.cs b/src/Microsoft.AspNetCore.Dispatcher/ITemplateFactoryComponent.cs new file mode 100644 index 0000000000..071f3e1913 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/ITemplateFactoryComponent.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Dispatcher +{ + public interface ITemplateFactoryComponent + { + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs index 41f6fb8e5a..b0e272d1f3 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinder.cs @@ -20,10 +20,10 @@ namespace Microsoft.AspNetCore.Dispatcher private readonly DispatcherValueCollection _filters; private readonly RoutePattern _pattern; - public RoutePatternBinder( + internal RoutePatternBinder( UrlEncoder urlEncoder, ObjectPool pool, - RoutePattern template, + RoutePattern pattern, DispatcherValueCollection defaults) { if (urlEncoder == null) @@ -36,14 +36,14 @@ namespace Microsoft.AspNetCore.Dispatcher throw new ArgumentNullException(nameof(pool)); } - if (template == null) + if (pattern == null) { - throw new ArgumentNullException(nameof(template)); + throw new ArgumentNullException(nameof(pattern)); } _urlEncoder = urlEncoder; _pool = pool; - _pattern = template; + _pattern = pattern; _defaults = defaults; // Any default that doesn't have a corresponding parameter is a 'filter' and if a value diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinderFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinderFactory.cs new file mode 100644 index 0000000000..51829e6745 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternBinderFactory.cs @@ -0,0 +1,96 @@ +// 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.Text.Encodings.Web; +using Microsoft.AspNetCore.Dispatcher.Patterns; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public class RoutePatternBinderFactory + { + private readonly UrlEncoder _encoder; + private readonly ObjectPool _pool; + + public RoutePatternBinderFactory(UrlEncoder encoder, ObjectPoolProvider objectPoolProvider) + { + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + if (objectPoolProvider == null) + { + throw new ArgumentNullException(nameof(objectPoolProvider)); + } + + _encoder = encoder; + _pool = objectPoolProvider.Create(new UriBuilderContextPooledObjectPolicy()); + } + + public RoutePatternBinder Create(string pattern) + { + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } + + return Create(RoutePattern.Parse(pattern), new DispatcherValueCollection()); + } + + public RoutePatternBinder Create(string pattern, DispatcherValueCollection defaults) + { + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } + + if (defaults == null) + { + throw new ArgumentNullException(nameof(defaults)); + } + + return Create(RoutePattern.Parse(pattern), defaults); + } + + public RoutePatternBinder Create(RoutePattern pattern) + { + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } + + return Create(pattern, new DispatcherValueCollection()); + } + + public RoutePatternBinder Create(RoutePattern pattern, DispatcherValueCollection defaults) + { + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } + + if (defaults == null) + { + throw new ArgumentNullException(nameof(defaults)); + } + + return new RoutePatternBinder(_encoder, _pool, pattern, defaults); + } + + private class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy + { + public UriBuildingContext Create() + { + return new UriBuildingContext(); + } + + public bool Return(UriBuildingContext obj) + { + obj.Clear(); + return true; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplate.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplate.cs new file mode 100644 index 0000000000..31327d6c72 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplate.cs @@ -0,0 +1,66 @@ +// 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.Http; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public class RoutePatternTemplate : Template + { + private readonly RoutePatternBinder _binder; + + public RoutePatternTemplate(RoutePatternBinder binder) + { + if (binder == null) + { + throw new ArgumentNullException(nameof(binder)); + } + + _binder = binder; + } + + public override string GetUrl(DispatcherValueCollection values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + return GetUrl(null, values); + } + + public override string GetUrl(HttpContext httpContext, DispatcherValueCollection values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + var ambientValues = GetAmbientValues(httpContext); + var result = _binder.GetValues(ambientValues, values); + if (result.acceptedValues == null) + { + return null; + } + + return _binder.BindValues(result.acceptedValues); + } + + private DispatcherValueCollection GetAmbientValues(HttpContext httpContext) + { + if (httpContext == null) + { + return new DispatcherValueCollection(); + } + + var feature = httpContext.Features.Get(); + if (feature == null) + { + return new DispatcherValueCollection(); + } + + return feature.Values; + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplateFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplateFactory.cs new file mode 100644 index 0000000000..4372cb4726 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternTemplateFactory.cs @@ -0,0 +1,51 @@ +// 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; + +namespace Microsoft.AspNetCore.Dispatcher +{ + internal class RoutePatternTemplateFactory : TemplateFactory + { + private readonly TemplateAddressSelector _selector; + private readonly RoutePatternBinderFactory _binderFactory; + + public RoutePatternTemplateFactory(TemplateAddressSelector selector, RoutePatternBinderFactory binderFactory) + { + if (selector == null) + { + throw new ArgumentNullException(nameof(selector)); + } + + if (binderFactory == null) + { + throw new ArgumentNullException(nameof(binderFactory)); + } + + _selector = selector; + _binderFactory = binderFactory; + } + + public override Template GetTemplate(DispatcherValueCollection key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var address = _selector.SelectAddress(key); + if (address == null) + { + return null; + } + + if (address is ITemplateAddress templateAddress) + { + var binder = _binderFactory.Create(templateAddress.Template, templateAddress.Defaults); + return new RoutePatternTemplate(binder); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/SegmentState.cs b/src/Microsoft.AspNetCore.Dispatcher/SegmentState.cs index 9aa4469a27..19843b71bd 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/SegmentState.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/SegmentState.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Dispatcher // used a value for {p1}, we have to output the entire segment up to the next "/". // Otherwise we could end up with the partial segment "v1" instead of the entire // segment "v1-v2.xml". - public enum SegmentState + internal enum SegmentState { Beginning, Inside, diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplateAddress.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplateAddress.cs index 606f9997f2..3053d0d936 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/TemplateAddress.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/TemplateAddress.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Dispatcher } Template = template; - Values = new DispatcherValueCollection(values); + Defaults = new DispatcherValueCollection(values); DisplayName = displayName; Metadata = metadata.ToArray(); } @@ -38,6 +38,6 @@ namespace Microsoft.AspNetCore.Dispatcher public string Template { get; } - public DispatcherValueCollection Values { get; } + public DispatcherValueCollection Defaults { get; } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplateAddressSelector.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplateAddressSelector.cs index efb405f6bf..4ce2d6a43f 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/TemplateAddressSelector.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/TemplateAddressSelector.cs @@ -21,6 +21,11 @@ namespace Microsoft.AspNetCore.Dispatcher _addressTable = addressTable; } + public Address SelectAddress(object values) + { + return SelectAddress(new DispatcherValueCollection(values)); + } + public Address SelectAddress(DispatcherValueCollection values) { if (values == null) @@ -69,7 +74,7 @@ namespace Microsoft.AspNetCore.Dispatcher private bool IsMatch(ITemplateAddress address, DispatcherValueCollection values) { - foreach (var kvp in address.Values) + foreach (var kvp in address.Defaults) { values.TryGetValue(kvp.Key, out var value); diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplateFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplateFactory.cs new file mode 100644 index 0000000000..5fdbabc93b --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/TemplateFactory.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Dispatcher +{ + public abstract class TemplateFactory + { + public Template GetTemplate(object values) + { + return GetTemplateFromKey(new DispatcherValueCollection(values)); + } + + public abstract Template GetTemplateFromKey(TKey key) where TKey : class; + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/TemplateFactoryOfT.cs b/src/Microsoft.AspNetCore.Dispatcher/TemplateFactoryOfT.cs new file mode 100644 index 0000000000..10ddc0cf1a --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/TemplateFactoryOfT.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Dispatcher +{ + public abstract class TemplateFactory : ITemplateFactoryComponent + { + public abstract Template GetTemplate(TKey key); + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/UriBuilderContextPooledObjectPolicy.cs b/src/Microsoft.AspNetCore.Dispatcher/UriBuilderContextPooledObjectPolicy.cs deleted file mode 100644 index 0e44bcb3f8..0000000000 --- a/src/Microsoft.AspNetCore.Dispatcher/UriBuilderContextPooledObjectPolicy.cs +++ /dev/null @@ -1,21 +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 Microsoft.Extensions.ObjectPool; - -namespace Microsoft.AspNetCore.Dispatcher -{ - public class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy - { - public UriBuildingContext Create() - { - return new UriBuildingContext(); - } - - public bool Return(UriBuildingContext obj) - { - obj.Clear(); - return true; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher/UriBuildingContext.cs b/src/Microsoft.AspNetCore.Dispatcher/UriBuildingContext.cs index d9b58a88fa..55ebc8b304 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/UriBuildingContext.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/UriBuildingContext.cs @@ -10,7 +10,7 @@ using System.Text.Encodings.Web; namespace Microsoft.AspNetCore.Dispatcher { [DebuggerDisplay("{DebuggerToString(),nq}")] - public class UriBuildingContext + internal class UriBuildingContext { // Holds the 'accepted' parts of the uri. private readonly StringBuilder _uri; diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs deleted file mode 100644 index 7f4e631e5f..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs +++ /dev/null @@ -1,58 +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.Text.Encodings.Web; -using Microsoft.AspNetCore.Dispatcher; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.Internal; -using Microsoft.AspNetCore.Routing.Template; -using Microsoft.Extensions.ObjectPool; - -namespace Microsoft.AspNetCore.Routing.Dispatcher -{ - // This isn't a proposed design, just a placeholder to demonstrate that things are wired up correctly. - public class RouteTemplateUrlGenerator - { - private readonly TemplateAddressSelector _addressSelector; - private readonly ObjectPool _pool; - private readonly UrlEncoder _urlEncoder; - - public RouteTemplateUrlGenerator(TemplateAddressSelector addressSelector, UrlEncoder urlEncoder, ObjectPool pool) - { - _addressSelector = addressSelector; - _urlEncoder = urlEncoder; - _pool = pool; - } - - public string GenerateUrl(HttpContext httpContext, object values) - { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - var address = _addressSelector.SelectAddress(new DispatcherValueCollection(values)) as ITemplateAddress; - if (address == null) - { - throw new InvalidOperationException("Can't find address"); - } - - var binder = new TemplateBinder(_urlEncoder, _pool, Template.TemplateParser.Parse(address.Template), new RouteValueDictionary()); - - var feature = httpContext.Features.Get(); - var result = binder.GetValues(feature.Values.AsRouteValueDictionary(), new RouteValueDictionary(values)); - if (result == null) - { - return null; - } - - return binder.BindValues(result.AcceptedValues); - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/RouteBase.cs b/src/Microsoft.AspNetCore.Routing/RouteBase.cs index 9e4eaf530e..90ef1b05e9 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteBase.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteBase.cs @@ -242,9 +242,8 @@ namespace Microsoft.AspNetCore.Routing { if (_binder == null) { - var urlEncoder = context.RequestServices.GetRequiredService(); - var pool = context.RequestServices.GetRequiredService>(); - _binder = new TemplateBinder(urlEncoder, pool, ParsedTemplate, Defaults); + var binderFactory = context.RequestServices.GetRequiredService(); + _binder = new TemplateBinder(binderFactory.Create(ParsedTemplate.ToRoutePattern(), Defaults)); } } diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs index b20f461aa5..48364b352b 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Text.Encodings.Web; using Microsoft.AspNetCore.Dispatcher; -using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNetCore.Routing.Template { @@ -12,28 +10,14 @@ namespace Microsoft.AspNetCore.Routing.Template { private readonly RoutePatternBinder _binder; - public TemplateBinder( - UrlEncoder urlEncoder, - ObjectPool pool, - RouteTemplate template, - RouteValueDictionary defaults) + public TemplateBinder(RoutePatternBinder binder) { - if (urlEncoder == null) + if (binder == null) { - throw new ArgumentNullException(nameof(urlEncoder)); + throw new ArgumentNullException(nameof(binder)); } - if (pool == null) - { - throw new ArgumentNullException(nameof(pool)); - } - - if (template == null) - { - throw new ArgumentNullException(nameof(template)); - } - - _binder = new RoutePatternBinder(urlEncoder, pool, template.ToRoutePattern(), defaults); + _binder = binder; } // Step 1: Get the list of values we're going to try to use to match and generate this URI diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs index 622f099604..f44aebf160 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs @@ -20,21 +20,12 @@ namespace Microsoft.AspNetCore.Routing.Tree { private readonly ILogger _logger; private readonly ILogger _constraintLogger; - private readonly UrlEncoder _urlEncoder; - private readonly ObjectPool _objectPool; + private readonly RoutePatternBinderFactory _binderFactory; private readonly IInlineConstraintResolver _constraintResolver; - /// - /// Initializes a new instance of . - /// - /// The . - /// The . - /// The . - /// The . public TreeRouteBuilder( ILoggerFactory loggerFactory, - UrlEncoder urlEncoder, - ObjectPool objectPool, + RoutePatternBinderFactory binderFactory, IInlineConstraintResolver constraintResolver) { if (loggerFactory == null) @@ -42,14 +33,9 @@ namespace Microsoft.AspNetCore.Routing.Tree throw new ArgumentNullException(nameof(loggerFactory)); } - if (urlEncoder == null) + if (binderFactory == null) { - throw new ArgumentNullException(nameof(urlEncoder)); - } - - if (objectPool == null) - { - throw new ArgumentNullException(nameof(objectPool)); + throw new ArgumentNullException(nameof(binderFactory)); } if (constraintResolver == null) @@ -57,8 +43,7 @@ namespace Microsoft.AspNetCore.Routing.Tree throw new ArgumentNullException(nameof(constraintResolver)); } - _urlEncoder = urlEncoder; - _objectPool = objectPool; + _binderFactory = binderFactory; _constraintResolver = constraintResolver; _logger = loggerFactory.CreateLogger(); @@ -251,8 +236,7 @@ namespace Microsoft.AspNetCore.Routing.Tree return new TreeRouter( trees.Values.OrderBy(tree => tree.Order).ToArray(), OutboundEntries, - _urlEncoder, - _objectPool, + _binderFactory, _logger, _constraintLogger, version); diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs index 5f9bc989ed..e7ab54341e 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs @@ -33,22 +33,10 @@ namespace Microsoft.AspNetCore.Routing.Tree private readonly ILogger _logger; private readonly ILogger _constraintLogger; - /// - /// Creates a new . - /// - /// The list of that contains the route entries. - /// The set of . - /// The . - /// The . - /// The instance. - /// The instance used - /// in . - /// The version of this route. public TreeRouter( UrlMatchingTree[] trees, IEnumerable linkGenerationEntries, - UrlEncoder urlEncoder, - ObjectPool objectPool, + RoutePatternBinderFactory binderFactory, ILogger routeLogger, ILogger constraintLogger, int version) @@ -63,14 +51,9 @@ namespace Microsoft.AspNetCore.Routing.Tree throw new ArgumentNullException(nameof(linkGenerationEntries)); } - if (urlEncoder == null) + if (binderFactory == null) { - throw new ArgumentNullException(nameof(urlEncoder)); - } - - if (objectPool == null) - { - throw new ArgumentNullException(nameof(objectPool)); + throw new ArgumentNullException(nameof(binderFactory)); } if (routeLogger == null) @@ -93,8 +76,7 @@ namespace Microsoft.AspNetCore.Routing.Tree foreach (var entry in linkGenerationEntries) { - - var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults); + var binder = new TemplateBinder(binderFactory.Create(entry.RouteTemplate.ToRoutePattern(), entry.Defaults)); var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder }; outboundMatches.Add(outboundMatch); diff --git a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs index 1b3d707636..10fd842549 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs @@ -20,7 +20,6 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest // This is a temporary layering issue, don't worry about it :) services.AddRouting(); - services.AddSingleton(); services.AddSingleton(); services.Configure(ConfigureDispatcher); diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/DefaultTemplateFactoryTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/DefaultTemplateFactoryTest.cs new file mode 100644 index 0000000000..90ab9ad210 --- /dev/null +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/DefaultTemplateFactoryTest.cs @@ -0,0 +1,64 @@ +// 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 Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public class DefaultTemplateFactoryTest + { + [Fact] + public void GetTemplateFromKey_UsesMatchingComponent_SelectsTemplate() + { + // Arrange + var expected = Mock.Of