From 7685e17e80d4a321e1bfb13e5ed5575df45741af Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 22 Sep 2017 10:53:08 -0700 Subject: [PATCH] Add Addresses and link generation --- samples/DispatcherSample/RouteValueAddress.cs | 21 ----- .../RouteValueAddressTable.cs | 28 ------ samples/DispatcherSample/Startup.cs | 22 +++-- samples/DispatcherSample/UrlGenerator.cs | 58 ------------- .../Address.cs | 6 ++ .../AddressGroup.cs | 10 +++ .../AddressTable.cs | 12 +++ .../Endpoint.cs | 2 + .../IAddressCollectionProvider.cs | 15 ++++ .../IEndpointCollectionProvider.cs | 15 ++++ .../DefaultAddressTable.cs | 33 +++++++ .../DispatcherBase.cs | 28 +++++- .../DispatcherCollection.cs | 40 +++++++++ .../DispatcherEntry.cs | 17 ++++ .../DispatcherMiddleware.cs | 2 +- .../DispatcherOptions.cs | 3 +- .../DispatcherServiceCollectionExtensions.cs | 3 + .../DispatcherValueAddress.cs | 46 ++++++++++ .../DispatcherValueAddressSelector.cs | 85 +++++++++++++++++++ .../Dispatcher/IRouteTemplateMetadata.cs | 14 +++ .../Dispatcher/RouteTemplateMetadata.cs | 31 +++++++ .../Dispatcher/RouteTemplateUrlGenerator.cs | 72 ++++++++++++++++ 22 files changed, 444 insertions(+), 119 deletions(-) delete mode 100644 samples/DispatcherSample/RouteValueAddress.cs delete mode 100644 samples/DispatcherSample/RouteValueAddressTable.cs delete mode 100644 samples/DispatcherSample/UrlGenerator.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressGroup.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressTable.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/IAddressCollectionProvider.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher.Abstractions/IEndpointCollectionProvider.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherCollection.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherValueAddress.cs create mode 100644 src/Microsoft.AspNetCore.Dispatcher/DispatcherValueAddressSelector.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Dispatcher/IRouteTemplateMetadata.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateMetadata.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs diff --git a/samples/DispatcherSample/RouteValueAddress.cs b/samples/DispatcherSample/RouteValueAddress.cs deleted file mode 100644 index 9bb8a59b42..0000000000 --- a/samples/DispatcherSample/RouteValueAddress.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.AspNetCore.Dispatcher; -using Microsoft.AspNetCore.Routing; - -namespace DispatcherSample -{ - public class RouteValueAddress : Address - { - public RouteValueAddress(string displayName, RouteValueDictionary dictionary) - { - DisplayName = displayName; - RouteValueDictionary = dictionary; - } - - public override string DisplayName { get; } - - public RouteValueDictionary RouteValueDictionary { get; set; } - } -} diff --git a/samples/DispatcherSample/RouteValueAddressTable.cs b/samples/DispatcherSample/RouteValueAddressTable.cs deleted file mode 100644 index f61dcf472f..0000000000 --- a/samples/DispatcherSample/RouteValueAddressTable.cs +++ /dev/null @@ -1,28 +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.Collections.Generic; -using Microsoft.AspNetCore.Routing; - -namespace DispatcherSample -{ - public class RouteValueAddressTable - { - public IList Addresses - { - get - { - var addresses = new List - { - new RouteValueAddress("Mickey", new RouteValueDictionary (new { Character = "Mickey" })), - new RouteValueAddress("Hakuna Matata", new RouteValueDictionary (new { Movie = "The Lion King"})), - new RouteValueAddress("Simba", new RouteValueDictionary (new { Movie = "The Lion King", Character = "Simba" })), - new RouteValueAddress("Mufasa", new RouteValueDictionary (new { Movie = "The Lion King", Character = "Mufasa" })), - new RouteValueAddress("Aladdin", new RouteValueDictionary (new { Movie = "Aladdin", Character = "Genie" })), - }; - - return addresses; - } - } - } -} diff --git a/samples/DispatcherSample/Startup.cs b/samples/DispatcherSample/Startup.cs index 878133d1d8..448ab9c4d1 100644 --- a/samples/DispatcherSample/Startup.cs +++ b/samples/DispatcherSample/Startup.cs @@ -30,6 +30,13 @@ namespace DispatcherSample { options.Dispatchers.Add(new RouteTemplateDispatcher("{controller=Home}/{action=Index}/{id?}", ConstraintResolver) { + Addresses = + { + new DispatcherValueAddress(new { controller = "Home", action = "Index", }, new object[]{ new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, "Home:Index()"), + new DispatcherValueAddress(new { controller = "Home", action = "About", }, new object[]{ new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, "Home:About()"), + new DispatcherValueAddress(new { controller = "Admin", action = "Index", }, new object[]{ new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, "Admin:Index()"), + new DispatcherValueAddress(new { controller = "Admin", action = "Users", }, new object[]{ new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, "Admin:GetUsers()/Admin:EditUsers()"), + }, Endpoints = { new SimpleEndpoint(Home_Index, Array.Empty(), new { controller = "Home", action = "Index", }, "Home:Index()"), @@ -43,14 +50,14 @@ namespace DispatcherSample new DispatcherValueEndpointSelector(), new HttpMethodEndpointSelector(), } - }.InvokeAsync); + }); options.HandlerFactories.Add((endpoint) => (endpoint as SimpleEndpoint)?.HandlerFactory); }); - services.AddSingleton(); - services.AddSingleton(); services.AddDispatcher(); + services.AddRouting(); + services.AddSingleton(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILogger logger) @@ -85,12 +92,15 @@ namespace DispatcherSample public static Task Home_Index(HttpContext httpContext) { - var urlGenerator = httpContext.RequestServices.GetService(); - var url = urlGenerator.GenerateURL(new RouteValueDictionary(new { Movie = "The Lion King", Character = "Mufasa" }), httpContext); + var url = httpContext.RequestServices.GetService(); return httpContext.Response.WriteAsync( $"" + $"" + - $"

Generated url: {url}

" + + $"

Some links you can visit

" + + $"

Home:Index()

" + + $"

Home:About()

" + + $"

Admin:Index()

" + + $"

Admin:GetUsers()/Admin:EditUsers()

" + $"" + $""); } diff --git a/samples/DispatcherSample/UrlGenerator.cs b/samples/DispatcherSample/UrlGenerator.cs deleted file mode 100644 index 9b7dca1d09..0000000000 --- a/samples/DispatcherSample/UrlGenerator.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 Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; - -namespace DispatcherSample -{ - public class UrlGenerator - { - private readonly RouteValueAddressTable _addressTable; - - public UrlGenerator(RouteValueAddressTable addressTable) - { - _addressTable = addressTable; - } - - //Find match from values to a template - public string GenerateURL(RouteValueDictionary routeValues, HttpContext context) - { - var address = FindAddress(_addressTable, routeValues); - return $"RouteName: {address.DisplayName} URL: /{address.RouteValueDictionary["Character"]}/{address.RouteValueDictionary["Movie"]}"; - } - - //Look up the Addresses table - private RouteValueAddress FindAddress(RouteValueAddressTable addressTable, RouteValueDictionary routeValues) - { - var addressMatch = new RouteValueAddress(null, new RouteValueDictionary()); - foreach (var address in addressTable.Addresses) - { - foreach (var key in address.RouteValueDictionary.Keys) - { - if (!routeValues.Keys.Contains(key)) - { - addressMatch.RouteValueDictionary.Clear(); - break; - } - - if (routeValues.Values.Contains(address.RouteValueDictionary[key])) - { - addressMatch.RouteValueDictionary[key] = routeValues[key]; - } - } - - if (addressMatch.RouteValueDictionary.Count == routeValues.Count) - { - return new RouteValueAddress(address.DisplayName, address.RouteValueDictionary); - } - else - { - addressMatch.RouteValueDictionary.Clear(); - } - } - - return addressMatch; - } - } -} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Address.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Address.cs index a9008a6291..a65cb0b05e 100644 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Address.cs +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Address.cs @@ -1,10 +1,16 @@ // 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.Collections.Generic; +using System.Diagnostics; + namespace Microsoft.AspNetCore.Dispatcher { + [DebuggerDisplay("{DisplayName,nq}")] public abstract class Address { public abstract string DisplayName { get; } + + public abstract IReadOnlyList Metadata { get; } } } diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressGroup.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressGroup.cs new file mode 100644 index 0000000000..3162e47b9c --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressGroup.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.Dispatcher.Abstractions +{ + class AddressGroup + { + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressTable.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressTable.cs new file mode 100644 index 0000000000..8ae1019518 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/AddressTable.cs @@ -0,0 +1,12 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public abstract class AddressTable + { + public abstract IReadOnlyList> AddressGroups { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs index 2577d16e62..a621c76ee3 100644 --- a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/Endpoint.cs @@ -2,9 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Diagnostics; namespace Microsoft.AspNetCore.Dispatcher { + [DebuggerDisplay("{DisplayName,nq}")] public abstract class Endpoint { public abstract string DisplayName { get; } diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IAddressCollectionProvider.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IAddressCollectionProvider.cs new file mode 100644 index 0000000000..75f942bcb3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IAddressCollectionProvider.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. + +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public interface IAddressCollectionProvider + { + IReadOnlyList
Addresses { get; } + + IChangeToken ChangeToken { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IEndpointCollectionProvider.cs b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IEndpointCollectionProvider.cs new file mode 100644 index 0000000000..b4c5fa1d12 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher.Abstractions/IEndpointCollectionProvider.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. + +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public interface IEndpointCollectionProvider + { + IReadOnlyList Endpoints { get; } + + IChangeToken ChangeToken { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs b/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs new file mode 100644 index 0000000000..d67f698c64 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/DefaultAddressTable.cs @@ -0,0 +1,33 @@ +// 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 Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public class DefaultAddressTable : AddressTable + { + private readonly DispatcherOptions _options; + private readonly List
[] _groups; + + public DefaultAddressTable(IOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _options = options.Value; + + _groups = new List
[options.Value.Dispatchers.Count]; + for (var i = 0; i < options.Value.Dispatchers.Count; i++) + { + _groups[i] = new List
(options.Value.Dispatchers[i].AddressProvider?.Addresses ?? Array.Empty
()); + } + } + + public override IReadOnlyList> AddressGroups => _groups; + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs index 61ecfb087f..e655828e01 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherBase.cs @@ -6,13 +6,29 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Dispatcher { - public abstract class DispatcherBase + public abstract class DispatcherBase : IAddressCollectionProvider, IEndpointCollectionProvider { - private IList _endpoints; - private IList _endpointSelectors; + private List
_addresses; + private List _endpoints; + private List _endpointSelectors; + + public virtual IList
Addresses + { + get + { + if (_addresses == null) + { + _addresses = new List
(); + } + + return _addresses; + } + } public virtual IList Endpoints { @@ -40,6 +56,12 @@ namespace Microsoft.AspNetCore.Dispatcher } } + public IChangeToken ChangeToken => NullChangeToken.Singleton; + + IReadOnlyList
IAddressCollectionProvider.Addresses => _addresses; + + IReadOnlyList IEndpointCollectionProvider.Endpoints => _endpoints; + public virtual async Task InvokeAsync(HttpContext httpContext) { if (httpContext == null) diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherCollection.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherCollection.cs new file mode 100644 index 0000000000..6c5f7e0b80 --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherCollection.cs @@ -0,0 +1,40 @@ +// 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.ObjectModel; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public class DispatcherCollection : Collection + { + public void Add(DispatcherBase dispatcher) + { + if (dispatcher == null) + { + throw new ArgumentNullException(nameof(dispatcher)); + } + + Add(new DispatcherEntry() + { + Dispatcher = dispatcher.InvokeAsync, + AddressProvider = dispatcher, + EndpointProvider = dispatcher, + }); + } + + public void Add(RequestDelegate dispatcher) + { + if (dispatcher == null) + { + throw new ArgumentNullException(nameof(dispatcher)); + } + + Add(new DispatcherEntry() + { + Dispatcher = dispatcher, + }); + } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs new file mode 100644 index 0000000000..aeb602ff3b --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherEntry.cs @@ -0,0 +1,17 @@ +// 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.Dispatcher.Abstractions; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Dispatcher +{ + public class DispatcherEntry + { + public RequestDelegate Dispatcher { get; set; } + + public IAddressCollectionProvider AddressProvider { get; set; } + + public IEndpointCollectionProvider EndpointProvider { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs index 4261b36eaf..4f32b16551 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherMiddleware.cs @@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Dispatcher foreach (var entry in _options.Dispatchers) { - await entry(httpContext); + await entry.Dispatcher(httpContext); if (feature.Endpoint != null || feature.RequestDelegate != null) { _logger.LogInformation("Matched endpoint {Endpoint}", feature.Endpoint.DisplayName); diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs index 7f01d4c8f1..ad5d71a1a2 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs @@ -1,14 +1,13 @@ // 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; using System.Collections.Generic; namespace Microsoft.AspNetCore.Dispatcher { public class DispatcherOptions { - public IList Dispatchers { get; } = new List(); + public DispatcherCollection Dispatchers { get; } = new DispatcherCollection(); public IList HandlerFactories { get; } = new List(); } diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherServiceCollectionExtensions.cs index e3c6fe7021..23d5a6fdd9 100644 --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherServiceCollectionExtensions.cs @@ -17,6 +17,9 @@ namespace Microsoft.Extensions.DependencyInjection } services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; } } diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherValueAddress.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherValueAddress.cs new file mode 100644 index 0000000000..065f56eb7b --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherValueAddress.cs @@ -0,0 +1,46 @@ +// 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 DispatcherValueAddress : Address + { + public DispatcherValueAddress(object values) + : this(values, Array.Empty(), null) + { + } + + + public DispatcherValueAddress(object values, IEnumerable metadata) + : this(values, metadata, null) + { + } + + public DispatcherValueAddress(object values, IEnumerable metadata, string displayName) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + Values = new DispatcherValueCollection(values); + Metadata = metadata.ToArray(); + DisplayName = displayName; + } + + public override string DisplayName { get; } + + public override IReadOnlyList Metadata { get; } + + public DispatcherValueCollection Values { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherValueAddressSelector.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherValueAddressSelector.cs new file mode 100644 index 0000000000..d7adebfe5e --- /dev/null +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherValueAddressSelector.cs @@ -0,0 +1,85 @@ +// 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; + +namespace Microsoft.AspNetCore.Dispatcher +{ + // This isn't a proposed design, just a placeholder to demonstrate that things are wired up correctly. + public class DispatcherValueAddressSelector + { + private readonly AddressTable _addressTable; + + public DispatcherValueAddressSelector(AddressTable addressTable) + { + if (addressTable == null) + { + throw new ArgumentNullException(nameof(addressTable)); + } + + _addressTable = addressTable; + } + + public Address SelectAddress(DispatcherValueCollection values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + // Capture the current state so we don't see partial updates. + var groups = _addressTable.AddressGroups; + for (var i = 0; i < groups.Count; i++) + { + var matches = new List
(); + var group = groups[i]; + + for (var j = 0; j < group.Count; j++) + { + var address = group[j] as DispatcherValueAddress; + if (address == null) + { + continue; + } + + if (IsMatch(address, values)) + { + matches.Add(address); + } + } + + switch (matches.Count) + { + case 0: + // No match, keep going. + break; + + case 1: + return matches[0]; + + default: + throw new InvalidOperationException("Ambiguous bro!"); + + } + } + + return null; + } + + private bool IsMatch(DispatcherValueAddress address, DispatcherValueCollection values) + { + foreach (var kvp in address.Values) + { + values.TryGetValue(kvp.Key, out var value); + + if (!string.Equals(Convert.ToString(kvp.Value) ?? string.Empty, Convert.ToString(value) ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/IRouteTemplateMetadata.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/IRouteTemplateMetadata.cs new file mode 100644 index 0000000000..13f30517b2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/IRouteTemplateMetadata.cs @@ -0,0 +1,14 @@ +// 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.Dispatcher; + +namespace Microsoft.AspNetCore.Routing.Dispatcher +{ + public interface IRouteTemplateMetadata + { + string RouteTemplate { get; } + + DispatcherValueCollection Defaults { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateMetadata.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateMetadata.cs new file mode 100644 index 0000000000..d476cb556f --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateMetadata.cs @@ -0,0 +1,31 @@ +// 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.Dispatcher; + +namespace Microsoft.AspNetCore.Routing.Dispatcher +{ + public class RouteTemplateMetadata : IRouteTemplateMetadata + { + public RouteTemplateMetadata(string routeTemplate) + : this(routeTemplate, null) + { + } + + public RouteTemplateMetadata(string routeTemplate, object defaults) + { + if (routeTemplate == null) + { + throw new ArgumentNullException(nameof(routeTemplate)); + } + + RouteTemplate = routeTemplate; + Defaults = new DispatcherValueCollection(defaults); + } + + public string RouteTemplate { get; } + + public DispatcherValueCollection Defaults { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs new file mode 100644 index 0000000000..604f3c394a --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/RouteTemplateUrlGenerator.cs @@ -0,0 +1,72 @@ +// 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 DispatcherValueAddressSelector _addressSelector; + private readonly ObjectPool _pool; + private readonly UrlEncoder _urlEncoder; + + public RouteTemplateUrlGenerator(DispatcherValueAddressSelector 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)); + if (address == null) + { + throw new InvalidOperationException("Can't find address"); + } + + var (template, defaults) = GetRouteTemplate(address); + var binder = new TemplateBinder(_urlEncoder, _pool, TemplateParser.Parse(template), defaults.AsRouteValueDictionary()); + + 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); + } + private (string, DispatcherValueCollection) GetRouteTemplate(Address address) + { + for (var i = address.Metadata.Count - 1; i >= 0; i--) + { + var metadata = address.Metadata[i] as IRouteTemplateMetadata; + if (metadata != null) + { + return (metadata.RouteTemplate, metadata.Defaults); + } + } + + return (null, null); + } + } +}