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.
This commit is contained in:
Ryan Nowak 2017-10-19 08:20:15 -07:00
parent 2d661396df
commit 736b49294d
35 changed files with 602 additions and 313 deletions

View File

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

View File

@ -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<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
new RoutePatternBinderFactory(UrlEncoder.Default, new DefaultObjectPoolProvider()),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets"), "default", 0);

View File

@ -21,7 +21,6 @@ namespace DispatcherSample
// This is a temporary layering issue, don't worry about :)
services.AddRouting();
services.AddSingleton<RouteTemplateUrlGenerator>();
services.AddSingleton<IDefaultMatcherFactory, TreeMatcherFactory>();
// 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<RouteTemplateUrlGenerator>();
var templateFactory = httpContext.RequestServices.GetRequiredService<TemplateFactory>();
return httpContext.Response.WriteAsync(
$"<html>" +
$"<body>" +
$"<h1>Some links you can visit</h1>" +
$"<p><a href=\"{url.GenerateUrl(httpContext, new { controller = "Home", action = "Index", })}\">Home:Index()</a></p>" +
$"<p><a href=\"{url.GenerateUrl(httpContext, new { controller = "Home", action = "About", })}\">Home:About()</a></p>" +
$"<p><a href=\"{url.GenerateUrl(httpContext, new { controller = "Admin", action = "Index", })}\">Admin:Index()</a></p>" +
$"<p><a href=\"{url.GenerateUrl(httpContext, new { controller = "Admin", action = "Users", })}\">Admin:GetUsers()/Admin:EditUsers()</a></p>" +
$"<p><a href=\"{templateFactory.GetTemplate(new { controller = "Home", action = "Index", }).GetUrl(httpContext)}\">Home:Index()</a></p>" +
$"<p><a href=\"{templateFactory.GetTemplate(new { controller = "Home", action = "About", }).GetUrl(httpContext)}\">Home:About()</a></p>" +
$"<p><a href=\"{templateFactory.GetTemplate(new { controller = "Admin", action = "Index", }).GetUrl(httpContext)}\">Admin:Index()</a></p>" +
$"<p><a href=\"{templateFactory.GetTemplate(new { controller = "Admin", action = "Users", }).GetUrl(httpContext)}\">Admin:GetUsers()/Admin:EditUsers()</a></p>" +
$"</body>" +
$"</html>");
}

View File

@ -1,10 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.AspNetCore.Dispatcher.Abstractions
{
class AddressGroup
{
}
}

View File

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

View File

@ -3,7 +3,7 @@
namespace Microsoft.AspNetCore.Dispatcher
{
public struct BufferValue
internal struct BufferValue
{
public BufferValue(string value, bool requiresEncoding)
{

View File

@ -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<ITemplateFactoryComponent> components)
{
if (components == null)
{
throw new ArgumentNullException(nameof(components));
}
_components = components.ToArray();
}
public override Template GetTemplateFromKey<TKey>(TKey key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
for (var i = 0; i < _components.Length; i++)
{
var component = _components[i] as TemplateFactory<TKey>;
if (component == null)
{
continue;
}
var template = component.GetTemplate(key);
if (template != null)
{
return template;
}
}
return null;
}
}
}

View File

@ -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<IConfigureOptions<DispatcherOptions>, DefaultDispatcherConfigureOptions>());
services.AddSingleton<AddressTable, DefaultAddressTable>();
services.AddSingleton<TemplateAddressSelector>();
//
// Addresses + Templates
//
services.TryAddSingleton<AddressTable, DefaultAddressTable>();
services.TryAddSingleton<TemplateFactory, DefaultTemplateFactory>();
services.TryAddSingleton<ITemplateFactoryComponent, RoutePatternTemplateFactory>();
services.TryAddSingleton<TemplateAddressSelector>();
//
// Infrastructure
// Misc Infrastructure
//
services.AddSingleton<ObjectPool<UriBuildingContext>>(s =>
{
var provider = s.GetRequiredService<ObjectPoolProvider>();
return provider.Create<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy());
});
services.TryAddSingleton<RoutePatternBinderFactory>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHandlerFactory, TemplateEndpointHandlerFactory>());

View File

@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Dispatcher
{
string Template { get; }
DispatcherValueCollection Values { get; }
DispatcherValueCollection Defaults { get; }
}
}

View File

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

View File

@ -20,10 +20,10 @@ namespace Microsoft.AspNetCore.Dispatcher
private readonly DispatcherValueCollection _filters;
private readonly RoutePattern _pattern;
public RoutePatternBinder(
internal RoutePatternBinder(
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> 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

View File

@ -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<UriBuildingContext> _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<UriBuildingContext>
{
public UriBuildingContext Create()
{
return new UriBuildingContext();
}
public bool Return(UriBuildingContext obj)
{
obj.Clear();
return true;
}
}
}
}

View File

@ -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<IDispatcherFeature>();
if (feature == null)
{
return new DispatcherValueCollection();
}
return feature.Values;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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>(TKey key) where TKey : class;
}
}

View File

@ -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<TKey> : ITemplateFactoryComponent
{
public abstract Template GetTemplate(TKey key);
}
}

View File

@ -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<UriBuildingContext>
{
public UriBuildingContext Create()
{
return new UriBuildingContext();
}
public bool Return(UriBuildingContext obj)
{
obj.Clear();
return true;
}
}
}

View File

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

View File

@ -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<UriBuildingContext> _pool;
private readonly UrlEncoder _urlEncoder;
public RouteTemplateUrlGenerator(TemplateAddressSelector addressSelector, UrlEncoder urlEncoder, ObjectPool<UriBuildingContext> 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<IDispatcherFeature>();
var result = binder.GetValues(feature.Values.AsRouteValueDictionary(), new RouteValueDictionary(values));
if (result == null)
{
return null;
}
return binder.BindValues(result.AcceptedValues);
}
}
}

View File

@ -242,9 +242,8 @@ namespace Microsoft.AspNetCore.Routing
{
if (_binder == null)
{
var urlEncoder = context.RequestServices.GetRequiredService<UrlEncoder>();
var pool = context.RequestServices.GetRequiredService<ObjectPool<UriBuildingContext>>();
_binder = new TemplateBinder(urlEncoder, pool, ParsedTemplate, Defaults);
var binderFactory = context.RequestServices.GetRequiredService<RoutePatternBinderFactory>();
_binder = new TemplateBinder(binderFactory.Create(ParsedTemplate.ToRoutePattern(), Defaults));
}
}

View File

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

View File

@ -20,21 +20,12 @@ namespace Microsoft.AspNetCore.Routing.Tree
{
private readonly ILogger _logger;
private readonly ILogger _constraintLogger;
private readonly UrlEncoder _urlEncoder;
private readonly ObjectPool<UriBuildingContext> _objectPool;
private readonly RoutePatternBinderFactory _binderFactory;
private readonly IInlineConstraintResolver _constraintResolver;
/// <summary>
/// Initializes a new instance of <see cref="TreeRouteBuilder"/>.
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="urlEncoder">The <see cref="UrlEncoder"/>.</param>
/// <param name="objectPool">The <see cref="ObjectPool{UrlBuildingContext}"/>.</param>
/// <param name="constraintResolver">The <see cref="IInlineConstraintResolver"/>.</param>
public TreeRouteBuilder(
ILoggerFactory loggerFactory,
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> 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<TreeRouter>();
@ -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);

View File

@ -33,22 +33,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
private readonly ILogger _logger;
private readonly ILogger _constraintLogger;
/// <summary>
/// Creates a new <see cref="TreeRouter"/>.
/// </summary>
/// <param name="trees">The list of <see cref="UrlMatchingTree"/> that contains the route entries.</param>
/// <param name="linkGenerationEntries">The set of <see cref="OutboundRouteEntry"/>.</param>
/// <param name="urlEncoder">The <see cref="UrlEncoder"/>.</param>
/// <param name="objectPool">The <see cref="ObjectPool{T}"/>.</param>
/// <param name="routeLogger">The <see cref="ILogger"/> instance.</param>
/// <param name="constraintLogger">The <see cref="ILogger"/> instance used
/// in <see cref="RouteConstraintMatcher"/>.</param>
/// <param name="version">The version of this route.</param>
public TreeRouter(
UrlMatchingTree[] trees,
IEnumerable<OutboundRouteEntry> linkGenerationEntries,
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> 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);

View File

@ -20,7 +20,6 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest
// This is a temporary layering issue, don't worry about it :)
services.AddRouting();
services.AddSingleton<RouteTemplateUrlGenerator>();
services.AddSingleton<IDefaultMatcherFactory, TreeMatcherFactory>();
services.Configure<DispatcherOptions>(ConfigureDispatcher);

View File

@ -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<Template>();
var factory = new DefaultTemplateFactory(new ITemplateFactoryComponent[]
{
Mock.Of<TemplateFactory<string>>(f => f.GetTemplate("foo") == expected),
});
// Act
var template = factory.GetTemplateFromKey("foo");
// Assert
Assert.Same(expected, template);
}
[Fact]
public void GetTemplateFromKey_UsesMatchingComponent_IgnoresOtherComponents()
{
// Arrange
var expected = Mock.Of<Template>();
var factory = new DefaultTemplateFactory(new ITemplateFactoryComponent[]
{
Mock.Of<TemplateFactory<int>>(f => f.GetTemplate(17) == Mock.Of<Template>()),
Mock.Of<TemplateFactory<string>>(f => f.GetTemplate("foo") == expected),
});
// Act
var template = factory.GetTemplateFromKey("foo");
// Assert
Assert.Same(expected, template);
}
[Fact]
public void GetTemplateFromKey_UsesMatchingComponent_ReturnsFirstMatch()
{
// Arrange
var expected = Mock.Of<Template>();
var factory = new DefaultTemplateFactory(new ITemplateFactoryComponent[]
{
Mock.Of<TemplateFactory<string>>(), // Will return null
Mock.Of<TemplateFactory<string>>(f => f.GetTemplate("foo") == expected),
});
// Act
var template = factory.GetTemplateFromKey("foo");
// Assert
Assert.Same(expected, template);
}
}
}

View File

@ -6,16 +6,21 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Dispatcher.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.WebEncoders.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Dispatcher
{
public class RoutePatternBinderTests
public class RoutePatternBinderTest
{
public RoutePatternBinderTest()
{
BinderFactory = new RoutePatternBinderFactory(new UrlTestEncoder(), new DefaultObjectPoolProvider());
}
public RoutePatternBinderFactory BinderFactory { get; }
public static TheoryData EmptyAndNullDefaultValues =>
new TheoryData<string, DispatcherValueCollection, DispatcherValueCollection, string>
{
@ -114,12 +119,7 @@ namespace Microsoft.AspNetCore.Dispatcher
string expected)
{
// Arrange
var encoder = new UrlTestEncoder();
var binder = new RoutePatternBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
RoutePattern.Parse(pattern),
defaults);
var binder = BinderFactory.Create(pattern, defaults);
// Act & Assert
(var acceptedValues, var combinedValues) = binder.GetValues(ambientValues: null, values: values);
@ -264,12 +264,7 @@ namespace Microsoft.AspNetCore.Dispatcher
string expected)
{
// Arrange
var encoder = new UrlTestEncoder();
var binder = new RoutePatternBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
RoutePattern.Parse(pattern),
defaults);
var binder = BinderFactory.Create(pattern, defaults);
// Act & Assert
(var acceptedValues, var combinedValues) = binder.GetValues(ambientValues: ambientValues, values: values);
@ -695,11 +690,7 @@ namespace Microsoft.AspNetCore.Dispatcher
// Arrange
var pattern = "{area?}/{controller=Home}/{action=Index}/{id?}";
var encoder = new UrlTestEncoder();
var binder = new RoutePatternBinder(
new UrlTestEncoder(),
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
RoutePattern.Parse(pattern),
defaults: null);
var binder = BinderFactory.Create(pattern);
var ambientValues = new DispatcherValueCollection();
var routeValues = new DispatcherValueCollection(new { controller = "Test", action = "Index" });
@ -1116,7 +1107,41 @@ namespace Microsoft.AspNetCore.Dispatcher
#endif
private static void RunTest(
[Theory]
[InlineData(null, null, true)]
[InlineData("blog", null, false)]
[InlineData(null, "store", false)]
[InlineData("Cool", "cool", true)]
[InlineData("Co0l", "cool", false)]
public void RoutePartsEqualTest(object left, object right, bool expected)
{
// Arrange & Act & Assert
if (expected)
{
Assert.True(RoutePatternBinder.RoutePartsEqual(left, right));
}
else
{
Assert.False(RoutePatternBinder.RoutePartsEqual(left, right));
}
}
private void RunTest(
string pattern,
object defaults,
object ambientValues,
object values,
string expected)
{
RunTest(
pattern,
new DispatcherValueCollection(defaults),
new DispatcherValueCollection(ambientValues),
new DispatcherValueCollection(values),
expected);
}
private void RunTest(
string pattern,
DispatcherValueCollection defaults,
DispatcherValueCollection ambientValues,
@ -1125,16 +1150,11 @@ namespace Microsoft.AspNetCore.Dispatcher
UrlEncoder encoder = null)
{
// Arrange
encoder = encoder ?? new UrlTestEncoder();
var binder = new RoutePatternBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
RoutePattern.Parse(pattern),
defaults);
var binderFactory = encoder == null ? BinderFactory : new RoutePatternBinderFactory(encoder, new DefaultObjectPoolProvider());
var binder = binderFactory.Create(pattern, defaults ?? new DispatcherValueCollection());
// Act & Assert
(var acceptedValues, var combinedValues) = binder.GetValues(ambientValues, values);
(var acceptedValues, var combinedValues) = binder.GetValues(ambientValues, values);
if (acceptedValues == null)
{
if (expected == null)
@ -1180,40 +1200,6 @@ namespace Microsoft.AspNetCore.Dispatcher
}
}
private static void RunTest(
string pattern,
object defaults,
object ambientValues,
object values,
string expected)
{
RunTest(
pattern,
new DispatcherValueCollection(defaults),
new DispatcherValueCollection(ambientValues),
new DispatcherValueCollection(values),
expected);
}
[Theory]
[InlineData(null, null, true)]
[InlineData("blog", null, false)]
[InlineData(null, "store", false)]
[InlineData("Cool", "cool", true)]
[InlineData("Co0l", "cool", false)]
public void RoutePartsEqualTest(object left, object right, bool expected)
{
// Arrange & Act & Assert
if (expected)
{
Assert.True(RoutePatternBinder.RoutePartsEqual(left, right));
}
else
{
Assert.False(RoutePatternBinder.RoutePartsEqual(left, right));
}
}
private class PathAndQuery
{
public PathAndQuery(string uri)

View File

@ -8,7 +8,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Dispatcher
{
public class RoutePatternMatcherTests
public class RoutePatternMatcherTest
{
[Fact]
public void TryMatch_Success()

View File

@ -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.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.ObjectPool;
using Xunit;
namespace Microsoft.AspNetCore.Dispatcher
{
// Not getting too in-depth with the tests here, the core URL generation is already tested elsewhere
public class RoutePatternTemplateTest
{
public RoutePatternTemplateTest()
{
BinderFactory = new RoutePatternBinderFactory(UrlEncoder.Default, new DefaultObjectPoolProvider());
}
public RoutePatternBinderFactory BinderFactory { get; }
[Fact]
public void GetUrl_WithAllRequiredValues_GeneratesUrl()
{
// Arrange
var template = new RoutePatternTemplate(BinderFactory.Create("api/products/{id}"));
// Act
var url = template.GetUrl(new DispatcherValueCollection(new { id = 17 }));
// Assert
Assert.Equal("/api/products/17", url);
}
[Fact]
public void GetUrl_WithoutAllRequiredValues_GeneratesUrl()
{
// Arrange
var template = new RoutePatternTemplate(BinderFactory.Create("api/products/{id}"));
// Act
var url = template.GetUrl(new DispatcherValueCollection(new { name = "billy" }));
// Assert
Assert.Null(url);
}
[Fact]
public void GetUrl_WithAmbientValues_GeneratesUrl()
{
// Arrange
var template = new RoutePatternTemplate(BinderFactory.Create("api/products/{id}/{name}"));
var httpContext = new DefaultHttpContext();
httpContext.Features.Set<IDispatcherFeature>(new DispatcherFeature()
{
Values = new DispatcherValueCollection(new { id = 17 }),
});
// Act
var url = template.GetUrl(httpContext, new DispatcherValueCollection(new { name = "billy" }));
// Assert
Assert.Equal("/api/products/17/billy", url);
}
}
}

View File

@ -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.Collections.Generic;
using Microsoft.AspNetCore.Routing.Constraints;
using Xunit;
@ -10,18 +9,6 @@ namespace Microsoft.AspNetCore.Routing.Tests
{
public class DateTimeRouteConstraintTests
{
public static IEnumerable<object[]> GetDateTimeObject
{
get
{
yield return new object[]
{
DateTime.Now,
true
};
}
}
[Theory]
[InlineData("12/25/2009", true)]
[InlineData("25/12/2009 11:45:00 PM", false)]
@ -36,9 +23,7 @@ namespace Microsoft.AspNetCore.Routing.Tests
[InlineData("12/25/2009 11:45:00 PM", true)]
[InlineData("2009-05-12T11:45:00Z", true)]
[InlineData("not-parseable-as-date", false)]
[InlineData(false, false)]
[MemberData(nameof(GetDateTimeObject))]
public void DateTimeRouteConstraint(object parameterValue, bool expected)
public void DateTimeRouteConstraint_ParsesStrings(string parameterValue, bool expected)
{
// Arrange
var constraint = new DateTimeRouteConstraint();
@ -49,5 +34,31 @@ namespace Microsoft.AspNetCore.Routing.Tests
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void DateTimeRouteConstraint_AcceptsDateTimeObjects_ReturnsTrue()
{
// Arrange
var constraint = new DateTimeRouteConstraint();
// Act
var actual = ConstraintsTestHelper.TestConstraint(constraint, DateTime.Now);
// Assert
Assert.True(actual);
}
[Fact]
public void DateTimeRouteConstraint_IgnoresOtherTypes_ReturnsFalse()
{
// Arrange
var constraint = new DateTimeRouteConstraint();
// Act
var actual = ConstraintsTestHelper.TestConstraint(constraint, false);
// Assert
Assert.False(actual);
}
}
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
@ -17,7 +16,12 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
{
public class TemplateBinderTests
{
private readonly IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver();
public TemplateBinderTests()
{
BinderFactory = new RoutePatternBinderFactory(new UrlTestEncoder(), new DefaultObjectPoolProvider());
}
public RoutePatternBinderFactory BinderFactory { get; }
public static TheoryData EmptyAndNullDefaultValues =>
new TheoryData<string, RouteValueDictionary, RouteValueDictionary, string>
@ -117,12 +121,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
string expected)
{
// Arrange
var encoder = new UrlTestEncoder();
var binder = new TemplateBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
TemplateParser.Parse(template),
defaults);
var binder = new TemplateBinder(BinderFactory.Create(template, defaults));
// Act & Assert
var result = binder.GetValues(ambientValues: null, values: values);
@ -267,12 +266,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
string expected)
{
// Arrange
var encoder = new UrlTestEncoder();
var binder = new TemplateBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
TemplateParser.Parse(template),
defaults);
var binder = new TemplateBinder(BinderFactory.Create(template, defaults));
// Act & Assert
var result = binder.GetValues(ambientValues: ambientValues, values: values);
@ -697,12 +691,8 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
{
// Arrange
var template = "{area?}/{controller=Home}/{action=Index}/{id?}";
var encoder = new UrlTestEncoder();
var binder = new TemplateBinder(
new UrlTestEncoder(),
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
TemplateParser.Parse(template),
defaults: null);
var binder = new TemplateBinder(BinderFactory.Create(template));
var ambientValues = new RouteValueDictionary();
var routeValues = new RouteValueDictionary(new { controller = "Test", action = "Index" });
@ -1119,7 +1109,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
#endif
private static void RunTest(
private void RunTest(
string template,
RouteValueDictionary defaults,
RouteValueDictionary ambientValues,
@ -1128,13 +1118,8 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
UrlEncoder encoder = null)
{
// Arrange
encoder = encoder ?? new UrlTestEncoder();
var binder = new TemplateBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
TemplateParser.Parse(template),
defaults);
var binderFactory = encoder == null ? BinderFactory : new RoutePatternBinderFactory(encoder, new DefaultObjectPoolProvider());
var binder = new TemplateBinder(binderFactory.Create(template, defaults ?? new RouteValueDictionary()));
// Act & Assert
var result = binder.GetValues(ambientValues, values);
@ -1183,7 +1168,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
}
}
private static void RunTest(
private void RunTest(
string template,
object defaults,
object ambientValues,

View File

@ -243,15 +243,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
private static TreeRouteBuilder CreateBuilder()
{
var objectPoolProvider = new DefaultObjectPoolProvider();
var objectPolicy = new UriBuilderContextPooledObjectPolicy();
var objectPool = objectPoolProvider.Create(objectPolicy);
var constraintResolver = GetInlineConstraintResolver();
var builder = new TreeRouteBuilder(
NullLoggerFactory.Instance,
UrlEncoder.Default,
objectPool,
new RoutePatternBinderFactory(UrlEncoder.Default, new DefaultObjectPoolProvider()),
constraintResolver);
return builder;
}

View File

@ -23,8 +23,12 @@ namespace Microsoft.AspNetCore.Routing.Tree
{
private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);
private static UrlEncoder Encoder = UrlTestEncoder.Default;
private static ObjectPool<UriBuildingContext> Pool = new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy());
public TreeRouterTest()
{
BinderFactory = new RoutePatternBinderFactory(new UrlTestEncoder(), new DefaultObjectPoolProvider());
}
public RoutePatternBinderFactory BinderFactory { get; }
[Theory]
[InlineData("template/5", "template/{parameter:int}")]
@ -1988,15 +1992,10 @@ namespace Microsoft.AspNetCore.Routing.Tree
private static TreeRouteBuilder CreateBuilder()
{
var objectPoolProvider = new DefaultObjectPoolProvider();
var objectPolicy = new UriBuilderContextPooledObjectPolicy();
var objectPool = objectPoolProvider.Create<UriBuildingContext>(objectPolicy);
var constraintResolver = CreateConstraintResolver();
var builder = new TreeRouteBuilder(
NullLoggerFactory.Instance,
UrlEncoder.Default,
objectPool,
new RoutePatternBinderFactory(UrlTestEncoder.Default, new DefaultObjectPoolProvider()),
constraintResolver);
return builder;
}