Add Addresses and link generation

This commit is contained in:
Ryan Nowak 2017-09-22 10:53:08 -07:00
parent 475712d613
commit 7685e17e80
22 changed files with 444 additions and 119 deletions

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

View File

@ -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<RouteValueAddress> Addresses
{
get
{
var addresses = new List<RouteValueAddress>
{
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;
}
}
}
}

View File

@ -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<object>(), 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<UrlGenerator>();
services.AddSingleton<RouteValueAddressTable>();
services.AddDispatcher();
services.AddRouting();
services.AddSingleton<RouteTemplateUrlGenerator>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILogger<Startup> logger)
@ -85,12 +92,15 @@ namespace DispatcherSample
public static Task Home_Index(HttpContext httpContext)
{
var urlGenerator = httpContext.RequestServices.GetService<UrlGenerator>();
var url = urlGenerator.GenerateURL(new RouteValueDictionary(new { Movie = "The Lion King", Character = "Mufasa" }), httpContext);
var url = httpContext.RequestServices.GetService<RouteTemplateUrlGenerator>();
return httpContext.Response.WriteAsync(
$"<html>" +
$"<body>" +
$"<p>Generated url: {url}</p>" +
$"<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>" +
$"</body>" +
$"</html>");
}

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

View File

@ -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<object> Metadata { get; }
}
}

View File

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

View File

@ -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<IReadOnlyList<Address>> AddressGroups { get; }
}
}

View File

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

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.
using System.Collections.Generic;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Dispatcher
{
public interface IAddressCollectionProvider
{
IReadOnlyList<Address> Addresses { get; }
IChangeToken ChangeToken { get; }
}
}

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.
using System.Collections.Generic;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Dispatcher
{
public interface IEndpointCollectionProvider
{
IReadOnlyList<Endpoint> Endpoints { get; }
IChangeToken ChangeToken { get; }
}
}

View File

@ -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<Address>[] _groups;
public DefaultAddressTable(IOptions<DispatcherOptions> options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_options = options.Value;
_groups = new List<Address>[options.Value.Dispatchers.Count];
for (var i = 0; i < options.Value.Dispatchers.Count; i++)
{
_groups[i] = new List<Address>(options.Value.Dispatchers[i].AddressProvider?.Addresses ?? Array.Empty<Address>());
}
}
public override IReadOnlyList<IReadOnlyList<Address>> AddressGroups => _groups;
}
}

View File

@ -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<Endpoint> _endpoints;
private IList<EndpointSelector> _endpointSelectors;
private List<Address> _addresses;
private List<Endpoint> _endpoints;
private List<EndpointSelector> _endpointSelectors;
public virtual IList<Address> Addresses
{
get
{
if (_addresses == null)
{
_addresses = new List<Address>();
}
return _addresses;
}
}
public virtual IList<Endpoint> Endpoints
{
@ -40,6 +56,12 @@ namespace Microsoft.AspNetCore.Dispatcher
}
}
public IChangeToken ChangeToken => NullChangeToken.Singleton;
IReadOnlyList<Address> IAddressCollectionProvider.Addresses => _addresses;
IReadOnlyList<Endpoint> IEndpointCollectionProvider.Endpoints => _endpoints;
public virtual async Task InvokeAsync(HttpContext httpContext)
{
if (httpContext == null)

View File

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

View File

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

View File

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

View File

@ -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<RequestDelegate> Dispatchers { get; } = new List<RequestDelegate>();
public DispatcherCollection Dispatchers { get; } = new DispatcherCollection();
public IList<EndpointHandlerFactory> HandlerFactories { get; } = new List<EndpointHandlerFactory>();
}

View File

@ -17,6 +17,9 @@ namespace Microsoft.Extensions.DependencyInjection
}
services.AddSingleton<IStartupFilter, DispatcherEndpointStartupFilter>();
services.AddSingleton<AddressTable, DefaultAddressTable>();
services.AddSingleton<DispatcherValueAddressSelector>();
return services;
}
}

View File

@ -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<object>(), null)
{
}
public DispatcherValueAddress(object values, IEnumerable<object> metadata)
: this(values, metadata, null)
{
}
public DispatcherValueAddress(object values, IEnumerable<object> 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<object> Metadata { get; }
public DispatcherValueCollection Values { get; }
}
}

View File

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

View File

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

View File

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

View File

@ -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<UriBuildingContext> _pool;
private readonly UrlEncoder _urlEncoder;
public RouteTemplateUrlGenerator(DispatcherValueAddressSelector 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));
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<IDispatcherFeature>();
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);
}
}
}