Making endpoints and addresses easier

This makes endpoints and addresses easier to work with by dropping the
'metadata first' approach for the the things that are really at the core
of the dispatcher.
This commit is contained in:
Ryan Nowak 2017-09-28 22:07:32 -07:00
parent 44d08dcb2b
commit bd750ad76d
19 changed files with 189 additions and 248 deletions

View File

@ -23,11 +23,11 @@ namespace DispatcherSample
for (var i = context.Endpoints.Count - 1; i >= 0; i--)
{
var endpoint = context.Endpoints[i];
IHttpMethodMetadata metadata = null;
ITemplateEndpoint metadata = null;
for (var j = endpoint.Metadata.Count - 1; j >= 0; j--)
{
metadata = endpoint.Metadata[j] as IHttpMethodMetadata;
metadata = endpoint.Metadata[j] as ITemplateEndpoint;
if (metadata != null)
{
break;
@ -70,17 +70,9 @@ namespace DispatcherSample
}
}
private bool Matches(IHttpMethodMetadata metadata, string httpMethod)
private bool Matches(ITemplateEndpoint endpoint, string httpMethod)
{
for (var i = 0; i < metadata.AllowedMethods.Count; i++)
{
if (string.Equals(metadata.AllowedMethods[i], httpMethod, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
return string.Equals(endpoint.HttpMethod, httpMethod, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -1,23 +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.Collections.Generic;
namespace DispatcherSample
{
public class HttpMethodMetadata : IHttpMethodMetadata
{
public HttpMethodMetadata(string httpMethod)
{
if (httpMethod == null)
{
throw new ArgumentNullException(nameof(httpMethod));
}
AllowedMethods = new[] { httpMethod, };
}
public IReadOnlyList<string> AllowedMethods { get; }
}
}

View File

@ -1,12 +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;
namespace DispatcherSample
{
public interface IHttpMethodMetadata
{
IReadOnlyList<string> AllowedMethods { get; }
}
}

View File

@ -1,7 +1,6 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
@ -32,27 +31,27 @@ namespace DispatcherSample
{
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()"),
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "Index", }, "Home:Index()"),
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "About", }, "Home:About()"),
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Index", }, "Admin:Index()"),
new TemplateAddress("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Users", }, "Admin:GetUsers()/Admin:EditUsers()"),
},
Endpoints =
{
new SimpleEndpoint(Home_Index, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, new { controller = "Home", action = "Index", }, "Home:Index()"),
new SimpleEndpoint(Home_About, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, new { controller = "Home", action = "About", }, "Home:About()"),
new SimpleEndpoint(Admin_Index, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), }, new { controller = "Admin", action = "Index", }, "Admin:Index()"),
new SimpleEndpoint(Admin_GetUsers, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), new HttpMethodMetadata("GET"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:GetUsers()"),
new SimpleEndpoint(Admin_EditUsers, new object[] { new RouteTemplateMetadata("{controller=Home}/{action=Index}/{id?}"), new HttpMethodMetadata("POST"), new AuthorizationPolicyMetadata("Admin"), }, new { controller = "Admin", action = "Users", }, "Admin:EditUsers()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "Index", }, Home_Index, "Home:Index()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Home", action = "About", }, Home_About, "Home:About()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Index", }, Admin_Index, "Admin:Index()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Users", }, "GET", Admin_GetUsers, "Admin:GetUsers()", new AuthorizationPolicyMetadata("Admin")),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Admin", action = "Users", }, "POST", Admin_EditUsers, "Admin:EditUsers()", new AuthorizationPolicyMetadata("Admin")),
},
Selectors =
{
new DispatcherValueEndpointSelector(),
new TemplateEndpointSelector(),
new HttpMethodEndpointSelector(),
}
});
options.HandlerFactories.Add((endpoint) => (endpoint as SimpleEndpoint)?.HandlerFactory);
options.HandlerFactories.Add((endpoint) => (endpoint as TemplateEndpoint)?.HandlerFactory);
});
services.AddDispatcher();

View File

@ -7,12 +7,12 @@ using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Dispatcher
{
public class SimpleDispatcherDataSource : DispatcherDataSource
public class DefaultDispatcherDataSource : DispatcherDataSource
{
private readonly List<Address> _addresses;
private readonly List<Endpoint> _endpoints;
public SimpleDispatcherDataSource()
public DefaultDispatcherDataSource()
{
_addresses = new List<Address>();
_endpoints = new List<Endpoint>();

View File

@ -18,7 +18,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddSingleton<IStartupFilter, DispatcherEndpointStartupFilter>();
services.AddSingleton<AddressTable, DefaultAddressTable>();
services.AddSingleton<DispatcherValueAddressSelector>();
services.AddSingleton<TemplateAddressSelector>();
return services;
}

View File

@ -3,8 +3,10 @@
namespace Microsoft.AspNetCore.Dispatcher
{
public interface IDispatcherValueSelectableEndpoint
public interface ITemplateAddress
{
string Template { get; }
DispatcherValueCollection Values { get; }
}
}

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.
namespace Microsoft.AspNetCore.Dispatcher
{
public interface ITemplateEndpoint
{
string HttpMethod { get; }
string Template { get; }
DispatcherValueCollection Values { get; }
}
}

View File

@ -1,82 +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.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher
{
public class SimpleEndpoint : Endpoint, IDispatcherValueSelectableEndpoint
{
public SimpleEndpoint(RequestDelegate requestDelegate)
: this(requestDelegate, Array.Empty<object>(), null, null)
{
}
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory)
: this(delegateFactory, Array.Empty<object>(), null, null)
{
}
public SimpleEndpoint(RequestDelegate requestDelegate, IEnumerable<object> metadata)
: this(requestDelegate, metadata, null, null)
{
}
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory, IEnumerable<object> metadata)
: this(delegateFactory, metadata, null, null)
{
}
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory, IEnumerable<object> metadata, object values)
: this(delegateFactory, metadata, null, null)
{
}
public SimpleEndpoint(RequestDelegate requestDelegate, IEnumerable<object> metadata, object values, string displayName)
{
if (metadata == null)
{
throw new ArgumentNullException(nameof(metadata));
}
if (requestDelegate == null)
{
throw new ArgumentNullException(nameof(requestDelegate));
}
HandlerFactory = (next) => requestDelegate;
Metadata = metadata.ToArray();
Values = new DispatcherValueCollection(values);
DisplayName = displayName;
}
public SimpleEndpoint(Func<RequestDelegate, RequestDelegate> delegateFactory, IEnumerable<object> metadata, object values, string displayName)
{
if (metadata == null)
{
throw new ArgumentNullException(nameof(metadata));
}
if (delegateFactory == null)
{
throw new ArgumentNullException(nameof(delegateFactory));
}
HandlerFactory = delegateFactory;
Metadata = metadata.ToArray();
Values = new DispatcherValueCollection(values);
DisplayName = displayName;
}
public override string DisplayName { get; }
public override IReadOnlyList<object> Metadata { get; }
public Func<RequestDelegate, RequestDelegate> HandlerFactory { get; }
public DispatcherValueCollection Values { get; }
}
}

View File

@ -7,24 +7,18 @@ using System.Linq;
namespace Microsoft.AspNetCore.Dispatcher
{
public class DispatcherValueAddress : Address
public class TemplateAddress : Address, ITemplateAddress
{
public DispatcherValueAddress(object values)
: this(values, Array.Empty<object>(), null)
public TemplateAddress(string template, object values, params object[] metadata)
: this(template, values, null, metadata)
{
}
public DispatcherValueAddress(object values, IEnumerable<object> metadata)
: this(values, metadata, null)
public TemplateAddress(string template, object values, string displayName, params object[] metadata)
{
}
public DispatcherValueAddress(object values, IEnumerable<object> metadata, string displayName)
{
if (values == null)
if (template == null)
{
throw new ArgumentNullException(nameof(values));
throw new ArgumentNullException(nameof(template));
}
if (metadata == null)
@ -32,15 +26,18 @@ namespace Microsoft.AspNetCore.Dispatcher
throw new ArgumentNullException(nameof(metadata));
}
Template = template;
Values = new DispatcherValueCollection(values);
Metadata = metadata.ToArray();
DisplayName = displayName;
Metadata = metadata.ToArray();
}
public override string DisplayName { get; }
public override IReadOnlyList<object> Metadata { get; }
public string Template { get; }
public DispatcherValueCollection Values { get; }
}
}

View File

@ -7,11 +7,11 @@ 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
public class TemplateAddressSelector
{
private readonly AddressTable _addressTable;
public DispatcherValueAddressSelector(AddressTable addressTable)
public TemplateAddressSelector(AddressTable addressTable)
{
if (addressTable == null)
{
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Dispatcher
for (var j = 0; j < group.Count; j++)
{
var address = group[j] as DispatcherValueAddress;
var address = group[j] as ITemplateAddress;
if (address == null)
{
continue;
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Dispatcher
if (IsMatch(address, values))
{
matches.Add(address);
matches.Add(group[j]);
}
}
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Dispatcher
return null;
}
private bool IsMatch(DispatcherValueAddress address, DispatcherValueCollection values)
private bool IsMatch(ITemplateAddress address, DispatcherValueCollection values)
{
foreach (var kvp in address.Values)
{

View File

@ -0,0 +1,127 @@
// 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;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Dispatcher
{
public class TemplateEndpoint : Endpoint, ITemplateEndpoint
{
public TemplateEndpoint(string template, RequestDelegate requestDelegate, params object[] metadata)
: this(template, (object)null, (string)null, requestDelegate, null, metadata)
{
}
public TemplateEndpoint(string template, Func<RequestDelegate, RequestDelegate> delegateFactory, params object[] metadata)
: this(template, (object)null, (string)null, delegateFactory, null, metadata)
{
}
public TemplateEndpoint(string template, object values, RequestDelegate requestDelegate, params object[] metadata)
: this(template, values, null, requestDelegate, null, metadata)
{
}
public TemplateEndpoint(string template, object values, Func<RequestDelegate, RequestDelegate> delegateFactory, params object[] metadata)
: this(template, values, null, delegateFactory, null, metadata)
{
}
public TemplateEndpoint(string template, object values, RequestDelegate requestDelegate, string displayName, params object[] metadata)
: this(template, values, null, requestDelegate, displayName, metadata)
{
}
public TemplateEndpoint(string template, object values, Func<RequestDelegate, RequestDelegate> delegateFactory, string displayName, params object[] metadata)
: this(template, values, null, delegateFactory, displayName, metadata)
{
}
public TemplateEndpoint(string template, object values, string httpMethod, RequestDelegate requestDelegate, params object[] metadata)
: this(template, values, httpMethod, requestDelegate, null, metadata)
{
}
public TemplateEndpoint(string template, object values, string httpMethod, Func<RequestDelegate, RequestDelegate> delegateFactory, params object[] metadata)
: this(template, values, httpMethod, delegateFactory, null, metadata)
{
}
public TemplateEndpoint(
string template,
object values,
string httpMethod,
RequestDelegate requestDelegate,
string displayName,
params object[] metadata)
{
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
if (requestDelegate == null)
{
throw new ArgumentNullException(nameof(requestDelegate));
}
if (metadata == null)
{
throw new ArgumentNullException(nameof(metadata));
}
Template = template;
Values = new DispatcherValueCollection(values);
HttpMethod = httpMethod;
HandlerFactory = (next) => requestDelegate;
DisplayName = displayName;
Metadata = metadata.ToArray();
}
public TemplateEndpoint(
string template,
object values,
string httpMethod,
Func<RequestDelegate, RequestDelegate> delegateFactory,
string displayName,
params object[] metadata)
{
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
if (delegateFactory == null)
{
throw new ArgumentNullException(nameof(delegateFactory));
}
if (metadata == null)
{
throw new ArgumentNullException(nameof(metadata));
}
Template = template;
Values = new DispatcherValueCollection(values);
HttpMethod = httpMethod;
HandlerFactory = delegateFactory;
DisplayName = displayName;
Metadata = metadata.ToArray();
}
public override string DisplayName { get; }
public string HttpMethod { get; }
public override IReadOnlyList<object> Metadata { get; }
public Func<RequestDelegate, RequestDelegate> HandlerFactory { get; }
public string Template { get; }
public DispatcherValueCollection Values { get; }
}
}

View File

@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Dispatcher
{
public class DispatcherValueEndpointSelector : EndpointSelector
public class TemplateEndpointSelector : EndpointSelector
{
public override Task SelectAsync(EndpointSelectorContext context)
{
@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Dispatcher
for (var i = context.Endpoints.Count - 1; i >= 0; i--)
{
var endpoint = context.Endpoints[i] as IDispatcherValueSelectableEndpoint;
var endpoint = context.Endpoints[i] as ITemplateEndpoint;
if (!CompareRouteValues(dispatcherFeature.Values, endpoint.Values))
{
context.Endpoints.RemoveAt(i);

View File

@ -1,14 +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;
namespace Microsoft.AspNetCore.Routing.Dispatcher
{
public interface IRouteTemplateMetadata
{
string RouteTemplate { get; }
DispatcherValueCollection Defaults { get; }
}
}

View File

@ -1,12 +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.
namespace Microsoft.AspNetCore.Routing.Dispatcher
{
public interface ITreeDispatcherMetadata
{
int Order { get; }
string RouteTemplate { get; }
}
}

View File

@ -1,33 +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 Microsoft.AspNetCore.Dispatcher;
namespace Microsoft.AspNetCore.Routing.Dispatcher
{
public class RouteTemplateMetadata : IRouteTemplateMetadata, ITreeDispatcherMetadata
{
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; }
public int Order { get; set; }
}
}

View File

@ -14,11 +14,11 @@ 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 TemplateAddressSelector _addressSelector;
private readonly ObjectPool<UriBuildingContext> _pool;
private readonly UrlEncoder _urlEncoder;
public RouteTemplateUrlGenerator(DispatcherValueAddressSelector addressSelector, UrlEncoder urlEncoder, ObjectPool<UriBuildingContext> pool)
public RouteTemplateUrlGenerator(TemplateAddressSelector addressSelector, UrlEncoder urlEncoder, ObjectPool<UriBuildingContext> pool)
{
_addressSelector = addressSelector;
_urlEncoder = urlEncoder;
@ -37,14 +37,13 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
throw new ArgumentNullException(nameof(values));
}
var address = _addressSelector.SelectAddress(new DispatcherValueCollection(values));
var address = _addressSelector.SelectAddress(new DispatcherValueCollection(values)) as ITemplateAddress;
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 binder = new TemplateBinder(_urlEncoder, _pool, TemplateParser.Parse(address.Template), new RouteValueDictionary());
var feature = httpContext.Features.Get<IDispatcherFeature>();
var result = binder.GetValues(feature.Values.AsRouteValueDictionary(), new RouteValueDictionary(values));
@ -55,18 +54,5 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
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);
}
}
}

View File

@ -142,16 +142,16 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
{
var endpoint = endpoints[i];
var metadata = endpoint.Metadata.OfType<ITreeDispatcherMetadata>().LastOrDefault();
if (metadata == null)
var templateEndpoint = endpoint as ITemplateEndpoint;
if (templateEndpoint == null)
{
continue;
}
if (!groups.TryGetValue(new Key(metadata.Order, metadata.RouteTemplate), out var group))
if (!groups.TryGetValue(new Key(0, templateEndpoint.Template), out var group))
{
group = new List<Endpoint>();
groups.Add(new Key(metadata.Order, metadata.RouteTemplate), group);
groups.Add(new Key(0, templateEndpoint.Template), group);
}
group.Add(endpoint);

View File

@ -58,11 +58,11 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest
{
Endpoints =
{
new SimpleEndpoint(Products_Get, new object[]{ new RouteTemplateMetadata("api/products"), }),
new TemplateEndpoint("api/products", Products_Get),
},
});
options.HandlerFactories.Add(endpoint => (endpoint as SimpleEndpoint)?.HandlerFactory);
options.HandlerFactories.Add(endpoint => (endpoint as TemplateEndpoint)?.HandlerFactory);
}
private Task Products_Get(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Get");