Endpoint routing startup experience (#889)

This commit is contained in:
James Newton-King 2018-10-23 13:54:36 +13:00 committed by GitHub
parent be0e602d2f
commit 0ef4b4173c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 826 additions and 329 deletions

View File

@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Benchmarks
{
@ -18,8 +17,13 @@ namespace Benchmarks
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}
var endpointDataSource = new DefaultEndpointDataSource(new[]
public void Configure(IApplicationBuilder app)
{
app.UseEndpointRouting(builder =>
{
var endpointDataSource = new DefaultEndpointDataSource(new[]
{
new RouteEndpoint(
requestDelegate: (httpContext) =>
@ -37,12 +41,8 @@ namespace Benchmarks
displayName: "Plaintext"),
});
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
}
public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app)
{
app.UseEndpointRouting();
builder.DataSources.Add(endpointDataSource);
});
app.UseEndpoint();
}

View File

@ -0,0 +1,25 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using RoutingSample.Web.AuthorizationMiddleware;
namespace Microsoft.AspNetCore.Builder
{
public static class AuthorizationAppBuilderExtensions
{
public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.UseMiddleware<AuthorizationMiddleware>();
}
}
}

View File

@ -0,0 +1,25 @@
// 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 System.Threading.Tasks;
namespace RoutingSample.Web.AuthorizationMiddleware
{
public class AuthorizationMetadata
{
public AuthorizationMetadata(IEnumerable<string> roles)
{
if (roles == null)
{
throw new ArgumentNullException(nameof(roles));
}
Roles = roles.ToArray();
}
public IReadOnlyList<string> Roles { get; }
}
}

View File

@ -0,0 +1,59 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace RoutingSample.Web.AuthorizationMiddleware
{
public class AuthorizationMiddleware
{
private readonly ILogger _logger;
private readonly RequestDelegate _next;
public AuthorizationMiddleware(ILogger<AuthorizationMiddleware> logger, RequestDelegate next)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
_logger = logger;
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
var endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;
if (endpoint != null)
{
var metadata = endpoint.Metadata.GetMetadata<AuthorizationMetadata>();
// Only run authorization if endpoint has metadata
if (metadata != null)
{
if (!httpContext.Request.Query.TryGetValue("x-role", out var role) ||
!metadata.Roles.Contains(role.ToString()))
{
httpContext.Response.StatusCode = 401;
httpContext.Response.ContentType = "text/plain";
await httpContext.Response.WriteAsync($"Unauthorized access to '{endpoint.DisplayName}'.");
return;
}
}
}
await _next(httpContext);
}
}
}

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.Routing;
using RoutingSample.Web.AuthorizationMiddleware;
namespace Microsoft.AspNetCore.Builder
{
public static class EndpointConventionBuilderExtensions
{
public static IEndpointConventionBuilder RequireAuthorization(this IEndpointConventionBuilder builder, params string[] roles)
{
builder.Apply(endpointBuilder => endpointBuilder.Metadata.Add(new AuthorizationMetadata(roles)));
return builder;
}
}
}

View File

@ -11,9 +11,9 @@ using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder
{
public static class EndpointDataSourceBuilderExtensions
public static class EndpointRouteBuilderExtensions
{
public static EndpointBuilder MapHello(this EndpointDataSourceBuilder builder, string template, string greeter)
public static IEndpointConventionBuilder MapHello(this IEndpointRouteBuilder builder, string template, string greeter)
{
if (builder == null)
{
@ -24,10 +24,10 @@ namespace Microsoft.AspNetCore.Builder
.UseHello(greeter)
.Build();
return builder.MapEndpoint(
pipeline,
return builder.Map(
template,
"Hello");
"Hello " + greeter,
pipeline);
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
@ -36,19 +37,29 @@ namespace RoutingSample.Web
{
builder.MapHello("/helloworld", "World");
builder.MapEndpoint(
builder.MapHello("/helloworld-secret", "Secret World")
.RequireAuthorization("swordfish");
builder.MapGet(
"/",
(httpContext) =>
{
var dataSource = httpContext.RequestServices.GetRequiredService<EndpointDataSource>();
var sb = new StringBuilder();
sb.AppendLine("Endpoints:");
foreach (var endpoint in dataSource.Endpoints.OfType<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase))
{
sb.AppendLine($"- {endpoint.RoutePattern.RawText}");
}
var response = httpContext.Response;
var payloadLength = _homePayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_homePayload, 0, payloadLength);
},
"/",
"Home");
builder.MapEndpoint(
return response.WriteAsync(sb.ToString());
});
builder.MapGet(
"/plaintext",
(httpContext) =>
{
var response = httpContext.Response;
@ -57,30 +68,28 @@ namespace RoutingSample.Web
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength);
},
"/plaintext",
"Plaintext");
builder.MapEndpoint(
});
builder.MapGet(
"/withconstraints/{id:endsWith(_001)}",
(httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("WithConstraints");
},
"/withconstraints/{id:endsWith(_001)}",
"withconstraints");
builder.MapEndpoint(
});
builder.MapGet(
"/withoptionalconstraints/{id:endsWith(_001)?}",
(httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("withoptionalconstraints");
},
"/withoptionalconstraints/{id:endsWith(_001)?}",
"withoptionalconstraints");
builder.MapEndpoint(
});
builder.MapGet(
"/graph",
"DFA Graph",
(httpContext) =>
{
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
@ -91,11 +100,9 @@ namespace RoutingSample.Web
}
return Task.CompletedTask;
},
"/graph",
"DFA Graph",
new HttpMethodMetadata(new[] { "GET", }));
builder.MapEndpoint(
});
builder.MapGet(
"/WithSingleAsteriskCatchAll/{*path}",
(httpContext) =>
{
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
@ -106,10 +113,9 @@ namespace RoutingSample.Web
return response.WriteAsync(
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { }));
},
"/WithSingleAsteriskCatchAll/{*path}",
"WithSingleAsteriskCatchAll",
new RouteValuesAddressMetadata(routeName: "WithSingleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
builder.MapEndpoint(
builder.MapGet(
"/WithDoubleAsteriskCatchAll/{**path}",
(httpContext) =>
{
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
@ -120,12 +126,12 @@ namespace RoutingSample.Web
return response.WriteAsync(
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
},
"/WithDoubleAsteriskCatchAll/{**path}",
"WithDoubleAsteriskCatchAll",
new RouteValuesAddressMetadata(routeName: "WithDoubleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
});
app.UseStaticFiles();
app.UseAuthorization();
app.UseEndpoint();
}

View File

@ -0,0 +1,210 @@
// 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;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder
{
public static class EndpointRouteBuilderExtensions
{
// Avoid creating a new array every call
private static readonly string[] GetVerb = new[] { "GET" };
private static readonly string[] PostVerb = new[] { "POST" };
private static readonly string[] PutVerb = new[] { "PUT" };
private static readonly string[] DeleteVerb = new[] { "DELETE" };
#region MapVerbs
public static IEndpointConventionBuilder MapGet(
this IEndpointRouteBuilder builder,
string pattern,
RequestDelegate requestDelegate,
params object[] metadata)
{
return MapVerbs(builder, pattern, displayName: null, requestDelegate, GetVerb, metadata);
}
public static IEndpointConventionBuilder MapGet(
this IEndpointRouteBuilder builder,
string pattern,
string displayName,
RequestDelegate requestDelegate,
params object[] metadata)
{
return MapVerbs(builder, pattern, displayName, requestDelegate, GetVerb, metadata);
}
public static IEndpointConventionBuilder MapPost(
this IEndpointRouteBuilder builder,
string pattern,
RequestDelegate requestDelegate,
params object[] metadata)
{
return MapVerbs(builder, pattern, displayName: null, requestDelegate, PostVerb, metadata);
}
public static IEndpointConventionBuilder MapPost(
this IEndpointRouteBuilder builder,
string pattern,
string displayName,
RequestDelegate requestDelegate,
params object[] metadata)
{
return MapVerbs(builder, pattern, displayName, requestDelegate, PostVerb, metadata);
}
public static IEndpointConventionBuilder MapPut(
this IEndpointRouteBuilder builder,
string pattern,
RequestDelegate requestDelegate,
params object[] metadata)
{
return MapVerbs(builder, pattern, displayName: null, requestDelegate, PutVerb, metadata);
}
public static IEndpointConventionBuilder MapPut(
this IEndpointRouteBuilder builder,
string pattern,
string displayName,
RequestDelegate requestDelegate,
params object[] metadata)
{
return MapVerbs(builder, pattern, displayName, requestDelegate, PutVerb, metadata);
}
public static IEndpointConventionBuilder MapDelete(
this IEndpointRouteBuilder builder,
string pattern,
RequestDelegate requestDelegate,
params object[] metadata)
{
return MapVerbs(builder, pattern, displayName: null, requestDelegate, DeleteVerb, metadata);
}
public static IEndpointConventionBuilder MapDelete(
this IEndpointRouteBuilder builder,
string pattern,
string displayName,
RequestDelegate requestDelegate,
params object[] metadata)
{
return MapVerbs(builder, pattern, displayName, requestDelegate, DeleteVerb, metadata);
}
public static IEndpointConventionBuilder MapVerbs(
this IEndpointRouteBuilder builder,
string pattern,
RequestDelegate requestDelegate,
IList<string> httpMethods,
params object[] metadata)
{
return MapVerbs(builder, pattern, displayName: null, requestDelegate, httpMethods, metadata);
}
public static IEndpointConventionBuilder MapVerbs(
this IEndpointRouteBuilder builder,
string pattern,
string displayName,
RequestDelegate requestDelegate,
IList<string> httpMethods,
params object[] metadata)
{
if (httpMethods == null)
{
throw new ArgumentNullException(nameof(httpMethods));
}
var resolvedMetadata = new List<object>();
resolvedMetadata.Add(new HttpMethodMetadata(httpMethods));
if (metadata != null)
{
resolvedMetadata.AddRange(metadata);
}
return Map(builder, pattern, displayName ?? $"{pattern} HTTP: {string.Join(", ", httpMethods)}", requestDelegate, metadata: resolvedMetadata.ToArray());
}
#endregion
#region Map
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder builder,
string pattern,
RequestDelegate requestDelegate,
params object[] metadata)
{
return Map(builder, RoutePatternFactory.Parse(pattern), pattern, requestDelegate, metadata);
}
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder builder,
string pattern,
string displayName,
RequestDelegate requestDelegate,
params object[] metadata)
{
return Map(builder, RoutePatternFactory.Parse(pattern), displayName, requestDelegate, metadata);
}
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder builder,
RoutePattern pattern,
RequestDelegate requestDelegate,
params object[] metadata)
{
return Map(builder, pattern, pattern.RawText ?? pattern.DebuggerToString(), requestDelegate, metadata);
}
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder builder,
RoutePattern pattern,
string displayName,
RequestDelegate requestDelegate,
params object[] metadata)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
}
if (requestDelegate == null)
{
throw new ArgumentNullException(nameof(requestDelegate));
}
const int defaultOrder = 0;
var routeEndpointModel = new RouteEndpointModel(
requestDelegate,
pattern,
defaultOrder);
routeEndpointModel.DisplayName = displayName;
if (metadata != null)
{
foreach (var item in metadata)
{
routeEndpointModel.Metadata.Add(item);
}
}
var modelEndpointDataSource = builder.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (modelEndpointDataSource == null)
{
modelEndpointDataSource = new ModelEndpointDataSource();
builder.DataSources.Add(modelEndpointDataSource);
}
return modelEndpointDataSource.AddEndpointModel(routeEndpointModel);
}
#endregion
}
}

View File

@ -2,10 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Builder
{
@ -14,25 +14,36 @@ namespace Microsoft.AspNetCore.Builder
// Property key is used by MVC package to check that routing is registered
private const string EndpointRoutingRegisteredKey = "__EndpointRoutingMiddlewareRegistered";
public static IApplicationBuilder UseEndpointRouting(this IApplicationBuilder builder)
public static IApplicationBuilder UseEndpointRouting(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure)
{
return builder.UseEndpointRouting(null);
}
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
public static IApplicationBuilder UseEndpointRouting(this IApplicationBuilder builder, Action<EndpointDataSourceBuilder> configure)
{
VerifyRoutingIsRegistered(builder);
if (configure != null)
var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
EndpointDataSource middlewareEndpointDataSource;
var endpointRouteBuilder = builder.ApplicationServices.GetRequiredService<IEndpointRouteBuilder>();
if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultEndpointRouteBuilder)
{
var dataSourceBuilder = (DefaultEndpointDataSourceBuilder)builder.ApplicationServices.GetRequiredService<EndpointDataSourceBuilder>();
dataSourceBuilder.ApplicationBuilder = builder;
configure(dataSourceBuilder);
defaultEndpointRouteBuilder.ApplicationBuilder = builder;
}
configure(endpointRouteBuilder);
foreach (var dataSource in endpointRouteBuilder.DataSources)
{
routeOptions.Value.EndpointDataSources.Add(dataSource);
}
// Create endpoint data source for data sources registered in configure
middlewareEndpointDataSource = new CompositeEndpointDataSource(endpointRouteBuilder.DataSources);
builder.Properties[EndpointRoutingRegisteredKey] = true;
return builder.UseMiddleware<EndpointRoutingMiddleware>();
return builder.UseMiddleware<EndpointRoutingMiddleware>(middlewareEndpointDataSource);
}
public static IApplicationBuilder UseEndpoint(this IApplicationBuilder builder)

View File

@ -1,69 +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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder
{
public static class MapEndpointEndpointDataSourceBuilderExtensions
{
public static RouteEndpointBuilder MapEndpoint(
this EndpointDataSourceBuilder builder,
RequestDelegate requestDelegate,
string pattern,
string displayName)
{
return MapEndpoint(builder, requestDelegate, pattern, displayName, metadata: null);
}
public static RouteEndpointBuilder MapEndpoint(
this EndpointDataSourceBuilder builder,
RequestDelegate requestDelegate,
RoutePattern pattern,
string displayName)
{
return MapEndpoint(builder, requestDelegate, pattern, displayName, metadata: null);
}
public static RouteEndpointBuilder MapEndpoint(
this EndpointDataSourceBuilder builder,
RequestDelegate requestDelegate,
string pattern,
string displayName,
params object[] metadata)
{
return MapEndpoint(builder, requestDelegate, RoutePatternFactory.Parse(pattern), displayName, metadata);
}
public static RouteEndpointBuilder MapEndpoint(
this EndpointDataSourceBuilder builder,
RequestDelegate requestDelegate,
RoutePattern pattern,
string displayName,
params object[] metadata)
{
const int defaultOrder = 0;
var endpointBuilder = new RouteEndpointBuilder(
requestDelegate,
pattern,
defaultOrder);
endpointBuilder.DisplayName = displayName;
if (metadata != null)
{
foreach (var item in metadata)
{
endpointBuilder.Metadata.Add(item);
}
}
builder.Endpoints.Add(endpointBuilder);
return endpointBuilder;
}
}
}

View File

@ -1,34 +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;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Routing
{
internal class BuilderEndpointDataSource : EndpointDataSource
{
private readonly EndpointDataSourceBuilder _builder;
public BuilderEndpointDataSource(EndpointDataSourceBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
_builder = builder;
}
public override IChangeToken GetChangeToken()
{
return NullChangeToken.Singleton;
}
public override IReadOnlyList<Endpoint> Endpoints => _builder.Endpoints.Select(b => b.Build()).ToArray();
}
}

View File

@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Text;
@ -18,24 +20,49 @@ namespace Microsoft.AspNetCore.Routing
[DebuggerDisplay("{DebuggerDisplayString,nq}")]
public sealed class CompositeEndpointDataSource : EndpointDataSource
{
private readonly EndpointDataSource[] _dataSources;
private readonly object _lock;
private readonly ICollection<EndpointDataSource> _dataSources;
private IReadOnlyList<Endpoint> _endpoints;
private IChangeToken _consumerChangeToken;
private CancellationTokenSource _cts;
internal CompositeEndpointDataSource(IEnumerable<EndpointDataSource> dataSources)
private CompositeEndpointDataSource()
{
if (dataSources == null)
{
throw new ArgumentNullException(nameof(dataSources));
}
CreateChangeToken();
_dataSources = dataSources.ToArray();
_lock = new object();
}
internal CompositeEndpointDataSource(ObservableCollection<EndpointDataSource> dataSources) : this()
{
dataSources.CollectionChanged += OnDataSourcesChanged;
_dataSources = dataSources;
}
public CompositeEndpointDataSource(IEnumerable<EndpointDataSource> endpointDataSources) : this()
{
_dataSources = new List<EndpointDataSource>();
foreach (var dataSource in endpointDataSources)
{
_dataSources.Add(dataSource);
}
}
private void OnDataSourcesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
lock (_lock)
{
// Only trigger changes if composite data source has already initialized endpoints
if (_endpoints != null)
{
HandleChange();
}
}
}
public IEnumerable<EndpointDataSource> DataSources => _dataSources;
/// <summary>
/// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/>
/// instances.

View File

@ -3,16 +3,17 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.DependencyInjection
{
internal class ConfigureEndpointOptions : IConfigureOptions<EndpointOptions>
internal class ConfigureRouteOptions : IConfigureOptions<RouteOptions>
{
private readonly IEnumerable<EndpointDataSource> _dataSources;
private readonly ICollection<EndpointDataSource> _dataSources;
public ConfigureEndpointOptions(IEnumerable<EndpointDataSource> dataSources)
public ConfigureRouteOptions(ICollection<EndpointDataSource> dataSources)
{
if (dataSources == null)
{
@ -22,17 +23,14 @@ namespace Microsoft.Extensions.DependencyInjection
_dataSources = dataSources;
}
public void Configure(EndpointOptions options)
public void Configure(RouteOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
foreach (var dataSource in _dataSources)
{
options.DataSources.Add(dataSource);
}
options.EndpointDataSources = _dataSources;
}
}
}

View File

@ -1,17 +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.Builder;
namespace Microsoft.AspNetCore.Routing
{
internal class DefaultEndpointDataSourceBuilder : EndpointDataSourceBuilder
{
public IApplicationBuilder ApplicationBuilder { get; set; }
public override ICollection<EndpointBuilder> Endpoints { get; } = new List<EndpointBuilder>();
public override IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New();
}
}

View File

@ -0,0 +1,25 @@
// 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.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Routing
{
internal class DefaultEndpointRouteBuilder : IEndpointRouteBuilder
{
public DefaultEndpointRouteBuilder()
{
DataSources = new List<EndpointDataSource>();
}
public IApplicationBuilder ApplicationBuilder { get; set; }
public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New();
public ICollection<EndpointDataSource> DataSources { get; }
public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices;
}
}

View File

@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing
public DefaultLinkGenerator(
ParameterPolicyFactory parameterPolicyFactory,
CompositeEndpointDataSource dataSource,
EndpointDataSource dataSource,
ObjectPool<UriBuildingContext> uriBuildingContextPool,
IOptions<RouteOptions> routeOptions,
ILogger<DefaultLinkGenerator> logger,

View File

@ -2,6 +2,7 @@
// 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.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Matching;
@ -49,21 +50,22 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton(typeof(RoutingMarkerService));
// Collect all data sources from DI.
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<EndpointOptions>, ConfigureEndpointOptions>());
// Setup global collection of endpoint data sources
var dataSources = new ObservableCollection<EndpointDataSource>();
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, ConfigureRouteOptions>(
serviceProvider => new ConfigureRouteOptions(dataSources)));
// Allow global access to the list of endpoints.
services.TryAddSingleton<CompositeEndpointDataSource>(s =>
services.TryAddSingleton<EndpointDataSource>(s =>
{
var options = s.GetRequiredService<IOptions<EndpointOptions>>();
return new CompositeEndpointDataSource(options.Value.DataSources);
// Call internal ctor and pass global collection
return new CompositeEndpointDataSource(dataSources);
});
//
// Endpoint Infrastructure
//
services.TryAddSingleton<EndpointDataSource, BuilderEndpointDataSource>();
services.TryAddSingleton<EndpointDataSourceBuilder, DefaultEndpointDataSourceBuilder>();
services.TryAddTransient<IEndpointRouteBuilder, DefaultEndpointRouteBuilder>();
//
// Default matcher implementation

View File

@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing
{
public abstract class EndpointBuilder
public abstract class EndpointModel
{
public RequestDelegate RequestDelegate { get; set; }

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Routing
{
private readonly DataSourceDependentCache<Dictionary<string, Endpoint[]>> _cache;
public EndpointNameAddressScheme(CompositeEndpointDataSource dataSource)
public EndpointNameAddressScheme(EndpointDataSource dataSource)
{
_cache = new DataSourceDependentCache<Dictionary<string, Endpoint[]>>(dataSource, Initialize);
}

View File

@ -1,13 +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 Microsoft.AspNetCore.Routing
{
// Internal for 2.2. Public API for configuring endpoints will be added in 3.0
internal class EndpointOptions
{
public IList<EndpointDataSource> DataSources { get; } = new List<EndpointDataSource>();
}
}

View File

@ -16,14 +16,14 @@ namespace Microsoft.AspNetCore.Routing
{
private readonly MatcherFactory _matcherFactory;
private readonly ILogger _logger;
private readonly CompositeEndpointDataSource _endpointDataSource;
private readonly EndpointDataSource _endpointDataSource;
private readonly RequestDelegate _next;
private Task<Matcher> _initializationTask;
public EndpointRoutingMiddleware(
MatcherFactory matcherFactory,
CompositeEndpointDataSource endpointDataSource,
EndpointDataSource endpointDataSource,
ILogger<EndpointRoutingMiddleware> logger,
RequestDelegate next)
{

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;
namespace Microsoft.AspNetCore.Routing
{
public interface IEndpointConventionBuilder
{
void Apply(Action<EndpointModel> convention);
}
}

View File

@ -1,15 +1,18 @@
// 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.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Routing
{
public abstract class EndpointDataSourceBuilder
public interface IEndpointRouteBuilder
{
public abstract ICollection<EndpointBuilder> Endpoints { get; }
IApplicationBuilder CreateApplicationBuilder();
public abstract IApplicationBuilder CreateApplicationBuilder();
IServiceProvider ServiceProvider { get; }
ICollection<EndpointDataSource> DataSources { get; }
}
}

View File

@ -0,0 +1,68 @@
// 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;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Routing
{
internal class ModelEndpointDataSource : EndpointDataSource
{
private List<EndpointConventionBuilder> _endpointConventionBuilders;
public ModelEndpointDataSource()
{
_endpointConventionBuilders = new List<EndpointConventionBuilder>();
}
public IEndpointConventionBuilder AddEndpointModel(EndpointModel endpointModel)
{
var builder = new EndpointConventionBuilder(endpointModel);
_endpointConventionBuilders.Add(builder);
return builder;
}
public override IChangeToken GetChangeToken()
{
return NullChangeToken.Singleton;
}
public override IReadOnlyList<Endpoint> Endpoints => _endpointConventionBuilders.Select(e => e.Build()).ToArray();
// for testing
internal IEnumerable<EndpointModel> EndpointModels => _endpointConventionBuilders.Select(b => b.EndpointModel);
private class EndpointConventionBuilder : IEndpointConventionBuilder
{
internal EndpointModel EndpointModel { get; }
private readonly List<Action<EndpointModel>> _conventions;
public EndpointConventionBuilder(EndpointModel endpointModel)
{
EndpointModel = endpointModel;
_conventions = new List<Action<EndpointModel>>();
}
public void Apply(Action<EndpointModel> convention)
{
_conventions.Add(convention);
}
public Endpoint Build()
{
foreach (var convention in _conventions)
{
convention(EndpointModel);
}
return EndpointModel.Build();
}
}
}
}

View File

@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
return null;
}
private string DebuggerToString()
internal string DebuggerToString()
{
return RawText ?? string.Join(SeparatorString, PathSegments.Select(s => s.DebuggerToString()));
}

View File

@ -6,13 +6,13 @@ using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing
{
public sealed class RouteEndpointBuilder : EndpointBuilder
public sealed class RouteEndpointModel : EndpointModel
{
public RoutePattern RoutePattern { get; set; }
public int Order { get; set; }
public RouteEndpointBuilder(
public RouteEndpointModel(
RequestDelegate requestDelegate,
RoutePattern routePattern,
int order)
@ -24,14 +24,14 @@ namespace Microsoft.AspNetCore.Routing
public override Endpoint Build()
{
var matcherEndpoint = new RouteEndpoint(
var routeEndpoint = new RouteEndpoint(
RequestDelegate,
RoutePattern,
Order,
new EndpointMetadataCollection(Metadata),
DisplayName);
return matcherEndpoint;
return routeEndpoint;
}
}
}

View File

@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Routing
{
public class RouteOptions
{
public ICollection<EndpointDataSource> EndpointDataSources { get; internal set; }
/// <summary>
/// Gets or sets a value indicating whether all generated paths URLs are lower-case.
/// Use <see cref="LowercaseQueryStrings" /> to configure the behavior for query strings.

View File

@ -14,11 +14,11 @@ namespace Microsoft.AspNetCore.Routing
{
internal class RouteValuesAddressScheme : IEndpointAddressScheme<RouteValuesAddress>
{
private readonly CompositeEndpointDataSource _dataSource;
private readonly EndpointDataSource _dataSource;
private LinkGenerationDecisionTree _allMatchesLinkGenerationTree;
private Dictionary<string, List<OutboundMatchResult>> _namedMatchResults;
public RouteValuesAddressScheme(CompositeEndpointDataSource dataSource)
public RouteValuesAddressScheme(EndpointDataSource dataSource)
{
_dataSource = dataSource;

View File

@ -29,7 +29,6 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
{
// Arrange
var expectedContentType = "text/plain";
var expectedContent = "Endpoint Routing sample endpoints:" + Environment.NewLine + "/plaintext";
// Act
var response = await _client.GetAsync("/");
@ -39,8 +38,6 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
Assert.NotNull(response.Content);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType);
var actualContent = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedContent, actualContent);
}
[Fact]

View File

@ -2,13 +2,17 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Builder.Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Moq;
using Xunit;
@ -23,7 +27,7 @@ namespace Microsoft.AspNetCore.Builder
var app = new ApplicationBuilder(Mock.Of<IServiceProvider>());
// Act
var ex = Assert.Throws<InvalidOperationException>(() => app.UseEndpointRouting());
var ex = Assert.Throws<InvalidOperationException>(() => app.UseEndpointRouting(builder => { }));
// Assert
Assert.Equal(
@ -58,7 +62,7 @@ namespace Microsoft.AspNetCore.Builder
var app = new ApplicationBuilder(services);
app.UseEndpointRouting();
app.UseEndpointRouting(builder => { });
var appFunc = app.Build();
var httpContext = new DefaultHttpContext();
@ -75,17 +79,20 @@ namespace Microsoft.AspNetCore.Builder
{
// Arrange
var endpoint = new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
RoutePatternFactory.Parse("{*p}"),
0,
EndpointMetadataCollection.Empty,
"Test");
TestConstants.EmptyRequestDelegate,
RoutePatternFactory.Parse("{*p}"),
0,
EndpointMetadataCollection.Empty,
"Test");
var services = CreateServices(endpoint);
var services = CreateServices();
var app = new ApplicationBuilder(services);
app.UseEndpointRouting();
app.UseEndpointRouting(builder =>
{
builder.DataSources.Add(new DefaultEndpointDataSource(endpoint));
});
var appFunc = app.Build();
var httpContext = new DefaultHttpContext();
@ -126,7 +133,7 @@ namespace Microsoft.AspNetCore.Builder
var app = new ApplicationBuilder(services);
app.UseEndpointRouting();
app.UseEndpointRouting(builder => { });
app.UseEndpoint();
var appFunc = app.Build();
@ -140,36 +147,83 @@ namespace Microsoft.AspNetCore.Builder
}
[Fact]
public void UseEndpointRouting_CallWithBuilder_SetsEndpointBuilder()
public void UseEndpointRouting_CallWithBuilder_SetsEndpointDataSource()
{
// Arrange
var services = CreateServices();
var matcherEndpointDataSources = new List<EndpointDataSource>();
var matcherFactoryMock = new Mock<MatcherFactory>();
matcherFactoryMock
.Setup(m => m.CreateMatcher(It.IsAny<EndpointDataSource>()))
.Callback((EndpointDataSource arg) =>
{
matcherEndpointDataSources.Add(arg);
})
.Returns(new TestMatcher(false));
var services = CreateServices(matcherFactoryMock.Object);
var app = new ApplicationBuilder(services);
// Act
app.UseEndpointRouting(builder =>
{
builder.MapEndpoint(d => null, "/", "Test endpoint");
builder.Map("/1", "Test endpoint 1", d => null);
builder.Map("/2", "Test endpoint 2", d => null);
});
app.UseEndpointRouting(builder =>
{
builder.Map("/3", "Test endpoint 3", d => null);
builder.Map("/4", "Test endpoint 4", d => null);
});
// This triggers the middleware to be created and the matcher factory to be called
// with the datasource we want to test
var requestDelegate = app.Build();
requestDelegate(new DefaultHttpContext());
// Assert
var dataSourceBuilder = (DefaultEndpointDataSourceBuilder)services.GetRequiredService<EndpointDataSourceBuilder>();
var endpointBuilder = Assert.Single(dataSourceBuilder.Endpoints);
Assert.Equal("Test endpoint", endpointBuilder.DisplayName);
Assert.Equal(2, matcherEndpointDataSources.Count);
// Each middleware has its own endpoints
Assert.Collection(matcherEndpointDataSources[0].Endpoints,
e => Assert.Equal("Test endpoint 1", e.DisplayName),
e => Assert.Equal("Test endpoint 2", e.DisplayName));
Assert.Collection(matcherEndpointDataSources[1].Endpoints,
e => Assert.Equal("Test endpoint 3", e.DisplayName),
e => Assert.Equal("Test endpoint 4", e.DisplayName));
var compositeEndpointBuilder = services.GetRequiredService<EndpointDataSource>();
// Global middleware has all endpoints
Assert.Collection(compositeEndpointBuilder.Endpoints,
e => Assert.Equal("Test endpoint 1", e.DisplayName),
e => Assert.Equal("Test endpoint 2", e.DisplayName),
e => Assert.Equal("Test endpoint 3", e.DisplayName),
e => Assert.Equal("Test endpoint 4", e.DisplayName));
}
private IServiceProvider CreateServices(params Endpoint[] endpoints)
private IServiceProvider CreateServices()
{
return CreateServices(matcherFactory: null);
}
private IServiceProvider CreateServices(MatcherFactory matcherFactory)
{
var services = new ServiceCollection();
if (matcherFactory != null)
{
services.AddSingleton<MatcherFactory>(matcherFactory);
}
services.AddLogging();
services.AddOptions();
services.AddRouting();
services.AddSingleton<EndpointDataSource>(new DefaultEndpointDataSource(endpoints));
var serviceProvder = services.BuildServiceProvider();
return services.BuildServiceProvider();
return serviceProvder;
}
}
}

View File

@ -14,38 +14,50 @@ namespace Microsoft.AspNetCore.Builder
{
public class MapEndpointEndpointDataSourceBuilderExtensionsTest
{
private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder)
{
return Assert.IsType<ModelEndpointDataSource>(Assert.Single(endpointRouteBuilder.DataSources));
}
private RouteEndpointModel GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder)
{
return Assert.IsType<RouteEndpointModel>(Assert.Single(GetBuilderEndpointDataSource(endpointRouteBuilder).EndpointModels));
}
[Fact]
public void MapEndpoint_StringPattern_BuildsEndpoint()
{
// Arrange
var builder = new DefaultEndpointDataSourceBuilder();
var builder = new DefaultEndpointRouteBuilder();
RequestDelegate requestDelegate = (d) => null;
// Act
var endpointBuilder = builder.MapEndpoint(requestDelegate, "/", "Display name!");
var endpointBuilder = builder.Map("/", "Display name!", requestDelegate);
// Assert
Assert.Equal(endpointBuilder, Assert.Single(builder.Endpoints));
Assert.Equal(requestDelegate, endpointBuilder.RequestDelegate);
Assert.Equal("Display name!", endpointBuilder.DisplayName);
Assert.Equal("/", endpointBuilder.RoutePattern.RawText);
var endpointBuilder1 = GetRouteEndpointBuilder(builder);
Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate);
Assert.Equal("Display name!", endpointBuilder1.DisplayName);
Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
}
[Fact]
public void MapEndpoint_TypedPattern_BuildsEndpoint()
{
// Arrange
var builder = new DefaultEndpointDataSourceBuilder();
var builder = new DefaultEndpointRouteBuilder();
RequestDelegate requestDelegate = (d) => null;
// Act
var endpointBuilder = builder.MapEndpoint(requestDelegate, RoutePatternFactory.Parse("/"), "Display name!");
var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), "Display name!", requestDelegate);
// Assert
Assert.Equal(endpointBuilder, Assert.Single(builder.Endpoints));
Assert.Equal(requestDelegate, endpointBuilder.RequestDelegate);
Assert.Equal("Display name!", endpointBuilder.DisplayName);
Assert.Equal("/", endpointBuilder.RoutePattern.RawText);
var endpointBuilder1 = GetRouteEndpointBuilder(builder);
Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate);
Assert.Equal("Display name!", endpointBuilder1.DisplayName);
Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
}
[Fact]
@ -53,18 +65,18 @@ namespace Microsoft.AspNetCore.Builder
{
// Arrange
var metadata = new object();
var builder = new DefaultEndpointDataSourceBuilder();
var builder = new DefaultEndpointRouteBuilder();
RequestDelegate requestDelegate = (d) => null;
// Act
var endpointBuilder = builder.MapEndpoint(requestDelegate, "/", "Display name!", new[] { metadata });
var endpointBuilder = builder.Map("/", "Display name!", requestDelegate, new[] { metadata });
// Assert
Assert.Equal(endpointBuilder, Assert.Single(builder.Endpoints));
Assert.Equal(requestDelegate, endpointBuilder.RequestDelegate);
Assert.Equal("Display name!", endpointBuilder.DisplayName);
Assert.Equal("/", endpointBuilder.RoutePattern.RawText);
Assert.Equal(metadata, Assert.Single(endpointBuilder.Metadata));
var endpointBuilder1 = GetRouteEndpointBuilder(builder);
Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate);
Assert.Equal("Display name!", endpointBuilder1.DisplayName);
Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
Assert.Equal(metadata, Assert.Single(endpointBuilder1.Metadata));
}
[Fact]
@ -72,18 +84,18 @@ namespace Microsoft.AspNetCore.Builder
{
// Arrange
var metadata = new object();
var builder = new DefaultEndpointDataSourceBuilder();
var builder = new DefaultEndpointRouteBuilder();
RequestDelegate requestDelegate = (d) => null;
// Act
var endpointBuilder = builder.MapEndpoint(requestDelegate, RoutePatternFactory.Parse("/"), "Display name!", new[] { metadata });
var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), "Display name!", requestDelegate, new[] { metadata });
// Assert
Assert.Equal(endpointBuilder, Assert.Single(builder.Endpoints));
Assert.Equal(requestDelegate, endpointBuilder.RequestDelegate);
Assert.Equal("Display name!", endpointBuilder.DisplayName);
Assert.Equal("/", endpointBuilder.RoutePattern.RawText);
Assert.Equal(metadata, Assert.Single(endpointBuilder.Metadata));
var endpointBuilder1 = GetRouteEndpointBuilder(builder);
Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate);
Assert.Equal("Display name!", endpointBuilder1.DisplayName);
Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
Assert.Equal(metadata, Assert.Single(endpointBuilder1.Metadata));
}
}
}

View File

@ -1,11 +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 System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
@ -259,7 +261,12 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }, endpoints: new[] { endpoint, });
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o => o.LowercaseUrls = true);
};
var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, });
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
// Act
@ -283,7 +290,12 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("Foo/{bar=BAR}/{id?}");
var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }, endpoints: new[] { endpoint, });
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o => o.LowercaseUrls = true);
};
var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, });
var httpContext = CreateHttpContext();
// Act
@ -334,7 +346,12 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }, endpoints: new[] { endpoint, });
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o => o.LowercaseUrls = true);
};
var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, });
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
// Act
@ -362,8 +379,17 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o =>
{
o.LowercaseUrls = true;
o.LowercaseQueryStrings = true;
});
};
var linkGenerator = CreateLinkGenerator(
new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true },
configure,
endpoints: new[] { endpoint, });
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
@ -387,8 +413,13 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o => o.AppendTrailingSlash = true);
};
var linkGenerator = CreateLinkGenerator(
new RouteOptions() { AppendTrailingSlash = true },
configure,
endpoints: new[] { endpoint });
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
@ -412,8 +443,18 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o =>
{
o.LowercaseUrls = true;
o.LowercaseQueryStrings = true;
o.AppendTrailingSlash = true;
});
};
var linkGenerator = CreateLinkGenerator(
new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true, AppendTrailingSlash = true },
configure,
endpoints: new[] { endpoint });
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
@ -437,8 +478,13 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o => o.LowercaseUrls = true);
};
var linkGenerator = CreateLinkGenerator(
new RouteOptions() { LowercaseUrls = true },
configure,
endpoints: new[] { endpoint });
var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" });
@ -466,8 +512,13 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o => o.LowercaseUrls = false);
};
var linkGenerator = CreateLinkGenerator(
new RouteOptions() { LowercaseUrls = false },
configure,
endpoints: new[] { endpoint });
var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" });
@ -494,8 +545,17 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o =>
{
o.LowercaseUrls = true;
o.LowercaseQueryStrings = true;
});
};
var linkGenerator = CreateLinkGenerator(
new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true },
configure,
endpoints: new[] { endpoint });
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
@ -523,8 +583,17 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o =>
{
o.LowercaseUrls = false;
o.LowercaseQueryStrings = false;
});
};
var linkGenerator = CreateLinkGenerator(
new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = false },
configure,
endpoints: new[] { endpoint });
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });
@ -552,8 +621,13 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}");
Action<IServiceCollection> configure = (s) =>
{
s.Configure<RouteOptions>(o => o.AppendTrailingSlash = false);
};
var linkGenerator = CreateLinkGenerator(
new RouteOptions() { AppendTrailingSlash = false },
configure,
endpoints: new[] { endpoint });
var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" });

View File

@ -177,10 +177,15 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), });
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), });
var routeOptions = new RouteOptions();
routeOptions.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
Action<IServiceCollection> configureServices = s =>
{
s.Configure<RouteOptions>(o =>
{
o.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
};
var linkGenerator = CreateLinkGenerator(routeOptions: routeOptions, configureServices: null, endpoint1, endpoint2);
var linkGenerator = CreateLinkGenerator(configureServices, endpoint1, endpoint2);
// Act
var path = linkGenerator.GetPathByAddress(
@ -198,10 +203,15 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), });
var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), });
var routeOptions = new RouteOptions();
routeOptions.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
Action<IServiceCollection> configureServices = s =>
{
s.Configure<RouteOptions>(o =>
{
o.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
};
var linkGenerator = CreateLinkGenerator(routeOptions: routeOptions, configureServices: null, endpoint1, endpoint2);
var linkGenerator = CreateLinkGenerator(configureServices, endpoint1, endpoint2);
// Act
var path = linkGenerator.GetPathByAddress(
@ -313,15 +323,15 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}");
var routeOptions = new RouteOptions();
routeOptions.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
Action<IServiceCollection> configure = (s) =>
{
s.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform());
s.Configure<RouteOptions>(o =>
{
o.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
});
};
var linkGenerator = CreateLinkGenerator(routeOptions, configure, endpoint);
var linkGenerator = CreateLinkGenerator(configure, endpoint);
// Act
var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test" });
@ -336,15 +346,15 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}", policies: new { c = new UpperCaseParameterTransform(), });
var routeOptions = new RouteOptions();
routeOptions.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
Action<IServiceCollection> configure = (s) =>
{
s.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform());
s.Configure<RouteOptions>(o =>
{
o.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
});
};
var linkGenerator = CreateLinkGenerator(routeOptions, configure, endpoint);
var linkGenerator = CreateLinkGenerator(configure, endpoint);
// Act
var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test", c = "hithere", });
@ -707,9 +717,9 @@ namespace Microsoft.AspNetCore.Routing
private class IntAddressScheme : IEndpointAddressScheme<int>
{
private readonly CompositeEndpointDataSource _dataSource;
private readonly EndpointDataSource _dataSource;
public IntAddressScheme(CompositeEndpointDataSource dataSource)
public IntAddressScheme(EndpointDataSource dataSource)
{
_dataSource = dataSource;
}

View File

@ -151,10 +151,9 @@ namespace Microsoft.AspNetCore.Routing
logger = logger ?? new Logger<EndpointRoutingMiddleware>(NullLoggerFactory.Instance);
matcherFactory = matcherFactory ?? new TestMatcherFactory(true);
var options = Options.Create(new EndpointOptions());
var middleware = new EndpointRoutingMiddleware(
matcherFactory,
new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>()),
new DefaultEndpointDataSource(),
logger,
next);

View File

@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Routing
{
@ -44,29 +45,22 @@ namespace Microsoft.AspNetCore.Routing
private protected DefaultLinkGenerator CreateLinkGenerator(params Endpoint[] endpoints)
{
return CreateLinkGenerator(routeOptions: null, endpoints);
}
private protected DefaultLinkGenerator CreateLinkGenerator(RouteOptions routeOptions, params Endpoint[] endpoints)
{
return CreateLinkGenerator(routeOptions, configureServices: null, endpoints);
return CreateLinkGenerator(configureServices: null, endpoints);
}
private protected DefaultLinkGenerator CreateLinkGenerator(
RouteOptions routeOptions,
Action<IServiceCollection> configureServices,
params Endpoint[] endpoints)
{
return CreateLinkGenerator(routeOptions, configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty<Endpoint>()) });
return CreateLinkGenerator(configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty<Endpoint>()) });
}
private protected DefaultLinkGenerator CreateLinkGenerator(EndpointDataSource[] dataSources)
{
return CreateLinkGenerator(routeOptions: null, configureServices: null, dataSources);
return CreateLinkGenerator(configureServices: null, dataSources);
}
private protected DefaultLinkGenerator CreateLinkGenerator(
RouteOptions routeOptions,
Action<IServiceCollection> configureServices,
EndpointDataSource[] dataSources)
{
@ -74,25 +68,25 @@ namespace Microsoft.AspNetCore.Routing
AddAdditionalServices(services);
configureServices?.Invoke(services);
routeOptions = routeOptions ?? new RouteOptions();
dataSources = dataSources ?? Array.Empty<EndpointDataSource>();
services.Configure<EndpointOptions>((o) =>
services.Configure<RouteOptions>(o =>
{
for (var i = 0; i < dataSources.Length; i++)
if (dataSources != null)
{
o.DataSources.Add(dataSources[i]);
foreach (var dataSource in dataSources)
{
o.EndpointDataSources.Add(dataSource);
}
}
});
var options = Options.Create(routeOptions);
var serviceProvider = services.BuildServiceProvider();
var routeOptions = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
return new DefaultLinkGenerator(
new DefaultParameterPolicyFactory(options, serviceProvider),
serviceProvider.GetRequiredService<CompositeEndpointDataSource>(),
new DefaultParameterPolicyFactory(routeOptions, serviceProvider),
new CompositeEndpointDataSource(routeOptions.Value.EndpointDataSources),
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
options,
routeOptions,
NullLogger<DefaultLinkGenerator>.Instance,
serviceProvider);
}

View File

@ -8,18 +8,18 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matching
namespace Microsoft.AspNetCore.Routing
{
public class MatcherEndpointBuilderTest
public class RouteEndpointModelTest
{
[Fact]
public void Build_AllValuesSet_EndpointCreated()
{
const int defaultOrder = 0;
object metadata = new object();
var metadata = new object();
RequestDelegate requestDelegate = (d) => null;
var builder = new RouteEndpointBuilder(requestDelegate, RoutePatternFactory.Parse("/"), defaultOrder)
var builder = new RouteEndpointModel(requestDelegate, RoutePatternFactory.Parse("/"), defaultOrder)
{
DisplayName = "Display name!",
Metadata = { metadata }