Merge remote-tracking branch 'Routing/rybrande/masterToSrc' into rybrande/Mondo22ToMaster

This commit is contained in:
Ryan Brandenburg 2018-11-26 12:35:21 -08:00
commit 01b58c9a3d
100 changed files with 2690 additions and 4096 deletions

View File

@ -1,10 +1,6 @@
<Project>
<PropertyGroup>
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.2' ">$(MicrosoftNETCoreApp22PackageVersion)</RuntimeFrameworkVersion>
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp3.0' ">$(MicrosoftNETCoreAppPackageVersion)</RuntimeFrameworkVersion>
<NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
<!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
<NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
</PropertyGroup>
</Project>

View File

@ -1,10 +1,8 @@
ASP.NET Routing
===
ASP.NET Routing [Archived]
===========================
AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/fe4o5h1s9ve86nyv/branch/master?svg=true)](https://ci.appveyor.com/project/aspnetci/Routing/branch/master)
Travis: [![Travis](https://travis-ci.org/aspnet/Routing.svg?branch=master)](https://travis-ci.org/aspnet/Routing)
**This GitHub project has been archived.** Ongoing development on this project can be found in <https://github.com/aspnet/AspNetCore>.
Contains routing middleware for routing requests to application logic.
This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [AspNetCore](https://github.com/aspnet/AspNetCore) repo.

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TargetFramework Condition="'$(BenchmarksTargetFramework)' != ''">$(BenchmarksTargetFramework)</TargetFramework>
<UseP2PReferences Condition="'$(UseP2PReferences)'=='' AND '$(BenchmarksTargetFramework)'==''">true</UseP2PReferences>
</PropertyGroup>

View File

@ -3,11 +3,10 @@
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
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,14 +41,10 @@ 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

@ -8,7 +8,7 @@
},
"Source": {
"Repository": "https://github.com/aspnet/routing.git",
"BranchOrCommit": "release/2.2",
"BranchOrCommit": "master",
"Project": "benchmarkapps/Benchmarks/Benchmarks.csproj"
},
"Port": 8080

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<OutputType>Exe</OutputType>
<ServerGarbageCollection>true</ServerGarbageCollection>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

View File

@ -1,182 +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 BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.Routing
{
public class RouteValueDictionaryBenchmark
{
private RouteValueDictionary _arrayValues;
private RouteValueDictionary _propertyValues;
// We modify the route value dictionaries in many of these benchmarks.
[IterationSetup]
public void Setup()
{
_arrayValues = new RouteValueDictionary()
{
{ "action", "Index" },
{ "controller", "Home" },
{ "id", "17" },
};
_propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" });
}
[Benchmark]
public RouteValueDictionary AddSingleItem()
{
var dictionary = new RouteValueDictionary
{
{ "action", "Index" }
};
return dictionary;
}
[Benchmark]
public RouteValueDictionary AddThreeItems()
{
var dictionary = new RouteValueDictionary
{
{ "action", "Index" },
{ "controller", "Home" },
{ "id", "15" }
};
return dictionary;
}
[Benchmark]
public RouteValueDictionary ConditionalAdd_ContainsKeyAdd()
{
var dictionary = _arrayValues;
if (!dictionary.ContainsKey("action"))
{
dictionary.Add("action", "Index");
}
if (!dictionary.ContainsKey("controller"))
{
dictionary.Add("controller", "Home");
}
if (!dictionary.ContainsKey("area"))
{
dictionary.Add("area", "Admin");
}
return dictionary;
}
[Benchmark]
public RouteValueDictionary ConditionalAdd_TryAdd()
{
var dictionary = _arrayValues;
dictionary.TryAdd("action", "Index");
dictionary.TryAdd("controller", "Home");
dictionary.TryAdd("area", "Admin");
return dictionary;
}
[Benchmark]
public RouteValueDictionary ForEachThreeItems_Array()
{
var dictionary = _arrayValues;
foreach (var kvp in dictionary)
{
GC.KeepAlive(kvp.Value);
}
return dictionary;
}
[Benchmark]
public RouteValueDictionary ForEachThreeItems_Properties()
{
var dictionary = _propertyValues;
foreach (var kvp in dictionary)
{
GC.KeepAlive(kvp.Value);
}
return dictionary;
}
[Benchmark]
public RouteValueDictionary GetThreeItems_Array()
{
var dictionary = _arrayValues;
GC.KeepAlive(dictionary["action"]);
GC.KeepAlive(dictionary["controller"]);
GC.KeepAlive(dictionary["id"]);
return dictionary;
}
[Benchmark]
public RouteValueDictionary GetThreeItems_Properties()
{
var dictionary = _propertyValues;
GC.KeepAlive(dictionary["action"]);
GC.KeepAlive(dictionary["controller"]);
GC.KeepAlive(dictionary["id"]);
return dictionary;
}
[Benchmark]
public RouteValueDictionary SetSingleItem()
{
var dictionary = new RouteValueDictionary
{
["action"] = "Index"
};
return dictionary;
}
[Benchmark]
public RouteValueDictionary SetExistingItem()
{
var dictionary = _arrayValues;
dictionary["action"] = "About";
return dictionary;
}
[Benchmark]
public RouteValueDictionary SetThreeItems()
{
var dictionary = new RouteValueDictionary
{
["action"] = "Index",
["controller"] = "Home",
["id"] = "15"
};
return dictionary;
}
[Benchmark]
public RouteValueDictionary TryGetValueThreeItems_Array()
{
var dictionary = _arrayValues;
dictionary.TryGetValue("action", out var action);
dictionary.TryGetValue("controller", out var controller);
dictionary.TryGetValue("id", out var id);
GC.KeepAlive(action);
GC.KeepAlive(controller);
GC.KeepAlive(id);
return dictionary;
}
[Benchmark]
public RouteValueDictionary TryGetValueThreeItems_Properties()
{
var dictionary = _propertyValues;
dictionary.TryGetValue("action", out var action);
dictionary.TryGetValue("controller", out var controller);
dictionary.TryGetValue("id", out var id);
GC.KeepAlive(action);
GC.KeepAlive(controller);
GC.KeepAlive(id);
return dictionary;
}
}
}

View File

@ -4,43 +4,39 @@
</PropertyGroup>
<PropertyGroup Label="Package Versions">
<BenchmarkDotNetPackageVersion>0.10.13</BenchmarkDotNetPackageVersion>
<InternalAspNetCoreSdkPackageVersion>2.2.0-preview2-20181011.10</InternalAspNetCoreSdkPackageVersion>
<MicrosoftAspNetCoreAppPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreAppPackageVersion>
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
<MicrosoftAspNetCoreHostingPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHostingPackageVersion>
<MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
<MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
<MicrosoftAspNetCoreHttpPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreHttpPackageVersion>
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreServerKestrelPackageVersion>
<MicrosoftAspNetCoreStaticFilesPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreStaticFilesPackageVersion>
<MicrosoftAspNetCoreTestHostPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreTestHostPackageVersion>
<MicrosoftAspNetCoreTestingPackageVersion>2.2.0-preview3-35496</MicrosoftAspNetCoreTestingPackageVersion>
<MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
<MicrosoftExtensionsConfigurationCommandLinePackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
<MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
<MicrosoftExtensionsConfigurationPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsConfigurationPackageVersion>
<MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
<MicrosoftExtensionsDependencyInjectionPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsDependencyInjectionPackageVersion>
<MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingConsolePackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsLoggingConsolePackageVersion>
<MicrosoftExtensionsLoggingPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsLoggingPackageVersion>
<MicrosoftExtensionsLoggingTestingPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsLoggingTestingPackageVersion>
<MicrosoftExtensionsObjectPoolPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsObjectPoolPackageVersion>
<MicrosoftExtensionsOptionsPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsOptionsPackageVersion>
<MicrosoftExtensionsPropertyHelperSourcesPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsPropertyHelperSourcesPackageVersion>
<MicrosoftExtensionsWebEncodersPackageVersion>2.2.0-preview3-35496</MicrosoftExtensionsWebEncodersPackageVersion>
<MicrosoftNETCoreApp20PackageVersion>2.0.9</MicrosoftNETCoreApp20PackageVersion>
<MicrosoftNETCoreApp21PackageVersion>2.1.3</MicrosoftNETCoreApp21PackageVersion>
<MicrosoftNETCoreApp22PackageVersion>2.2.0-preview3-27008-03</MicrosoftNETCoreApp22PackageVersion>
<InternalAspNetCoreSdkPackageVersion>3.0.0-build-20181114.5</InternalAspNetCoreSdkPackageVersion>
<MicrosoftAspNetCoreAppPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAppPackageVersion>
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
<MicrosoftAspNetCoreHostingPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHostingPackageVersion>
<MicrosoftAspNetCoreHttpAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
<MicrosoftAspNetCoreHttpExtensionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
<MicrosoftAspNetCoreHttpPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpPackageVersion>
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
<MicrosoftAspNetCoreServerKestrelPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreServerKestrelPackageVersion>
<MicrosoftAspNetCoreStaticFilesPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreStaticFilesPackageVersion>
<MicrosoftAspNetCoreTestHostPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreTestHostPackageVersion>
<MicrosoftAspNetCoreTestingPackageVersion>3.0.0-preview-181113-11</MicrosoftAspNetCoreTestingPackageVersion>
<MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
<MicrosoftExtensionsConfigurationCommandLinePackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
<MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
<MicrosoftExtensionsConfigurationPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationPackageVersion>
<MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
<MicrosoftExtensionsDependencyInjectionPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsDependencyInjectionPackageVersion>
<MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingConsolePackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingConsolePackageVersion>
<MicrosoftExtensionsLoggingPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingPackageVersion>
<MicrosoftExtensionsLoggingTestingPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingTestingPackageVersion>
<MicrosoftExtensionsObjectPoolPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsObjectPoolPackageVersion>
<MicrosoftExtensionsOptionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsOptionsPackageVersion>
<MicrosoftExtensionsPropertyHelperSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsPropertyHelperSourcesPackageVersion>
<MicrosoftExtensionsWebEncodersPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsWebEncodersPackageVersion>
<MicrosoftNETCoreAppPackageVersion>3.0.0-preview1-26907-05</MicrosoftNETCoreAppPackageVersion>
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
<MoqPackageVersion>4.10.0</MoqPackageVersion>
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
<SystemReflectionEmitLightweightPackageVersion>4.3.0</SystemReflectionEmitLightweightPackageVersion>
<SystemReflectionEmitPackageVersion>4.3.0</SystemReflectionEmitPackageVersion>
<XunitAnalyzersPackageVersion>0.10.0</XunitAnalyzersPackageVersion>
<XunitPackageVersion>2.3.1</XunitPackageVersion>
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>

View File

@ -4,14 +4,11 @@
<PropertyGroup>
<!-- These properties are use by the automation that updates dependencies.props -->
<LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
<LineupPackageVersion>2.2.0-*</LineupPackageVersion>
<LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
</PropertyGroup>
<ItemGroup>
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp22PackageVersion)" />
<DotNetCoreRuntime Include="$(MicrosoftNETCoreAppPackageVersion)" />
</ItemGroup>
<PropertyGroup>

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

@ -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 System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder
{
public static class EndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapHello(this IEndpointRouteBuilder builder, string template, string greeter)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
var pipeline = builder.CreateApplicationBuilder()
.UseHello(greeter)
.Build();
return builder.Map(
template,
"Hello " + greeter,
pipeline);
}
}
}

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 Microsoft.Extensions.Options;
using RoutingSample.Web.HelloExtension;
namespace Microsoft.AspNetCore.Builder
{
public static class HelloAppBuilderExtensions
{
public static IApplicationBuilder UseHello(this IApplicationBuilder app, string greeter)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.UseMiddleware<HelloMiddleware>(Options.Create(new HelloOptions
{
Greeter = greeter
}));
}
}
}

View File

@ -0,0 +1,45 @@
// 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.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace RoutingSample.Web.HelloExtension
{
public class HelloMiddleware
{
private readonly RequestDelegate _next;
private readonly HelloOptions _helloOptions;
private readonly byte[] _helloPayload;
public HelloMiddleware(RequestDelegate next, IOptions<HelloOptions> helloOptions)
{
_next = next;
_helloOptions = helloOptions.Value;
var payload = new List<byte>();
payload.AddRange(Encoding.UTF8.GetBytes("Hello"));
if (!string.IsNullOrEmpty(_helloOptions.Greeter))
{
payload.Add((byte)' ');
payload.AddRange(Encoding.UTF8.GetBytes(_helloOptions.Greeter));
}
_helloPayload = payload.ToArray();
}
public Task InvokeAsync(HttpContext context)
{
var response = context.Response;
var payloadLength = _helloPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloPayload, 0, payloadLength);
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace RoutingSample.Web.HelloExtension
{
public class HelloOptions
{
public string Greeter { get; set; }
}
}

View File

@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net461</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -3,11 +3,11 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Patterns;
@ -19,65 +19,70 @@ namespace RoutingSandbox
public class UseEndpointRoutingStartup
{
private static readonly byte[] _homePayload = Encoding.UTF8.GetBytes("Endpoint Routing sample endpoints:" + Environment.NewLine + "/plaintext");
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
private static readonly byte[] _plainTextPayload = Encoding.UTF8.GetBytes("Plain text!");
public void ConfigureServices(IServiceCollection services)
{
var endpointDataSource = new DefaultEndpointDataSource(new[]
{
new RouteEndpoint((httpContext) =>
{
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);
},
RoutePatternFactory.Parse("/"),
0,
EndpointMetadataCollection.Empty,
"Home"),
new RouteEndpoint((httpContext) =>
{
var response = httpContext.Response;
var payloadLength = _helloWorldPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
},
RoutePatternFactory.Parse("/plaintext"),
0,
EndpointMetadataCollection.Empty,
"Plaintext"),
new RouteEndpoint((httpContext) =>
{
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
{
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
graphWriter.Write(dataSource, writer);
}
return Task.CompletedTask;
},
RoutePatternFactory.Parse("/graph"),
0,
new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })),
"DFA Graph"),
});
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
services.AddRouting();
}
public void Configure(IApplicationBuilder app)
{
app.UseEndpointRouting();
app.UseEndpointRouting(builder =>
{
builder.MapHello("/helloworld", "World");
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;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync(sb.ToString());
});
builder.MapGet(
"/plaintext",
(httpContext) =>
{
var response = httpContext.Response;
var payloadLength = _plainTextPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength);
});
builder.MapGet(
"/graph",
"DFA Graph",
(httpContext) =>
{
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
{
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
graphWriter.Write(dataSource, writer);
}
return Task.CompletedTask;
});
});
app.UseStaticFiles();
// Imagine some more stuff here...
app.UseAuthorization();
app.UseEndpoint();
}

View File

@ -1,49 +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.Http
{
/// <summary>
/// Respresents a logical endpoint in an application.
/// </summary>
public class Endpoint
{
/// <summary>
/// Creates a new instance of <see cref="Endpoint"/>.
/// </summary>
/// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
/// <param name="metadata">
/// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
/// </param>
/// <param name="displayName">
/// The informational display name of the endpoint. May be null.
/// </param>
public Endpoint(
RequestDelegate requestDelegate,
EndpointMetadataCollection metadata,
string displayName)
{
// All are allowed to be null
RequestDelegate = requestDelegate;
Metadata = metadata ?? EndpointMetadataCollection.Empty;
DisplayName = displayName;
}
/// <summary>
/// Gets the informational display name of this endpoint.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// Gets the collection of metadata associated with this endpoint.
/// </summary>
public EndpointMetadataCollection Metadata { get; }
/// <summary>
/// Gets the delegate used to process requests for the endpoint.
/// </summary>
public RequestDelegate RequestDelegate { get; }
public override string ToString() => DisplayName ?? base.ToString();
}
}

View File

@ -1,201 +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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Microsoft.AspNetCore.Http
{
/// <summary>
/// A collection of arbitrary metadata associated with an endpoint.
/// </summary>
/// <remarks>
/// <see cref="EndpointMetadataCollection"/> instances contain a list of metadata items
/// of arbitrary types. The metadata items are stored as an ordered collection with
/// items arranged in ascending order of precedence.
/// </remarks>
public sealed class EndpointMetadataCollection : IReadOnlyList<object>
{
/// <summary>
/// An empty <see cref="EndpointMetadataCollection"/>.
/// </summary>
public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty<object>());
private readonly object[] _items;
private readonly ConcurrentDictionary<Type, object[]> _cache;
/// <summary>
/// Creates a new instance of <see cref="EndpointMetadataCollection"/>.
/// </summary>
/// <param name="items">The metadata items.</param>
public EndpointMetadataCollection(IEnumerable<object> items)
{
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
_items = items.ToArray();
_cache = new ConcurrentDictionary<Type, object[]>();
}
/// <summary>
/// Creates a new instance of <see cref="EndpointMetadataCollection"/>.
/// </summary>
/// <param name="items">The metadata items.</param>
public EndpointMetadataCollection(params object[] items)
: this((IEnumerable<object>)items)
{
}
/// <summary>
/// Gets the item at <paramref name="index"/>.
/// </summary>
/// <param name="index">The index of the item to retrieve.</param>
/// <returns>The item at <paramref name="index"/>.</returns>
public object this[int index] => _items[index];
/// <summary>
/// Gets the count of metadata items.
/// </summary>
public int Count => _items.Length;
/// <summary>
/// Gets the most significant metadata item of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of metadata to retrieve.</typeparam>
/// <returns>
/// The most significant metadata of type <typeparamref name="T"/> or <c>null</c>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetMetadata<T>() where T : class
{
if (_cache.TryGetValue(typeof(T), out var result))
{
var length = result.Length;
return length > 0 ? (T)result[length - 1] : default;
}
return GetMetadataSlow<T>();
}
private T GetMetadataSlow<T>() where T : class
{
var array = GetOrderedMetadataSlow<T>();
var length = array.Length;
return length > 0 ? array[length - 1] : default;
}
/// <summary>
/// Gets the metadata items of type <typeparamref name="T"/> in ascending
/// order of precedence.
/// </summary>
/// <typeparam name="T">The type of metadata.</typeparam>
/// <returns>A sequence of metadata items of <typeparamref name="T"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IEnumerable<T> GetOrderedMetadata<T>() where T : class
{
if (_cache.TryGetValue(typeof(T), out var result))
{
return (T[])result;
}
return GetOrderedMetadataSlow<T>();
}
private T[] GetOrderedMetadataSlow<T>() where T : class
{
var items = new List<T>();
for (var i = 0; i < _items.Length; i++)
{
if (_items[i] is T item)
{
items.Add(item);
}
}
var array = items.ToArray();
_cache.TryAdd(typeof(T), array);
return array;
}
/// <summary>
/// Gets an <see cref="IEnumerator"/> of all metadata items.
/// </summary>
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
public Enumerator GetEnumerator() => new Enumerator(this);
/// <summary>
/// Gets an <see cref="IEnumerator{Object}"/> of all metadata items.
/// </summary>
/// <returns>An <see cref="IEnumerator{Object}"/> of all metadata items.</returns>
IEnumerator<object> IEnumerable<object>.GetEnumerator() => GetEnumerator();
/// <summary>
/// Gets an <see cref="IEnumerator"/> of all metadata items.
/// </summary>
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Enumerates the elements of an <see cref="EndpointMetadataCollection"/>.
/// </summary>
public struct Enumerator : IEnumerator<object>
{
// Intentionally not readonly to prevent defensive struct copies
private object[] _items;
private int _index;
internal Enumerator(EndpointMetadataCollection collection)
{
_items = collection._items;
_index = 0;
Current = null;
}
/// <summary>
/// Gets the element at the current position of the enumerator
/// </summary>
public object Current { get; private set; }
/// <summary>
/// Releases all resources used by the <see cref="Enumerator"/>.
/// </summary>
public void Dispose()
{
}
/// <summary>
/// Advances the enumerator to the next element of the <see cref="Enumerator"/>.
/// </summary>
/// <returns>
/// <c>true</c> if the enumerator was successfully advanced to the next element;
/// <c>false</c> if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
if (_index < _items.Length)
{
Current = _items[_index++];
return true;
}
Current = null;
return false;
}
/// <summary>
/// Sets the enumerator to its initial position, which is before the first element in the collection.
/// </summary>
public void Reset()
{
_index = 0;
Current = null;
}
}
}
}

View File

@ -1,18 +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.Http.Features
{
/// <summary>
/// A feature interface for endpoint routing. Use <see cref="HttpContext.Features"/>
/// to access an instance associated with the current request.
/// </summary>
public interface IEndpointFeature
{
/// <summary>
/// Gets or sets the selected <see cref="Http.Endpoint"/> for the current
/// request.
/// </summary>
Endpoint Endpoint { get; set; }
}
}

View File

@ -1,16 +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.Routing;
namespace Microsoft.AspNetCore.Http.Features
{
public interface IRouteValuesFeature
{
/// <summary>
/// Gets or sets the <see cref="RouteValueDictionary"/> associated with the currrent
/// request.
/// </summary>
RouteValueDictionary RouteValues { get; set; }
}
}

View File

@ -5,7 +5,7 @@
Commonly used types:
Microsoft.AspNetCore.Routing.IRouter
Microsoft.AspNetCore.Routing.RouteData</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;routing</PackageTags>

View File

@ -2,6 +2,15 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
[assembly: TypeForwardedTo(typeof(IEndpointFeature))]
[assembly: TypeForwardedTo(typeof(IRouteValuesFeature))]
[assembly: TypeForwardedTo(typeof(Endpoint))]
[assembly: TypeForwardedTo(typeof(EndpointMetadataCollection))]
[assembly: TypeForwardedTo(typeof(RouteValueDictionary))]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@ -1,58 +0,0 @@
// <auto-generated />
namespace Microsoft.AspNetCore.Routing.Abstractions
{
using System.Globalization;
using System.Reflection;
using System.Resources;
internal static class Resources
{
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNetCore.Routing.Abstractions.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// An element with the key '{0}' already exists in the {1}.
/// </summary>
internal static string RouteValueDictionary_DuplicateKey
{
get => GetString("RouteValueDictionary_DuplicateKey");
}
/// <summary>
/// An element with the key '{0}' already exists in the {1}.
/// </summary>
internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1);
/// <summary>
/// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
/// </summary>
internal static string RouteValueDictionary_DuplicatePropertyName
{
get => GetString("RouteValueDictionary_DuplicatePropertyName");
}
/// <summary>
/// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
/// </summary>
internal static string FormatRouteValueDictionary_DuplicatePropertyName(object p0, object p1, object p2, object p3)
=> string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicatePropertyName"), p0, p1, p2, p3);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
System.Diagnostics.Debug.Assert(value != null);
if (formatterNames != null)
{
for (var i = 0; i < formatterNames.Length; i++)
{
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
}
}
return value;
}
}
}

View File

@ -1,126 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="RouteValueDictionary_DuplicateKey" xml:space="preserve">
<value>An element with the key '{0}' already exists in the {1}.</value>
</data>
<data name="RouteValueDictionary_DuplicatePropertyName" xml:space="preserve">
<value>The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.</value>
</data>
</root>

View File

@ -1,718 +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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Routing.Abstractions;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// An <see cref="IDictionary{String, Object}"/> type for route values.
/// </summary>
public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object>
{
// 4 is a good default capacity here because that leaves enough space for area/controller/action/id
private const int DefaultCapacity = 4;
internal KeyValuePair<string, object>[] _arrayStorage;
internal PropertyStorage _propertyStorage;
private int _count;
/// <summary>
/// Creates a new instance of <see cref="RouteValueDictionary"/> from the provided array.
/// The new instance will take ownership of the array, and may mutate it.
/// </summary>
/// <param name="items">The items array.</param>
/// <returns>A new <see cref="RouteValueDictionary"/>.</returns>
public static RouteValueDictionary FromArray(KeyValuePair<string, object>[] items)
{
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
// We need to compress the array by removing non-contiguous items. We
// typically have a very small number of items to process. We don't need
// to preserve order.
var start = 0;
var end = items.Length - 1;
// We walk forwards from the beginning of the array and fill in 'null' slots.
// We walk backwards from the end of the array end move items in non-null' slots
// into whatever start is pointing to. O(n)
while (start <= end)
{
if (items[start].Key != null)
{
start++;
}
else if (items[end].Key != null)
{
// Swap this item into start and advance
items[start] = items[end];
items[end] = default;
start++;
end--;
}
else
{
// Both null, we need to hold on 'start' since we
// still need to fill it with something.
end--;
}
}
return new RouteValueDictionary()
{
_arrayStorage = items,
_count = start,
};
}
/// <summary>
/// Creates an empty <see cref="RouteValueDictionary"/>.
/// </summary>
public RouteValueDictionary()
{
_arrayStorage = Array.Empty<KeyValuePair<string, object>>();
}
/// <summary>
/// Creates a <see cref="RouteValueDictionary"/> initialized with the specified <paramref name="values"/>.
/// </summary>
/// <param name="values">An object to initialize the dictionary. The value can be of type
/// <see cref="IDictionary{TKey, TValue}"/> or <see cref="IReadOnlyDictionary{TKey, TValue}"/>
/// or an object with public properties as key-value pairs.
/// </param>
/// <remarks>
/// If the value is a dictionary or other <see cref="IEnumerable{T}"/> of <see cref="KeyValuePair{String, Object}"/>,
/// then its entries are copied. Otherwise the object is interpreted as a set of key-value pairs where the
/// property names are keys, and property values are the values, and copied into the dictionary.
/// Only public instance non-index properties are considered.
/// </remarks>
public RouteValueDictionary(object values)
: this()
{
if (values is RouteValueDictionary dictionary)
{
if (dictionary._propertyStorage != null)
{
// PropertyStorage is immutable so we can just copy it.
_propertyStorage = dictionary._propertyStorage;
_count = dictionary._count;
return;
}
var other = dictionary._arrayStorage;
var storage = new KeyValuePair<string, object>[other.Length];
if (dictionary._count != 0)
{
Array.Copy(other, 0, storage, 0, dictionary._count);
}
_arrayStorage = storage;
_count = dictionary._count;
return;
}
if (values is IEnumerable<KeyValuePair<string, object>> keyValueEnumerable)
{
foreach (var kvp in keyValueEnumerable)
{
Add(kvp.Key, kvp.Value);
}
return;
}
if (values is IEnumerable<KeyValuePair<string, string>> stringValueEnumerable)
{
foreach (var kvp in stringValueEnumerable)
{
Add(kvp.Key, kvp.Value);
}
return;
}
if (values != null)
{
var storage = new PropertyStorage(values);
_propertyStorage = storage;
_count = storage.Properties.Length;
return;
}
}
/// <inheritdoc />
public object this[string key]
{
get
{
if (key == null)
{
ThrowArgumentNullExceptionForKey();
}
object value;
TryGetValue(key, out value);
return value;
}
set
{
if (key == null)
{
ThrowArgumentNullExceptionForKey();
}
// We're calling this here for the side-effect of converting from properties
// to array. We need to create the array even if we just set an existing value since
// property storage is immutable.
EnsureCapacity(_count);
var index = FindIndex(key);
if (index < 0)
{
EnsureCapacity(_count + 1);
_arrayStorage[_count++] = new KeyValuePair<string, object>(key, value);
}
else
{
_arrayStorage[index] = new KeyValuePair<string, object>(key, value);
}
}
}
/// <summary>
/// Gets the comparer for this dictionary.
/// </summary>
/// <remarks>
/// This will always be a reference to <see cref="StringComparer.OrdinalIgnoreCase"/>
/// </remarks>
public IEqualityComparer<string> Comparer => StringComparer.OrdinalIgnoreCase;
/// <inheritdoc />
public int Count => _count;
/// <inheritdoc />
bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;
/// <inheritdoc />
public ICollection<string> Keys
{
get
{
EnsureCapacity(_count);
var array = _arrayStorage;
var keys = new string[_count];
for (var i = 0; i < keys.Length; i++)
{
keys[i] = array[i].Key;
}
return keys;
}
}
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys => Keys;
/// <inheritdoc />
public ICollection<object> Values
{
get
{
EnsureCapacity(_count);
var array = _arrayStorage;
var values = new object[_count];
for (var i = 0; i < values.Length; i++)
{
values[i] = array[i].Value;
}
return values;
}
}
IEnumerable<object> IReadOnlyDictionary<string, object>.Values => Values;
/// <inheritdoc />
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
{
Add(item.Key, item.Value);
}
/// <inheritdoc />
public void Add(string key, object value)
{
if (key == null)
{
ThrowArgumentNullExceptionForKey();
}
EnsureCapacity(_count + 1);
var index = FindIndex(key);
if (index >= 0)
{
var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary));
throw new ArgumentException(message, nameof(key));
}
_arrayStorage[_count] = new KeyValuePair<string, object>(key, value);
_count++;
}
/// <inheritdoc />
public void Clear()
{
if (_count == 0)
{
return;
}
if (_propertyStorage != null)
{
_arrayStorage = Array.Empty<KeyValuePair<string, object>>();
_propertyStorage = null;
_count = 0;
return;
}
Array.Clear(_arrayStorage, 0, _count);
_count = 0;
}
/// <inheritdoc />
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
{
return TryGetValue(item.Key, out var value) && EqualityComparer<object>.Default.Equals(value, item.Value);
}
/// <inheritdoc />
public bool ContainsKey(string key)
{
if (key == null)
{
ThrowArgumentNullExceptionForKey();
}
return TryGetValue(key, out var _);
}
/// <inheritdoc />
void ICollection<KeyValuePair<string, object>>.CopyTo(
KeyValuePair<string, object>[] array,
int arrayIndex)
{
if (array == null)
{
throw new ArgumentNullException(nameof(array));
}
if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < this.Count)
{
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
}
if (Count == 0)
{
return;
}
EnsureCapacity(Count);
var storage = _arrayStorage;
Array.Copy(storage, 0, array, arrayIndex, _count);
}
/// <inheritdoc />
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
/// <inheritdoc />
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc />
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
{
if (Count == 0)
{
return false;
}
EnsureCapacity(Count);
var index = FindIndex(item.Key);
var array = _arrayStorage;
if (index >= 0 && EqualityComparer<object>.Default.Equals(array[index].Value, item.Value))
{
Array.Copy(array, index + 1, array, index, _count - index);
_count--;
array[_count] = default;
return true;
}
return false;
}
/// <inheritdoc />
public bool Remove(string key)
{
if (key == null)
{
ThrowArgumentNullExceptionForKey();
}
if (Count == 0)
{
return false;
}
// Ensure property storage is converted to array storage as we'll be
// applying the lookup and removal on the array
EnsureCapacity(_count);
var index = FindIndex(key);
if (index >= 0)
{
_count--;
var array = _arrayStorage;
Array.Copy(array, index + 1, array, index, _count - index);
array[_count] = default;
return true;
}
return false;
}
/// <summary>
/// Attempts to remove and return the value that has the specified key from the <see cref="RouteValueDictionary"/>.
/// </summary>
/// <param name="key">The key of the element to remove and return.</param>
/// <param name="value">When this method returns, contains the object removed from the <see cref="RouteValueDictionary"/>, or <c>null</c> if key does not exist.</param>
/// <returns>
/// <c>true</c> if the object was removed successfully; otherwise, <c>false</c>.
/// </returns>
public bool Remove(string key, out object value)
{
if (key == null)
{
ThrowArgumentNullExceptionForKey();
}
if (_count == 0)
{
value = default;
return false;
}
// Ensure property storage is converted to array storage as we'll be
// applying the lookup and removal on the array
EnsureCapacity(_count);
var index = FindIndex(key);
if (index >= 0)
{
_count--;
var array = _arrayStorage;
value = array[index].Value;
Array.Copy(array, index + 1, array, index, _count - index);
array[_count] = default;
return true;
}
value = default;
return false;
}
/// <summary>
/// Attempts to the add the provided <paramref name="key"/> and <paramref name="value"/> to the dictionary.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
/// <returns>Returns <c>true</c> if the value was added. Returns <c>false</c> if the key was already present.</returns>
public bool TryAdd(string key, object value)
{
if (key == null)
{
ThrowArgumentNullExceptionForKey();
}
// Since this is an attempt to write to the dictionary, just make it an array if it isn't. If the code
// path we're on event tries to write to the dictionary, it will likely get 'upgraded' at some point,
// so we do it here to keep the code size and complexity down.
EnsureCapacity(Count);
var index = FindIndex(key);
if (index >= 0)
{
return false;
}
EnsureCapacity(Count + 1);
_arrayStorage[Count] = new KeyValuePair<string, object>(key, value);
_count++;
return true;
}
/// <inheritdoc />
public bool TryGetValue(string key, out object value)
{
if (key == null)
{
ThrowArgumentNullExceptionForKey();
}
if (_propertyStorage == null)
{
return TryFindItem(key, out value);
}
return TryGetValueSlow(key, out value);
}
private bool TryGetValueSlow(string key, out object value)
{
if (_propertyStorage != null)
{
var storage = _propertyStorage;
for (var i = 0; i < storage.Properties.Length; i++)
{
if (string.Equals(storage.Properties[i].Name, key, StringComparison.OrdinalIgnoreCase))
{
value = storage.Properties[i].GetValue(storage.Value);
return true;
}
}
}
value = default;
return false;
}
private static void ThrowArgumentNullExceptionForKey()
{
throw new ArgumentNullException("key");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int capacity)
{
if (_propertyStorage != null || _arrayStorage.Length < capacity)
{
EnsureCapacitySlow(capacity);
}
}
private void EnsureCapacitySlow(int capacity)
{
if (_propertyStorage != null)
{
var storage = _propertyStorage;
// If we're converting from properties, it's likely due to an 'add' to make sure we have at least
// the default amount of space.
capacity = Math.Max(DefaultCapacity, Math.Max(storage.Properties.Length, capacity));
var array = new KeyValuePair<string, object>[capacity];
for (var i = 0; i < storage.Properties.Length; i++)
{
var property = storage.Properties[i];
array[i] = new KeyValuePair<string, object>(property.Name, property.GetValue(storage.Value));
}
_arrayStorage = array;
_propertyStorage = null;
return;
}
if (_arrayStorage.Length < capacity)
{
capacity = _arrayStorage.Length == 0 ? DefaultCapacity : _arrayStorage.Length * 2;
var array = new KeyValuePair<string, object>[capacity];
if (_count > 0)
{
Array.Copy(_arrayStorage, 0, array, 0, _count);
}
_arrayStorage = array;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int FindIndex(string key)
{
// Generally the bounds checking here will be elided by the JIT because this will be called
// on the same code path as EnsureCapacity.
var array = _arrayStorage;
var count = _count;
for (var i = 0; i < count; i++)
{
if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryFindItem(string key, out object value)
{
var array = _arrayStorage;
var count = _count;
// Elide bounds check for indexing.
if ((uint)count <= (uint)array.Length)
{
for (var i = 0; i < count; i++)
{
if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase))
{
value = array[i].Value;
return true;
}
}
}
value = null;
return false;
}
public struct Enumerator : IEnumerator<KeyValuePair<string, object>>
{
private readonly RouteValueDictionary _dictionary;
private int _index;
public Enumerator(RouteValueDictionary dictionary)
{
if (dictionary == null)
{
throw new ArgumentNullException();
}
_dictionary = dictionary;
Current = default;
_index = 0;
}
public KeyValuePair<string, object> Current { get; private set; }
object IEnumerator.Current => Current;
public void Dispose()
{
}
// Similar to the design of List<T>.Enumerator - Split into fast path and slow path for inlining friendliness
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
var dictionary = _dictionary;
// The uncommon case is that the propertyStorage is in use
if (dictionary._propertyStorage == null && ((uint)_index < (uint)dictionary._count))
{
Current = dictionary._arrayStorage[_index];
_index++;
return true;
}
return MoveNextRare();
}
private bool MoveNextRare()
{
var dictionary = _dictionary;
if (dictionary._propertyStorage != null && ((uint)_index < (uint)dictionary._count))
{
var storage = dictionary._propertyStorage;
var property = storage.Properties[_index];
Current = new KeyValuePair<string, object>(property.Name, property.GetValue(storage.Value));
_index++;
return true;
}
_index = dictionary._count;
Current = default;
return false;
}
public void Reset()
{
Current = default;
_index = 0;
}
}
internal class PropertyStorage
{
private static readonly ConcurrentDictionary<Type, PropertyHelper[]> _propertyCache = new ConcurrentDictionary<Type, PropertyHelper[]>();
public readonly object Value;
public readonly PropertyHelper[] Properties;
public PropertyStorage(object value)
{
Debug.Assert(value != null);
Value = value;
// Cache the properties so we can know if we've already validated them for duplicates.
var type = Value.GetType();
if (!_propertyCache.TryGetValue(type, out Properties))
{
Properties = PropertyHelper.GetVisibleProperties(type);
ValidatePropertyNames(type, Properties);
_propertyCache.TryAdd(type, Properties);
}
}
private static void ValidatePropertyNames(Type type, PropertyHelper[] properties)
{
var names = new Dictionary<string, PropertyHelper>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < properties.Length; i++)
{
var property = properties[i];
if (names.TryGetValue(property.Name, out var duplicate))
{
var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName(
type.FullName,
property.Name,
duplicate.Name,
nameof(RouteValueDictionary));
throw new InvalidOperationException(message);
}
names.Add(property.Name, property);
}
}
}
}
}

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,25 +2,48 @@
// 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.Internal
namespace Microsoft.AspNetCore.Builder
{
public static class EndpointRoutingApplicationBuilderExtensions
{
// 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)
{
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
VerifyRoutingIsRegistered(builder);
var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
EndpointDataSource middlewareEndpointDataSource;
var endpointRouteBuilder = builder.ApplicationServices.GetRequiredService<IEndpointRouteBuilder>();
if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultEndpointRouteBuilder)
{
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)
@ -51,4 +74,4 @@ namespace Microsoft.AspNetCore.Internal
}
}
}
}
}

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

@ -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,9 +2,11 @@
// 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;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
@ -49,16 +51,23 @@ 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.TryAddTransient<IEndpointRouteBuilder, DefaultEndpointRouteBuilder>();
//
// Default matcher implementation
//
@ -78,6 +87,11 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
//
// Misc infrastructure
//
services.TryAddSingleton<RoutePatternTransformer, DefaultRoutePatternTransformer>();
return services;
}

View File

@ -0,0 +1,19 @@
// 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.Http;
namespace Microsoft.AspNetCore.Routing
{
public abstract class EndpointModel
{
public RequestDelegate RequestDelegate { get; set; }
public string DisplayName { get; set; }
public IList<object> Metadata { get; } = new List<object>();
public abstract Endpoint Build();
}
}

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

@ -0,0 +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 interface IEndpointRouteBuilder
{
IApplicationBuilder CreateApplicationBuilder();
IServiceProvider ServiceProvider { get; }
ICollection<EndpointDataSource> DataSources { get; }
}
}

View File

@ -0,0 +1,169 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// LICENSING NOTE: This file is from the dotnet corefx repository.
//
// See https://github.com/dotnet/corefx/blob/143df51926f2ad397fef9c9ca7ede88e2721e801/src/Common/src/System/Collections/Generic/ArrayBuilder.cs
using System;
using System.Diagnostics;
namespace Microsoft.AspNetCore.Routing.Internal
{
/// <summary>
/// Helper type for avoiding allocations while building arrays.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
internal struct ArrayBuilder<T>
{
private const int DefaultCapacity = 4;
private const int MaxCoreClrArrayLength = 0x7fefffff; // For byte arrays the limit is slightly larger
private T[] _array; // Starts out null, initialized on first Add.
private int _count; // Number of items into _array we're using.
/// <summary>
/// Initializes the <see cref="ArrayBuilder{T}"/> with a specified capacity.
/// </summary>
/// <param name="capacity">The capacity of the array to allocate.</param>
public ArrayBuilder(int capacity) : this()
{
Debug.Assert(capacity >= 0);
if (capacity > 0)
{
_array = new T[capacity];
}
}
/// <summary>
/// Gets the number of items this instance can store without re-allocating,
/// or 0 if the backing array is <c>null</c>.
/// </summary>
public int Capacity => _array?.Length ?? 0;
/// <summary>Gets the current underlying array.</summary>
public T[] Buffer => _array;
/// <summary>
/// Gets the number of items in the array currently in use.
/// </summary>
public int Count => _count;
/// <summary>
/// Gets or sets the item at a certain index in the array.
/// </summary>
/// <param name="index">The index into the array.</param>
public T this[int index]
{
get
{
Debug.Assert(index >= 0 && index < _count);
return _array[index];
}
}
/// <summary>
/// Adds an item to the backing array, resizing it if necessary.
/// </summary>
/// <param name="item">The item to add.</param>
public void Add(T item)
{
if (_count == Capacity)
{
EnsureCapacity(_count + 1);
}
UncheckedAdd(item);
}
/// <summary>
/// Gets the first item in this builder.
/// </summary>
public T First()
{
Debug.Assert(_count > 0);
return _array[0];
}
/// <summary>
/// Gets the last item in this builder.
/// </summary>
public T Last()
{
Debug.Assert(_count > 0);
return _array[_count - 1];
}
/// <summary>
/// Creates an array from the contents of this builder.
/// </summary>
/// <remarks>
/// Do not call this method twice on the same builder.
/// </remarks>
public T[] ToArray()
{
if (_count == 0)
{
return Array.Empty<T>();
}
Debug.Assert(_array != null); // Nonzero _count should imply this
T[] result = _array;
if (_count < result.Length)
{
// Avoid a bit of overhead (method call, some branches, extra codegen)
// which would be incurred by using Array.Resize
result = new T[_count];
Array.Copy(_array, 0, result, 0, _count);
}
#if DEBUG
// Try to prevent callers from using the ArrayBuilder after ToArray, if _count != 0.
_count = -1;
_array = null;
#endif
return result;
}
/// <summary>
/// Adds an item to the backing array, without checking if there is room.
/// </summary>
/// <param name="item">The item to add.</param>
/// <remarks>
/// Use this method if you know there is enough space in the <see cref="ArrayBuilder{T}"/>
/// for another item, and you are writing performance-sensitive code.
/// </remarks>
public void UncheckedAdd(T item)
{
Debug.Assert(_count < Capacity);
_array[_count++] = item;
}
private void EnsureCapacity(int minimum)
{
Debug.Assert(minimum > Capacity);
int capacity = Capacity;
int nextCapacity = capacity == 0 ? DefaultCapacity : 2 * capacity;
if ((uint)nextCapacity > (uint)MaxCoreClrArrayLength)
{
nextCapacity = Math.Max(capacity + 1, MaxCoreClrArrayLength);
}
nextCapacity = Math.Max(nextCapacity, minimum);
T[] next = new T[nextCapacity];
if (_count > 0)
{
Array.Copy(_array, 0, next, 0, _count);
}
_array = next;
}
}
}

View File

@ -119,12 +119,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
private class OutboundMatchClassifier : IClassifier<OutboundMatch>
{
public OutboundMatchClassifier()
{
ValueComparer = new RouteValueEqualityComparer();
}
public IEqualityComparer<object> ValueComparer { get; private set; }
public IEqualityComparer<object> ValueComparer => RouteValueEqualityComparer.Default;
public IDictionary<string, DecisionCriterionValue> GetCriteria(OutboundMatch item)
{

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.
#if IL_EMIT
using System;
using System.Diagnostics;
using System.Linq;
@ -596,5 +595,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
}
}
#endif

View File

@ -1,6 +1,5 @@
// 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.
#if IL_EMIT
using System;
using System.Threading;
@ -101,4 +100,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
}
}
#endif

View File

@ -84,11 +84,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
fallback = new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries);
}
#if IL_EMIT
return new ILEmitTrieJumpTable(defaultDestination, exitDestination, pathEntries, vectorize: null, fallback);
#else
return fallback;
#endif
}
}
}

View File

@ -4,7 +4,7 @@
Commonly used types:
Microsoft.AspNetCore.Routing.Route
Microsoft.AspNetCore.Routing.RouteCollection</Description>
<TargetFrameworks>netstandard2.0;netcoreapp2.2</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;routing</PackageTags>
@ -12,14 +12,10 @@ Microsoft.AspNetCore.Routing.RouteCollection</Description>
</PropertyGroup>
<PropertyGroup>
<!--
RefEmit is supported in netcoreapp.
<!--
The ability to save compiled assemblies is for testing and debugging, not shipped in the product.
-->
<ILEmit Condition="'$(TargetFramework)'!='netstandard2.0'">true</ILEmit>
<ILEmitSaveAssemblies Condition="'$(ILEmitSaveAssemblies)'==''">false</ILEmitSaveAssemblies>
<DefineConstants Condition="'$(ILEmit)'=='true'">IL_EMIT;$(DefineConstants)</DefineConstants>
<DefineConstants Condition="'$(ILEmitSaveAssemblies)'=='true'">IL_EMIT_SAVE_ASSEMBLIES;$(DefineConstants)</DefineConstants>
</PropertyGroup>

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

@ -0,0 +1,227 @@
// 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.Routing.Patterns
{
internal class DefaultRoutePatternTransformer : RoutePatternTransformer
{
private readonly ParameterPolicyFactory _policyFactory;
public DefaultRoutePatternTransformer(ParameterPolicyFactory policyFactory)
{
if (policyFactory == null)
{
throw new ArgumentNullException(nameof(policyFactory));
}
_policyFactory = policyFactory;
}
public override RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues)
{
if (original == null)
{
throw new ArgumentNullException(nameof(original));
}
return SubstituteRequiredValuesCore(original, new RouteValueDictionary(requiredValues));
}
private RoutePattern SubstituteRequiredValuesCore(RoutePattern original, RouteValueDictionary requiredValues)
{
// Process each required value in sequence. Bail if we find any rejection criteria. The goal
// of rejection is to avoid creating RoutePattern instances that can't *ever* match.
//
// If we succeed, then we need to create a new RoutePattern with the provided required values.
//
// Substitution can merge with existing RequiredValues already on the RoutePattern as long
// as all of the success criteria are still met at the end.
foreach (var kvp in requiredValues)
{
// There are three possible cases here:
// 1. Required value is null-ish
// 2. Required value corresponds to a parameter
// 3. Required value corresponds to a matching default value
//
// If none of these are true then we can reject this substitution.
RoutePatternParameterPart parameter;
if (RouteValueEqualityComparer.Default.Equals(kvp.Value, string.Empty))
{
// 1. Required value is null-ish - check to make sure that this route doesn't have a
// parameter or filter-like default.
if (original.GetParameter(kvp.Key) != null)
{
// Fail: we can't 'require' that a parameter be null. In theory this would be possible
// for an optional parameter, but that's not really in line with the usage of this feature
// so we don't handle it.
//
// Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = "" }
return null;
}
else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
!RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
{
// Fail: this route has a non-parameter default that doesn't match.
//
// Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = "" }
return null;
}
// Success: (for this parameter at least)
//
// Ex: {controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... }
continue;
}
else if ((parameter = original.GetParameter(kvp.Key)) != null)
{
// 2. Required value corresponds to a parameter - check to make sure that this value matches
// any IRouteConstraint implementations.
if (!MatchesConstraints(original, parameter, kvp.Key, requiredValues))
{
// Fail: this route has a constraint that failed.
//
// Ex: Admin/{controller:regex(Home|Login)}/{action=Index}/{id?} - with required values: { controller = "Store" }
return null;
}
// Success: (for this parameter at least)
//
// Ex: {area}/{controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... }
continue;
}
else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
{
// 3. Required value corresponds to a matching default value - check to make sure that this value matches
// any IRouteConstraint implementations. It's unlikely that this would happen in practice but it doesn't
// hurt for us to check.
if (!MatchesConstraints(original, parameter: null, kvp.Key, requiredValues))
{
// Fail: this route has a constraint that failed.
//
// Ex:
// Admin/Home/{action=Index}/{id?}
// defaults: { area = "Admin" }
// constraints: { area = "Blog" }
// with required values: { area = "Admin" }
return null;
}
// Success: (for this parameter at least)
//
// Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Admin", ... }
continue;
}
else
{
// Fail: this is a required value for a key that doesn't appear in the templates, or the route
// pattern has a different default value for a non-parameter.
//
// Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Blog", ... }
// OR (less likely)
// Ex: Admin/{controller=Home}/{action=Index}/{id?} with required values: { page = "/Index", ... }
return null;
}
}
List<RoutePatternParameterPart> updatedParameters = null;
List<RoutePatternPathSegment> updatedSegments = null;
RouteValueDictionary updatedDefaults = null;
// So if we get here, we're ready to update the route pattern. We need to update two things:
// 1. Remove any default values that conflict with the required values.
// 2. Merge any existing required values
foreach (var kvp in requiredValues)
{
var parameter = original.GetParameter(kvp.Key);
// We only need to handle the case where the required value maps to a parameter. That's the only
// case where we allow a default and a required value to disagree, and we already validated the
// other cases.
if (parameter != null &&
original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
!RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
{
if (updatedDefaults == null && updatedSegments == null && updatedParameters == null)
{
updatedDefaults = new RouteValueDictionary(original.Defaults);
updatedSegments = new List<RoutePatternPathSegment>(original.PathSegments);
updatedParameters = new List<RoutePatternParameterPart>(original.Parameters);
}
updatedDefaults.Remove(kvp.Key);
RemoveParameterDefault(updatedSegments, updatedParameters, parameter);
}
}
foreach (var kvp in original.RequiredValues)
{
requiredValues.TryAdd(kvp.Key, kvp.Value);
}
return new RoutePattern(
original.RawText,
updatedDefaults ?? original.Defaults,
original.ParameterPolicies,
requiredValues,
updatedParameters ?? original.Parameters,
updatedSegments ?? original.PathSegments);
}
private bool MatchesConstraints(RoutePattern pattern, RoutePatternParameterPart parameter, string key, RouteValueDictionary requiredValues)
{
if (pattern.ParameterPolicies.TryGetValue(key, out var policies))
{
for (var i = 0; i < policies.Count; i++)
{
var policy = _policyFactory.Create(parameter, policies[i]);
if (policy is IRouteConstraint constraint)
{
if (!constraint.Match(httpContext: null, NullRouter.Instance, key, requiredValues, RouteDirection.IncomingRequest))
{
return false;
}
}
}
}
return true;
}
private void RemoveParameterDefault(List<RoutePatternPathSegment> segments, List<RoutePatternParameterPart> parameters, RoutePatternParameterPart parameter)
{
// We know that a parameter can only appear once, so we only need to rewrite one segment and one parameter.
for (var i = 0; i < segments.Count; i++)
{
var segment = segments[i];
for (var j = 0; j < segment.Parts.Count; j++)
{
if (object.ReferenceEquals(parameter, segment.Parts[j]))
{
// Found it!
var updatedParameter = RoutePatternFactory.ParameterPart(parameter.Name, @default: null, parameter.ParameterKind, parameter.ParameterPolicies);
var updatedParts = new List<RoutePatternPart>(segment.Parts);
updatedParts[j] = updatedParameter;
segments[i] = RoutePatternFactory.Segment(updatedParts);
for (var k = 0; k < parameters.Count; k++)
{
if (ReferenceEquals(parameter, parameters[k]))
{
parameters[k] = updatedParameter;
break;
}
}
return;
}
}
}
}
}
}

View File

@ -2,8 +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.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing.Internal;
namespace Microsoft.AspNetCore.Routing.Patterns
{
@ -77,7 +76,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
currentIndex++;
}
var parseResults = ParseConstraints(parameter, parameterName, currentIndex, endIndex);
var parseResults = ParseConstraints(parameter, currentIndex, endIndex);
currentIndex = parseResults.CurrentIndex;
string defaultValue = null;
@ -91,17 +90,16 @@ namespace Microsoft.AspNetCore.Routing.Patterns
parameterName,
defaultValue,
parameterKind,
parseResults.ParameterPolicies.ToArray(),
parseResults.ParameterPolicies,
encodeSlashes);
}
private static ParameterPolicyParseResults ParseConstraints(
string text,
string parameterName,
int currentIndex,
int endIndex)
{
var constraints = new List<RoutePatternParameterPolicyReference>();
var constraints = new ArrayBuilder<RoutePatternParameterPolicyReference>(0);
var state = ParseState.Start;
var startIndex = currentIndex;
do
@ -234,7 +232,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
} while (state != ParseState.End);
return new ParameterPolicyParseResults(currentIndex, constraints);
return new ParameterPolicyParseResults(currentIndex, constraints.ToArray());
}
private enum ParseState
@ -249,9 +247,9 @@ namespace Microsoft.AspNetCore.Routing.Patterns
{
public readonly int CurrentIndex;
public readonly IReadOnlyList<RoutePatternParameterPolicyReference> ParameterPolicies;
public readonly RoutePatternParameterPolicyReference[] ParameterPolicies;
public ParameterPolicyParseResults(int currentIndex, IReadOnlyList<RoutePatternParameterPolicyReference> parameterPolicies)
public ParameterPolicyParseResults(int currentIndex, RoutePatternParameterPolicyReference[] parameterPolicies)
{
CurrentIndex = currentIndex;
ParameterPolicies = parameterPolicies;

View File

@ -23,17 +23,20 @@ namespace Microsoft.AspNetCore.Routing.Patterns
string rawText,
IReadOnlyDictionary<string, object> defaults,
IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> parameterPolicies,
IReadOnlyDictionary<string, object> requiredValues,
IReadOnlyList<RoutePatternParameterPart> parameters,
IReadOnlyList<RoutePatternPathSegment> pathSegments)
{
Debug.Assert(defaults != null);
Debug.Assert(parameterPolicies != null);
Debug.Assert(parameters != null);
Debug.Assert(requiredValues != null);
Debug.Assert(pathSegments != null);
RawText = rawText;
Defaults = defaults;
ParameterPolicies = parameterPolicies;
RequiredValues = requiredValues;
Parameters = parameters;
PathSegments = pathSegments;
@ -53,6 +56,29 @@ namespace Microsoft.AspNetCore.Routing.Patterns
/// </summary>
public IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> ParameterPolicies { get; }
/// <summary>
/// Gets a collection of route values that must be provided for this route pattern to be considered
/// applicable.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="RequiredValues"/> allows a framework to substitute route values into a parameterized template
/// so that the same route template specification can be used to create multiple route patterns.
/// <example>
/// This example shows how a route template can be used with required values to substitute known
/// route values for parameters.
/// <code>
/// Route Template: "{controller=Home}/{action=Index}/{id?}"
/// Route Values: { controller = "Store", action = "Index" }
/// </code>
///
/// A route pattern produced in this way will match and generate URL paths like: <c>/Store</c>,
/// <c>/Store/Index</c>, and <c>/Store/Index/17</c>.
/// </example>
/// </para>
/// </remarks>
public IReadOnlyDictionary<string, object> RequiredValues { get; }
/// <summary>
/// Gets the precedence value of the route pattern for URL matching.
/// </summary>
@ -110,7 +136,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

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
/// </summary>
public static class RoutePatternFactory
{
private static readonly IReadOnlyDictionary<string, object> EmptyDefaultsDictionary =
private static readonly IReadOnlyDictionary<string, object> EmptyDictionary =
new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
private static readonly IReadOnlyDictionary<string, IReadOnlyList<RoutePatternParameterPolicyReference>> EmptyPoliciesDictionary =
@ -61,7 +61,37 @@ namespace Microsoft.AspNetCore.Routing.Patterns
}
var original = RoutePatternParser.Parse(pattern);
return Pattern(original.RawText, defaults, parameterPolicies, original.PathSegments);
return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), requiredValues: null, original.PathSegments);
}
/// <summary>
/// Creates a <see cref="RoutePattern"/> from its string representation along
/// with provided default values and parameter policies.
/// </summary>
/// <param name="pattern">The route pattern string to parse.</param>
/// <param name="defaults">
/// Additional default values to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using <see cref="RouteValueDictionary"/>
/// and then merged into the parsed route pattern.
/// </param>
/// <param name="parameterPolicies">
/// Additional parameter policies to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using <see cref="RouteValueDictionary"/>
/// and then merged into the parsed route pattern.
/// </param>
/// <param name="requiredValues">
/// Route values that can be substituted for parameters in the route pattern. See remarks on <see cref="RoutePattern.RequiredValues"/>.
/// </param>
/// <returns>The <see cref="RoutePattern"/>.</returns>
public static RoutePattern Parse(string pattern, object defaults, object parameterPolicies, object requiredValues)
{
if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
}
var original = RoutePatternParser.Parse(pattern);
return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), Wrap(requiredValues), original.PathSegments);
}
/// <summary>
@ -76,7 +106,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, null, null, segments);
return PatternCore(null, null, null, null, segments);
}
/// <summary>
@ -92,7 +122,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, null, null, segments);
return PatternCore(rawText, null, null, null, segments);
}
/// <summary>
@ -121,14 +151,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments);
}
/// <summary>
/// Creates a <see cref="RoutePattern"/> from a collection of segments along
/// with provided default values and parameter policies.
/// </summary>
/// <param name="rawText">The raw text to associate with the route pattern.</param>
/// <param name="rawText">The raw text to associate with the route pattern. May be null.</param>
/// <param name="defaults">
/// Additional default values to associated with the route pattern. May be null.
/// The provided object will be converted to key-value pairs using <see cref="RouteValueDictionary"/>
@ -152,7 +182,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments);
}
/// <summary>
@ -167,7 +197,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, null, null, segments);
return PatternCore(null, null, null, requiredValues: null, segments);
}
/// <summary>
@ -183,7 +213,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, null, null, segments);
return PatternCore(rawText, null, null, requiredValues: null, segments);
}
/// <summary>
@ -212,7 +242,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments);
}
/// <summary>
@ -243,13 +273,14 @@ namespace Microsoft.AspNetCore.Routing.Patterns
throw new ArgumentNullException(nameof(segments));
}
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments);
return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments);
}
private static RoutePattern PatternCore(
string rawText,
RouteValueDictionary defaults,
RouteValueDictionary parameterPolicies,
RouteValueDictionary requiredValues,
IEnumerable<RoutePatternPathSegment> segments)
{
// We want to merge the segment data with the 'out of line' defaults and parameter policies.
@ -311,12 +342,56 @@ namespace Microsoft.AspNetCore.Routing.Patterns
}
}
// Each Required Value either needs to either:
// 1. be null-ish
// 2. have a corresponding parameter
// 3. have a corrsponding default that matches both key and value
if (requiredValues != null)
{
foreach (var kvp in requiredValues)
{
// 1.be null-ish
var found = RouteValueEqualityComparer.Default.Equals(string.Empty, kvp.Value);
// 2. have a corresponding parameter
if (!found && parameters != null)
{
for (var i = 0; i < parameters.Count; i++)
{
if (string.Equals(kvp.Key, parameters[i].Name, StringComparison.OrdinalIgnoreCase))
{
found = true;
break;
}
}
}
// 3. have a corrsponding default that matches both key and value
if (!found &&
updatedDefaults != null &&
updatedDefaults.TryGetValue(kvp.Key, out var defaultValue) &&
RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
{
found = true;
}
if (!found)
{
throw new InvalidOperationException(
$"No corresponding parameter or default value could be found for the required value " +
$"'{kvp.Key}={kvp.Value}'. A non-null required value must correspond to a route parameter or the " +
$"route pattern must have a matching default value.");
}
}
}
return new RoutePattern(
rawText,
updatedDefaults ?? EmptyDefaultsDictionary,
updatedDefaults ?? EmptyDictionary,
updatedParameterPolicies != null
? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<RoutePatternParameterPolicyReference>)kvp.Value.ToArray())
: EmptyPoliciesDictionary,
requiredValues ?? EmptyDictionary,
(IReadOnlyList<RoutePatternParameterPart>)parameters ?? Array.Empty<RoutePatternParameterPart>(),
updatedSegments);
@ -449,7 +524,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
throw new ArgumentNullException(nameof(parts));
}
return SegmentCore((RoutePatternPart[]) parts.Clone());
return SegmentCore((RoutePatternPart[])parts.Clone());
}
private static RoutePatternPathSegment SegmentCore(RoutePatternPart[] parts)
@ -670,7 +745,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns
parameterName: parameterName,
@default: @default,
parameterKind: parameterKind,
parameterPolicies: (RoutePatternParameterPolicyReference[]) parameterPolicies.Clone());
parameterPolicies: (RoutePatternParameterPolicyReference[])parameterPolicies.Clone());
}
private static RoutePatternParameterPart ParameterPartCore(
@ -802,5 +877,10 @@ namespace Microsoft.AspNetCore.Routing.Patterns
{
return new RoutePatternParameterPolicyReference(parameterPolicy);
}
private static RouteValueDictionary Wrap(object values)
{
return values == null ? null : new RouteValueDictionary(values);
}
}
}

View File

@ -0,0 +1,35 @@
// 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.Patterns
{
/// <summary>
/// A singleton service that provides transformations on <see cref="RoutePattern"/>.
/// </summary>
public abstract class RoutePatternTransformer
{
/// <summary>
/// Attempts to substitute the provided <paramref name="requiredValues"/> into the provided
/// <paramref name="original"/>.
/// </summary>
/// <param name="original">The original <see cref="RoutePattern"/>.</param>
/// <param name="requiredValues">The required values to substitute.</param>
/// <returns>
/// A new <see cref="RoutePattern"/> if substitution succeeds, otherwise <c>null</c>.
/// </returns>
/// <remarks>
/// <para>
/// Substituting required values into a route pattern is intended for us with a general-purpose
/// parameterize route specification that can match many logical endpoints. Calling
/// <see cref="SubstituteRequiredValues(RoutePattern, object)"/> can produce a derived route pattern
/// for each set of route values that corresponds to an endpoint.
/// </para>
/// <para>
/// The substitution process considers default values and <see cref="IRouteConstraint"/> implementations
/// when examining a required value. <see cref="SubstituteRequiredValues(RoutePattern, object)"/> will
/// return <c>null</c> if any required value cannot be substituted.
/// </para>
/// </remarks>
public abstract RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues);
}
}

View File

@ -0,0 +1,37 @@
// 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.Patterns;
namespace Microsoft.AspNetCore.Routing
{
public sealed class RouteEndpointModel : EndpointModel
{
public RoutePattern RoutePattern { get; set; }
public int Order { get; set; }
public RouteEndpointModel(
RequestDelegate requestDelegate,
RoutePattern routePattern,
int order)
{
RequestDelegate = requestDelegate;
RoutePattern = routePattern;
Order = order;
}
public override Endpoint Build()
{
var routeEndpoint = new RouteEndpoint(
RequestDelegate,
RoutePattern,
Order,
new EndpointMetadataCollection(Metadata),
DisplayName);
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

@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Routing
/// </remarks>
public class RouteValueEqualityComparer : IEqualityComparer<object>
{
public static readonly RouteValueEqualityComparer Default = new RouteValueEqualityComparer();
/// <inheritdoc />
public new bool Equals(object x, object y)
{

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;
@ -125,12 +125,21 @@ namespace Microsoft.AspNetCore.Routing
continue;
}
var metadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0)
{
continue;
}
if (endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>()?.SuppressLinkGeneration == true)
{
continue;
}
var entry = CreateOutboundRouteEntry(routeEndpoint);
var entry = CreateOutboundRouteEntry(
routeEndpoint,
metadata?.RequiredValues ?? routeEndpoint.RoutePattern.RequiredValues,
metadata?.RouteName);
var outboundMatch = new OutboundMatch() { Entry = entry };
allOutboundMatches.Add(outboundMatch);
@ -151,18 +160,20 @@ namespace Microsoft.AspNetCore.Routing
return (allOutboundMatches, namedOutboundMatchResults);
}
private OutboundRouteEntry CreateOutboundRouteEntry(RouteEndpoint endpoint)
private OutboundRouteEntry CreateOutboundRouteEntry(
RouteEndpoint endpoint,
IReadOnlyDictionary<string, object> requiredValues,
string routeName)
{
var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
var entry = new OutboundRouteEntry()
{
Handler = NullRouter.Instance,
Order = endpoint.Order,
Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern),
RequiredLinkValues = new RouteValueDictionary(routeValuesAddressMetadata?.RequiredValues),
RequiredLinkValues = new RouteValueDictionary(requiredValues),
RouteTemplate = new RouteTemplate(endpoint.RoutePattern),
Data = endpoint,
RouteName = routeValuesAddressMetadata?.RouteName,
RouteName = routeName,
};
entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
return entry;

View File

@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Routing.Template
// /api/template/{id:int} == 1.12
public static decimal ComputeInbound(RouteTemplate template)
{
ValidateSegementLength(template.Segments.Count);
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
// and 4 results in a combined precedence of 2.14 (decimal).
var precedence = 0m;
@ -40,6 +42,8 @@ namespace Microsoft.AspNetCore.Routing.Template
// See description on ComputeInbound(RouteTemplate)
internal static decimal ComputeInbound(RoutePattern routePattern)
{
ValidateSegementLength(routePattern.PathSegments.Count);
var precedence = 0m;
for (var i = 0; i < routePattern.PathSegments.Count; i++)
@ -62,6 +66,8 @@ namespace Microsoft.AspNetCore.Routing.Template
// /api/template/{id:int} == 5.54
public static decimal ComputeOutbound(RouteTemplate template)
{
ValidateSegementLength(template.Segments.Count);
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
// and 4 results in a combined precedence of 2.14 (decimal).
var precedence = 0m;
@ -82,6 +88,8 @@ namespace Microsoft.AspNetCore.Routing.Template
// see description on ComputeOutbound(RouteTemplate)
internal static decimal ComputeOutbound(RoutePattern routePattern)
{
ValidateSegementLength(routePattern.PathSegments.Count);
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
// and 4 results in a combined precedence of 2.14 (decimal).
var precedence = 0m;
@ -99,6 +107,15 @@ namespace Microsoft.AspNetCore.Routing.Template
return precedence;
}
private static void ValidateSegementLength(int length)
{
if (length > 28)
{
// An OverflowException will be thrown by Math.Pow when greater than 28
throw new InvalidOperationException("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.");
}
}
// Segments have the following order:
// 5 - Literal segments
// 4 - Multi-part segments && Constrained parameter segments

View File

@ -16,6 +16,13 @@ namespace Microsoft.AspNetCore.Routing.Template
public RouteTemplate(RoutePattern other)
{
if (other == null)
{
throw new ArgumentNullException(nameof(other));
}
// RequiredValues will be ignored. RouteTemplate doesn't support them.
TemplateText = other.RawText;
Segments = new List<TemplateSegment>(other.PathSegments.Select(p => new TemplateSegment(p)));
Parameters = new List<TemplatePart>();

View File

@ -1,13 +1,6 @@
<Project>
<Import Project="..\Directory.Build.props" />
<PropertyGroup>
<DeveloperBuildTestTfms>netcoreapp2.2</DeveloperBuildTestTfms>
<StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
<StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">$(StandardTestTfms)</StandardTestTfms>
<StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
</PropertyGroup>
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>

View File

@ -1,142 +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.Text;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace Microsoft.AspNetCore.Routing
{
public class EndpointMetadataCollectionTests
{
[Fact]
public void Constructor_Enumeration_ContainsValues()
{
// Arrange & Act
var metadata = new EndpointMetadataCollection(new List<object>
{
1,
2,
3,
});
// Assert
Assert.Equal(3, metadata.Count);
Assert.Collection(metadata,
value => Assert.Equal(1, value),
value => Assert.Equal(2, value),
value => Assert.Equal(3, value));
}
[Fact]
public void Constructor_ParamsArray_ContainsValues()
{
// Arrange & Act
var metadata = new EndpointMetadataCollection(1, 2, 3);
// Assert
Assert.Equal(3, metadata.Count);
Assert.Collection(metadata,
value => Assert.Equal(1, value),
value => Assert.Equal(2, value),
value => Assert.Equal(3, value));
}
[Fact]
public void GetMetadata_Match_ReturnsLastMatchingEntry()
{
// Arrange
var items = new object[]
{
new Metadata1(),
new Metadata2(),
new Metadata3(),
};
var metadata = new EndpointMetadataCollection(items);
// Act
var result = metadata.GetMetadata<IMetadata5>();
// Assert
Assert.Same(items[1], result);
}
[Fact]
public void GetMetadata_NoMatch_ReturnsNull()
{
// Arrange
var items = new object[]
{
new Metadata3(),
new Metadata3(),
new Metadata3(),
};
var metadata = new EndpointMetadataCollection(items);
// Act
var result = metadata.GetMetadata<IMetadata5>();
// Assert
Assert.Null(result);
}
[Fact]
public void GetOrderedMetadata_Match_ReturnsItemsInAscendingOrder()
{
// Arrange
var items = new object[]
{
new Metadata1(),
new Metadata2(),
new Metadata3(),
};
var metadata = new EndpointMetadataCollection(items);
// Act
var result = metadata.GetOrderedMetadata<IMetadata5>();
// Assert
Assert.Collection(
result,
i => Assert.Same(items[0], i),
i => Assert.Same(items[1], i));
}
[Fact]
public void GetOrderedMetadata_NoMatch_ReturnsEmpty()
{
// Arrange
var items = new object[]
{
new Metadata3(),
new Metadata3(),
new Metadata3(),
};
var metadata = new EndpointMetadataCollection(items);
// Act
var result = metadata.GetOrderedMetadata<IMetadata5>();
// Assert
Assert.Empty(result);
}
private interface IMetadata1 { }
private interface IMetadata2 { }
private interface IMetadata3 { }
private interface IMetadata4 { }
private interface IMetadata5 { }
private class Metadata1 : IMetadata1, IMetadata4, IMetadata5 { }
private class Metadata2 : IMetadata2, IMetadata5 { }
private class Metadata3 : IMetadata3 { }
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -207,13 +207,7 @@ namespace Microsoft.AspNetCore.Routing.DecisionTree
private class ItemClassifier : IClassifier<Item>
{
public IEqualityComparer<object> ValueComparer
{
get
{
return new RouteValueEqualityComparer();
}
}
public IEqualityComparer<object> ValueComparer => RouteValueEqualityComparer.Default;
public IDictionary<string, DecisionCriterionValue> GetCriteria(Item item)
{

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

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.
#if NETCOREAPP2_2
using System;
using System.Net;
using System.Net.Http;
@ -57,4 +56,3 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
}
}
}
#endif

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.
#if NETCOREAPP2_2
using System;
using System.Net;
using System.Net.Http;
@ -56,5 +55,4 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
_client.Dispose();
}
}
}
#endif
}

View File

@ -24,16 +24,48 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
_client.BaseAddress = new Uri("http://localhost");
}
[Theory]
[InlineData("Branch1")]
[InlineData("Branch2")]
public async Task Routing_CanRouteRequest_ToBranchRouter(string branch)
{
// Arrange
var message = new HttpRequestMessage(HttpMethod.Get, $"{branch}/api/get/5");
// Act
var response = await _client.SendAsync(message);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal($"{branch} - API Get 5", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task MatchesRootPath_AndReturnsPlaintext()
{
// Arrange
var expectedContentType = "text/plain";
var expectedContent = "Endpoint Routing sample endpoints:" + Environment.NewLine + "/plaintext";
// Act
var response = await _client.GetAsync("/");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType);
}
[Fact]
public async Task MatchesStaticRouteTemplate_AndReturnsPlaintext()
{
// Arrange
var expectedContentType = "text/plain";
var expectedContent = "Plain text!";
// Act
var response = await _client.GetAsync("/plaintext");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
@ -44,14 +76,14 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
}
[Fact]
public async Task MatchesStaticRouteTemplate_AndReturnsPlaintext()
public async Task MatchesHelloMiddleware_AndReturnsPlaintext()
{
// Arrange
var expectedContentType = "text/plain";
var expectedContent = "Hello, World!";
var expectedContent = "Hello World";
// Act
var response = await _client.GetAsync("/plaintext");
var response = await _client.GetAsync("/helloworld");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@ -10,7 +10,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\benchmarkapps\Benchmarks\Benchmarks.csproj" Condition="'$(TargetFramework)'=='netcoreapp2.2'" />
<ProjectReference Include="..\..\benchmarkapps\Benchmarks\Benchmarks.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -24,6 +24,22 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
_client.BaseAddress = new Uri("http://localhost");
}
[Theory]
[InlineData("Branch1")]
[InlineData("Branch2")]
public async Task Routing_CanRouteRequest_ToBranchRouter(string branch)
{
// Arrange
var message = new HttpRequestMessage(HttpMethod.Get, $"{branch}/api/get/5");
// Act
var response = await _client.SendAsync(message);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal($"{branch} - API Get 5", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task Routing_CanRouteRequestDelegate_ToSpecificHttpVerb()
{

View File

@ -2,20 +2,23 @@
// 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.Internal;
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;
namespace Microsoft.AspNetCore.Builder
{
public class EndpointRoutingBuilderExtensionsTest
public class EndpointRoutingApplicationBuilderExtensionsTest
{
[Fact]
public void UseEndpointRouting_ServicesNotRegistered_Throws()
@ -24,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(
@ -59,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();
@ -76,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();
@ -127,7 +133,7 @@ namespace Microsoft.AspNetCore.Builder
var app = new ApplicationBuilder(services);
app.UseEndpointRouting();
app.UseEndpointRouting(builder => { });
app.UseEndpoint();
var appFunc = app.Build();
@ -140,17 +146,84 @@ namespace Microsoft.AspNetCore.Builder
Assert.Null(httpContext.Features.Get<IEndpointFeature>());
}
private IServiceProvider CreateServices(params Endpoint[] endpoints)
[Fact]
public void UseEndpointRouting_CallWithBuilder_SetsEndpointDataSource()
{
// Arrange
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.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
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()
{
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

@ -0,0 +1,101 @@
// 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.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
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 DefaultEndpointRouteBuilder();
RequestDelegate requestDelegate = (d) => null;
// Act
var endpointBuilder = builder.Map("/", "Display name!", requestDelegate);
// Assert
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 DefaultEndpointRouteBuilder();
RequestDelegate requestDelegate = (d) => null;
// Act
var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), "Display name!", requestDelegate);
// Assert
var endpointBuilder1 = GetRouteEndpointBuilder(builder);
Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate);
Assert.Equal("Display name!", endpointBuilder1.DisplayName);
Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
}
[Fact]
public void MapEndpoint_StringPatternAndMetadata_BuildsEndpoint()
{
// Arrange
var metadata = new object();
var builder = new DefaultEndpointRouteBuilder();
RequestDelegate requestDelegate = (d) => null;
// Act
var endpointBuilder = builder.Map("/", "Display name!", requestDelegate, new[] { metadata });
// Assert
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]
public void MapEndpoint_TypedPatternAndMetadata_BuildsEndpoint()
{
// Arrange
var metadata = new object();
var builder = new DefaultEndpointRouteBuilder();
RequestDelegate requestDelegate = (d) => null;
// Act
var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), "Display name!", requestDelegate, new[] { metadata });
// Assert
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(
@ -311,17 +321,17 @@ namespace Microsoft.AspNetCore.Routing
public void GetLink_ParameterTransformer()
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}");
var routeOptions = new RouteOptions();
routeOptions.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}", requiredValues: new { controller = "Home", name = "Test" });
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" });
@ -334,17 +344,20 @@ namespace Microsoft.AspNetCore.Routing
public void GetLink_ParameterTransformer_ForQueryString()
{
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}", policies: new { c = new UpperCaseParameterTransform(), });
var routeOptions = new RouteOptions();
routeOptions.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
var endpoint = EndpointFactory.CreateRouteEndpoint(
"{controller:upper-case}/{name}",
requiredValues: new { controller = "Home", name = "Test", c = "hithere", },
policies: new { c = new 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 +720,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

@ -1,10 +1,11 @@
// 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.Patterns;
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing
{

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

@ -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.
#if IL_EMIT
using System;
using Xunit;
@ -50,4 +49,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
}
}
#endif

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.
#if IL_EMIT
using Moq;
using System.Threading.Tasks;
using Xunit;
@ -225,4 +224,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
}
}
#endif

View File

@ -21,7 +21,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
httpContext.Request.Path = path;
httpContext.RequestServices = CreateServices();
var context = new EndpointSelectorContext();
var context = new EndpointSelectorContext()
{
RouteValues = new RouteValueDictionary()
};
httpContext.Features.Set<IEndpointFeature>(context);
httpContext.Features.Set<IRouteValuesFeature>(context);

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.
#if IL_EMIT
namespace Microsoft.AspNetCore.Routing.Matching
{
public class NonVectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase
@ -9,4 +8,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
public override bool Vectorize => false;
}
}
#endif

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.
#if IL_EMIT
namespace Microsoft.AspNetCore.Routing.Matching
{
public class VectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase
@ -11,4 +10,3 @@ namespace Microsoft.AspNetCore.Routing.Matching
public override bool Vectorize => true;
}
}
#endif

View File

@ -1,23 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
<RootNamespace>Microsoft.AspNetCore.Routing</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<!--
RefEmit is supported in netcoreapp. We test on .NET Framework, but we don't support RefEmit in the product
on .NET Framework.
-->
<ILEmit Condition="'$(TargetFramework)'=='netcoreapp2.2'">true</ILEmit>
<DefineConstants Condition="'$(ILEmit)'=='true'">IL_EMIT;$(DefineConstants)</DefineConstants>
</PropertyGroup>
<PropertyGroup>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
</ItemGroup>

View File

@ -0,0 +1,339 @@
// 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.Constraints;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Patterns
{
public class DefaultRoutePatternTransformerTest
{
public DefaultRoutePatternTransformerTest()
{
var services = new ServiceCollection();
services.AddRouting();
services.AddOptions();
Transformer = services.BuildServiceProvider().GetRequiredService<RoutePatternTransformer>();
}
public RoutePatternTransformer Transformer { get; }
[Fact]
public void SubstituteRequiredValues_CanAcceptNullForAnyKey()
{
// Arrange
var template = "{controller=Home}/{action=Index}/{id?}";
var defaults = new { };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { a = (string)null, b = "", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("a", null), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("b", string.Empty), kvp));
}
[Fact]
public void SubstituteRequiredValues_RejectsNullForParameter()
{
// Arrange
var template = "{controller=Home}/{action=Index}/{id?}";
var defaults = new { };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = string.Empty, };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Null(actual);
}
[Fact]
public void SubstituteRequiredValues_RejectsNullForOutOfLineDefault()
{
// Arrange
var template = "{controller=Home}/{action=Index}/{id?}";
var defaults = new { area = "Admin" };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { area = string.Empty, };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Null(actual);
}
[Fact]
public void SubstituteRequiredValues_CanAcceptValueForParameter()
{
// Arrange
var template = "{controller}/{action}/{id?}";
var defaults = new { };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = "Home", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
}
[Fact]
public void SubstituteRequiredValues_CanAcceptValueForParameter_WithSameDefault()
{
// Arrange
var template = "{controller=Home}/{action=Index}/{id?}";
var defaults = new { };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = "Home", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
// We should not need to rewrite anything in this case.
Assert.Same(actual.Defaults, original.Defaults);
Assert.Same(actual.Parameters, original.Parameters);
Assert.Same(actual.PathSegments, original.PathSegments);
}
[Fact]
public void SubstituteRequiredValues_CanAcceptValueForParameter_WithDifferentDefault()
{
// Arrange
var template = "{controller=Blog}/{action=ReadPost}/{id?}";
var defaults = new { area = "Admin", };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { area = "Admin", controller = "Home", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("area", "Admin"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
// We should not need to rewrite anything in this case.
Assert.NotSame(actual.Defaults, original.Defaults);
Assert.NotSame(actual.Parameters, original.Parameters);
Assert.NotSame(actual.PathSegments, original.PathSegments);
// other defaults were wiped out
Assert.Equal(new KeyValuePair<string, object>("area", "Admin"), Assert.Single(actual.Defaults));
Assert.Null(actual.GetParameter("controller").Default);
Assert.False(actual.Defaults.ContainsKey("controller"));
Assert.Null(actual.GetParameter("action").Default);
Assert.False(actual.Defaults.ContainsKey("action"));
}
[Fact]
public void SubstituteRequiredValues_CanAcceptValueForParameter_WithMatchingConstraint()
{
// Arrange
var template = "{controller}/{action}/{id?}";
var defaults = new { };
var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = "Home", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
}
[Fact]
public void SubstituteRequiredValues_CanRejectValueForParameter_WithNonMatchingConstraint()
{
// Arrange
var template = "{controller}/{action}/{id?}";
var defaults = new { };
var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = "Blog", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Null(actual);
}
[Fact]
public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue()
{
// Arrange
var template = "Home/Index/{id?}";
var defaults = new { controller = "Home", action = "Index", };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = "Home", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
}
[Fact]
public void SubstituteRequiredValues_CanRejectValueForDefault_WithDifferentValue()
{
// Arrange
var template = "Home/Index/{id?}";
var defaults = new { controller = "Home", action = "Index", };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = "Blog", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Null(actual);
}
[Fact]
public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_Null()
{
// Arrange
var template = "Home/Index/{id?}";
var defaults = new { controller = (string)null, action = "", };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = string.Empty, action = (string)null, };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("action", null), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", ""), kvp));
}
[Fact]
public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_WithMatchingConstraint()
{
// Arrange
var template = "Home/Index/{id?}";
var defaults = new { controller = "Home", action = "Index", };
var policies = new { controller = "Home", };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = "Home", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
}
[Fact]
public void SubstituteRequiredValues_CanRejectValueForDefault_WithSameValue_WithNonMatchingConstraint()
{
// Arrange
var template = "Home/Index/{id?}";
var defaults = new { controller = "Home", action = "Index", };
var policies = new { controller = "Home", };
var original = RoutePatternFactory.Parse(template, defaults, policies);
var requiredValues = new { controller = "Home", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
}
[Fact]
public void SubstituteRequiredValues_CanMergeExistingRequiredValues()
{
// Arrange
var template = "Home/Index/{id?}";
var defaults = new { area = "Admin", controller = "Home", action = "Index", };
var policies = new { };
var original = RoutePatternFactory.Parse(template, defaults, policies, new { area = "Admin", controller = "Home", });
var requiredValues = new { controller = "Home", action = "Index", };
// Act
var actual = Transformer.SubstituteRequiredValues(original, requiredValues);
// Assert
Assert.Collection(
actual.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(new KeyValuePair<string, object>("action", "Index"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("area", "Admin"), kvp),
kvp => Assert.Equal(new KeyValuePair<string, object>("controller", "Home"), kvp));
}
}
}

View File

@ -441,6 +441,89 @@ namespace Microsoft.AspNetCore.Routing.Patterns
Assert.Null(paramPartD.Default);
}
[Fact]
public void Parse_WithRequiredValues()
{
// Arrange
var template = "{controller=Home}/{action=Index}/{id?}";
var defaults = new { area = "Admin", };
var policies = new { };
var requiredValues = new { area = "Admin", controller = "Store", action = "Index", };
// Act
var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
// Assert
Assert.Collection(
action.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); },
kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("Admin", kvp.Value); },
kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); });
}
[Fact]
public void Parse_WithRequiredValues_AllowsNullRequiredValue()
{
// Arrange
var template = "{controller=Home}/{action=Index}/{id?}";
var defaults = new { };
var policies = new { };
var requiredValues = new { area = (string)null, controller = "Store", action = "Index", };
// Act
var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
// Assert
Assert.Collection(
action.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); },
kvp => { Assert.Equal("area", kvp.Key); Assert.Null(kvp.Value); },
kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); });
}
[Fact]
public void Parse_WithRequiredValues_AllowsEmptyRequiredValue()
{
// Arrange
var template = "{controller=Home}/{action=Index}/{id?}";
var defaults = new { };
var policies = new { };
var requiredValues = new { area = "", controller = "Store", action = "Index", };
// Act
var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
// Assert
Assert.Collection(
action.RequiredValues.OrderBy(kvp => kvp.Key),
kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); },
kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("", kvp.Value); },
kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); });
}
[Fact]
public void Parse_WithRequiredValues_ThrowsForNonParameterNonDefault()
{
// Arrange
var template = "{controller=Home}/{action=Index}/{id?}";
var defaults = new { };
var policies = new { };
var requiredValues = new { area = "Admin", controller = "Store", action = "Index", };
// Act
var exception = Assert.Throws<InvalidOperationException>(() =>
{
var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues);
});
// Assert
Assert.Equal(
"No corresponding parameter or default value could be found for the required value " +
"'area=Admin'. A non-null required value must correspond to a route parameter or the " +
"route pattern must have a matching default value.",
exception.Message);
}
[Fact]
public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndArrayOfParameterPolicies_ShouldMakeCopyOfParameterPolicies()
{

View File

@ -0,0 +1,36 @@
// 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.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
namespace Microsoft.AspNetCore.Routing
{
public class RouteEndpointModelTest
{
[Fact]
public void Build_AllValuesSet_EndpointCreated()
{
const int defaultOrder = 0;
var metadata = new object();
RequestDelegate requestDelegate = (d) => null;
var builder = new RouteEndpointModel(requestDelegate, RoutePatternFactory.Parse("/"), defaultOrder)
{
DisplayName = "Display name!",
Metadata = { metadata }
};
var endpoint = Assert.IsType<RouteEndpoint>(builder.Build());
Assert.Equal("Display name!", endpoint.DisplayName);
Assert.Equal(defaultOrder, endpoint.Order);
Assert.Equal(requestDelegate, endpoint.RequestDelegate);
Assert.Equal("/", endpoint.RoutePattern.RawText);
Assert.Equal(metadata, Assert.Single(endpoint.Metadata));
}
}
}

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Routing
public void GetOutboundMatches_GetsNamedMatchesFor_EndpointsHaving_IRouteNameMetadata()
{
// Arrange
var endpoint1 = CreateEndpoint("/a");
var endpoint1 = CreateEndpoint("/a", routeName: "other");
var endpoint2 = CreateEndpoint("/a", routeName: "named");
// Act
@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing
public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName()
{
// Arrange
var endpoint1 = CreateEndpoint("/a");
var endpoint1 = CreateEndpoint("/a", routeName: "other");
var endpoint2 = CreateEndpoint("/a", routeName: "named");
var endpoint3 = CreateEndpoint("/b", routeName: "named");
@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Routing
public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName_IgnoringCase()
{
// Arrange
var endpoint1 = CreateEndpoint("/a");
var endpoint1 = CreateEndpoint("/a", routeName: "other");
var endpoint2 = CreateEndpoint("/a", routeName: "named");
var endpoint3 = CreateEndpoint("/b", routeName: "NaMed");
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing
public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches()
{
// Arrange 1
var endpoint1 = CreateEndpoint("/a");
var endpoint1 = CreateEndpoint("/a", metadataRequiredValues: new { });
var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
// Act 1
@ -93,21 +93,21 @@ namespace Microsoft.AspNetCore.Routing
Assert.Same(endpoint1, actual);
// Arrange 2
var endpoint2 = CreateEndpoint("/b");
var endpoint2 = CreateEndpoint("/b", metadataRequiredValues: new { });
// Act 2
// Trigger change
dynamicDataSource.AddEndpoint(endpoint2);
// Arrange 2
var endpoint3 = CreateEndpoint("/c");
var endpoint3 = CreateEndpoint("/c", metadataRequiredValues: new { });
// Act 2
// Trigger change
dynamicDataSource.AddEndpoint(endpoint3);
// Arrange 3
var endpoint4 = CreateEndpoint("/d");
var endpoint4 = CreateEndpoint("/d", metadataRequiredValues: new { });
// Act 3
// Trigger change
@ -146,13 +146,11 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { zipCode = 3510 },
requiredValues: new { id = 7 },
routeName: "OrdersApi");
metadataRequiredValues: new { id = 7 });
var endpoint2 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { id = 12 },
requiredValues: new { zipCode = 3510 },
routeName: "OrdersApi");
metadataRequiredValues: new { zipCode = 3510 });
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
// Act
@ -174,46 +172,12 @@ namespace Microsoft.AspNetCore.Routing
var endpoint1 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { zipCode = 3510 },
requiredValues: new { id = 7 },
routeName: "OrdersApi");
metadataRequiredValues: new { id = 7 });
var endpoint2 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { id = 12 },
routeName: "OrdersApi");
defaults: new { id = 12 });
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
// Act
var foundEndpoints = addressScheme.FindEndpoints(
new RouteValuesAddress
{
ExplicitValues = new RouteValueDictionary(new { id = 13 }),
AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }),
});
// Assert
var actual = Assert.Single(foundEndpoints);
Assert.Same(endpoint2, actual);
}
[Fact]
public void FindEndpoints_LookedUpByCriteria_MultipleMatches()
{
// Arrange
var endpoint1 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { zipCode = 3510 },
requiredValues: new { id = 7 },
routeName: "OrdersApi");
var endpoint2 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent}/{zipCode}",
defaults: new { id = 12 },
routeName: "OrdersApi");
var endpoint3 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { id = 12 },
routeName: "OrdersApi");
var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3);
// Act
var foundEndpoints = addressScheme.FindEndpoints(
new RouteValuesAddress
@ -223,7 +187,64 @@ namespace Microsoft.AspNetCore.Routing
});
// Assert
Assert.Contains(endpoint1, foundEndpoints);
var actual = Assert.Single(foundEndpoints);
Assert.Same(endpoint1, actual);
}
[Fact]
public void FindEndpoints_LookedUpByCriteria_MultipleMatches()
{
// Arrange
var endpoint1 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { zipCode = 3510 },
metadataRequiredValues: new { id = 7 });
var endpoint2 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent}/{zipCode}",
defaults: new { id = 12 },
metadataRequiredValues: new { id = 12 });
var endpoint3 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { id = 12 },
metadataRequiredValues: new { id = 12 });
var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3);
// Act
var foundEndpoints = addressScheme.FindEndpoints(
new RouteValuesAddress
{
ExplicitValues = new RouteValueDictionary(new { id = 12 }),
AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }),
});
// Assert
Assert.Collection(foundEndpoints,
e => Assert.Equal(endpoint3, e),
e => Assert.Equal(endpoint2, e));
}
[Fact]
public void FindEndpoints_LookedUpByCriteria_ExcludeEndpointWithoutRouteValuesAddressMetadata()
{
// Arrange
var endpoint1 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { zipCode = 3510 },
metadataRequiredValues: new { id = 7 });
var endpoint2 = CreateEndpoint("test");
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
// Act
var foundEndpoints = addressScheme.FindEndpoints(
new RouteValuesAddress
{
ExplicitValues = new RouteValueDictionary(new { id = 7 }),
AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }),
}).ToList();
// Assert
Assert.DoesNotContain(endpoint2, foundEndpoints);
Assert.Contains(endpoint1, foundEndpoints);
}
@ -234,7 +255,7 @@ namespace Microsoft.AspNetCore.Routing
var expected = CreateEndpoint(
"api/orders/{id}",
defaults: new { controller = "Orders", action = "GetById" },
requiredValues: new { controller = "Orders", action = "GetById" },
metadataRequiredValues: new { controller = "Orders", action = "GetById" },
routeName: "OrdersApi");
var addressScheme = CreateAddressScheme(expected);
@ -252,6 +273,29 @@ namespace Microsoft.AspNetCore.Routing
Assert.Same(expected, actual);
}
[Fact]
public void FindEndpoints_ReturnsEndpoint_UsingRoutePatternRequiredValues()
{
// Arrange
var expected = CreateEndpoint(
"api/orders/{id}",
defaults: new { controller = "Orders", action = "GetById" },
routePatternRequiredValues: new { controller = "Orders", action = "GetById" });
var addressScheme = CreateAddressScheme(expected);
// Act
var foundEndpoints = addressScheme.FindEndpoints(
new RouteValuesAddress
{
ExplicitValues = new RouteValueDictionary(new { id = 10 }),
AmbientValues = new RouteValueDictionary(new { controller = "Orders", action = "GetById" }),
});
// Assert
var actual = Assert.Single(foundEndpoints);
Assert.Same(expected, actual);
}
[Fact]
public void FindEndpoints_AlwaysReturnsEndpointsByRouteName_IgnoringMissingRequiredParameterValues()
{
@ -263,7 +307,7 @@ namespace Microsoft.AspNetCore.Routing
var expected = CreateEndpoint(
"api/orders/{id}",
defaults: new { controller = "Orders", action = "GetById" },
requiredValues: new { controller = "Orders", action = "GetById" },
metadataRequiredValues: new { controller = "Orders", action = "GetById" },
routeName: "OrdersApi");
var addressScheme = CreateAddressScheme(expected);
@ -302,7 +346,7 @@ namespace Microsoft.AspNetCore.Routing
// Arrange
var endpoint = EndpointFactory.CreateRouteEndpoint(
"/a",
metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), });
metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteValuesAddressMetadata(string.Empty), });
// Act
var addressScheme = CreateAddressScheme(endpoint);
@ -324,7 +368,8 @@ namespace Microsoft.AspNetCore.Routing
private RouteEndpoint CreateEndpoint(
string template,
object defaults = null,
object requiredValues = null,
object metadataRequiredValues = null,
object routePatternRequiredValues = null,
int order = 0,
string routeName = null,
EndpointMetadataCollection metadataCollection = null)
@ -332,16 +377,16 @@ namespace Microsoft.AspNetCore.Routing
if (metadataCollection == null)
{
var metadata = new List<object>();
if (!string.IsNullOrEmpty(routeName) || requiredValues != null)
if (!string.IsNullOrEmpty(routeName) || metadataRequiredValues != null)
{
metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues)));
metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(metadataRequiredValues)));
}
metadataCollection = new EndpointMetadataCollection(metadata);
}
return new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: routePatternRequiredValues),
order,
metadataCollection,
null);

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Template
{
public class RoutePatternPrecedenceTests : RoutePrecedenceTestsBase
{
protected override decimal ComputeMatched(string template)
{
return ComputeRoutePattern(template, RoutePrecedence.ComputeInbound);
}
protected override decimal ComputeGenerated(string template)
{
return ComputeRoutePattern(template, RoutePrecedence.ComputeOutbound);
}
private static decimal ComputeRoutePattern(string template, Func<RoutePattern, decimal> func)
{
var parsed = RoutePatternFactory.Parse(template);
return func(parsed);
}
}
}

View File

@ -2,13 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Template
{
public class RoutePrecedenceTests
public abstract class RoutePrecedenceTestsBase
{
[Theory]
[InlineData("Employees/{id}", "Employees/{employeeId}")]
@ -100,22 +98,34 @@ namespace Microsoft.AspNetCore.Routing.Template
Assert.True(xPrecedence > yPrecedence);
}
private static decimal ComputeMatched(string template)
[Fact]
public void ComputeGenerated_TooManySegments_ThrowHumaneError()
{
return Compute(template, RoutePrecedence.ComputeInbound);
}
private static decimal ComputeGenerated(string template)
{
return Compute(template, RoutePrecedence.ComputeOutbound);
var ex = Assert.Throws<InvalidOperationException>(() =>
{
// Arrange & Act
ComputeGenerated("{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}/{r}/{s}/{t}/{u}/{v}/{w}/{x}/{y}/{z}/{a2}/{b2}/{b3}");
});
// Assert
Assert.Equal("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.", ex.Message);
}
private static decimal Compute(string template, Func<RouteTemplate, decimal> func)
[Fact]
public void ComputeMatched_TooManySegments_ThrowHumaneError()
{
var options = new Mock<IOptions<RouteOptions>>();
options.SetupGet(o => o.Value).Returns(new RouteOptions());
var ex = Assert.Throws<InvalidOperationException>(() =>
{
// Arrange & Act
ComputeMatched("{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}/{r}/{s}/{t}/{u}/{v}/{w}/{x}/{y}/{z}/{a2}/{b2}/{b3}");
});
var parsed = TemplateParser.Parse(template);
return func(parsed);
// Assert
Assert.Equal("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.", ex.Message);
}
protected abstract decimal ComputeMatched(string template);
protected abstract decimal ComputeGenerated(string template);
}
}

View File

@ -0,0 +1,26 @@
// 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.Template
{
public class RouteTemplatePrecedenceTests : RoutePrecedenceTestsBase
{
protected override decimal ComputeMatched(string template)
{
return ComputeRouteTemplate(template, RoutePrecedence.ComputeInbound);
}
protected override decimal ComputeGenerated(string template)
{
return ComputeRouteTemplate(template, RoutePrecedence.ComputeOutbound);
}
private static decimal ComputeRouteTemplate(string template, Func<RouteTemplate, decimal> func)
{
var parsed = TemplateParser.Parse(template);
return func(parsed);
}
}
}

View File

@ -1,11 +1,4 @@
<Project>
<!-- Skip the parent folder to prevent getting test package references. -->
<Import Project="..\..\Directory.Build.props" />
<PropertyGroup>
<DeveloperBuildTestWebsiteTfms>netcoreapp2.2</DeveloperBuildTestWebsiteTfms>
<StandardTestWebsiteTfms>$(DeveloperBuildTestWebsiteTfms)</StandardTestWebsiteTfms>
<StandardTestWebsiteTfms Condition=" '$(DeveloperBuild)' != 'true' ">netcoreapp2.2</StandardTestWebsiteTfms>
<StandardTestWebsiteTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestWebsiteTfms);net461</StandardTestWebsiteTfms>
</PropertyGroup>
</Project>

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 System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder
{
public static class EndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapHello(this IEndpointRouteBuilder builder, string template, string greeter)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
var pipeline = builder.CreateApplicationBuilder()
.UseHello(greeter)
.Build();
return builder.Map(
template,
"Hello " + greeter,
pipeline);
}
}
}

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 Microsoft.Extensions.Options;
using RoutingWebSite.HelloExtension;
namespace Microsoft.AspNetCore.Builder
{
public static class HelloAppBuilderExtensions
{
public static IApplicationBuilder UseHello(this IApplicationBuilder app, string greeter)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.UseMiddleware<HelloMiddleware>(Options.Create(new HelloOptions
{
Greeter = greeter
}));
}
}
}

View File

@ -0,0 +1,45 @@
// 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.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace RoutingWebSite.HelloExtension
{
public class HelloMiddleware
{
private readonly RequestDelegate _next;
private readonly HelloOptions _helloOptions;
private readonly byte[] _helloPayload;
public HelloMiddleware(RequestDelegate next, IOptions<HelloOptions> helloOptions)
{
_next = next;
_helloOptions = helloOptions.Value;
var payload = new List<byte>();
payload.AddRange(Encoding.UTF8.GetBytes("Hello"));
if (!string.IsNullOrEmpty(_helloOptions.Greeter))
{
payload.Add((byte)' ');
payload.AddRange(Encoding.UTF8.GetBytes(_helloOptions.Greeter));
}
_helloPayload = payload.ToArray();
}
public Task InvokeAsync(HttpContext context)
{
var response = context.Response;
var payloadLength = _helloPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloPayload, 0, payloadLength);
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace RoutingWebSite.HelloExtension
{
public class HelloOptions
{
public string Greeter { get; set; }
}
}

View File

@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net461</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -3,10 +3,12 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
@ -19,7 +21,7 @@ namespace RoutingWebSite
public class UseEndpointRoutingStartup
{
private static readonly byte[] _homePayload = Encoding.UTF8.GetBytes("Endpoint Routing sample endpoints:" + Environment.NewLine + "/plaintext");
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
private static readonly byte[] _plainTextPayload = Encoding.UTF8.GetBytes("Plain text!");
public void ConfigureServices(IServiceCollection services)
{
@ -29,114 +31,91 @@ namespace RoutingWebSite
{
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringRouteConstraint));
});
var endpointDataSource = new DefaultEndpointDataSource(new[]
{
new RouteEndpoint((httpContext) =>
{
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);
},
RoutePatternFactory.Parse("/"),
0,
EndpointMetadataCollection.Empty,
"Home"),
new RouteEndpoint((httpContext) =>
{
var response = httpContext.Response;
var payloadLength = _helloWorldPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
},
RoutePatternFactory.Parse("/plaintext"),
0,
EndpointMetadataCollection.Empty,
"Plaintext"),
new RouteEndpoint((httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("WithConstraints");
},
RoutePatternFactory.Parse("/withconstraints/{id:endsWith(_001)}"),
0,
EndpointMetadataCollection.Empty,
"withconstraints"),
new RouteEndpoint((httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("withoptionalconstraints");
},
RoutePatternFactory.Parse("/withoptionalconstraints/{id:endsWith(_001)?}"),
0,
EndpointMetadataCollection.Empty,
"withoptionalconstraints"),
new RouteEndpoint((httpContext) =>
{
using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
{
var graphWriter = httpContext.RequestServices.GetRequiredService<DfaGraphWriter>();
var dataSource = httpContext.RequestServices.GetRequiredService<CompositeEndpointDataSource>();
graphWriter.Write(dataSource, writer);
}
return Task.CompletedTask;
},
RoutePatternFactory.Parse("/graph"),
0,
new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })),
"DFA Graph"),
new RouteEndpoint((httpContext) =>
{
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync(
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { }));
},
RoutePatternFactory.Parse("/WithSingleAsteriskCatchAll/{*path}"),
0,
new EndpointMetadataCollection(
new RouteValuesAddressMetadata(
routeName: "WithSingleAsteriskCatchAll",
requiredValues: new RouteValueDictionary())),
"WithSingleAsteriskCatchAll"),
new RouteEndpoint((httpContext) =>
{
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync(
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
},
RoutePatternFactory.Parse("/WithDoubleAsteriskCatchAll/{**path}"),
0,
new EndpointMetadataCollection(
new RouteValuesAddressMetadata(
routeName: "WithDoubleAsteriskCatchAll",
requiredValues: new RouteValueDictionary())),
"WithDoubleAsteriskCatchAll"),
});
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
}
public void Configure(IApplicationBuilder app)
{
app.UseEndpointRouting();
app.UseEndpointRouting(routes =>
{
routes.MapHello("/helloworld", "World");
routes.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;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync(sb.ToString());
});
routes.MapGet(
"/plaintext",
(httpContext) =>
{
var response = httpContext.Response;
var payloadLength = _plainTextPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength);
});
routes.MapGet(
"/withconstraints/{id:endsWith(_001)}",
(httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("WithConstraints");
});
routes.MapGet(
"/withoptionalconstraints/{id:endsWith(_001)?}",
(httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("withoptionalconstraints");
});
routes.MapGet(
"/WithSingleAsteriskCatchAll/{*path}",
(httpContext) =>
{
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync(
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { }));
},
new RouteValuesAddressMetadata(routeName: "WithSingleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
routes.MapGet(
"/WithDoubleAsteriskCatchAll/{**path}",
(httpContext) =>
{
var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync(
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
},
new RouteValuesAddressMetadata(routeName: "WithDoubleAsteriskCatchAll", requiredValues: new RouteValueDictionary()));
});
app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));
app.Map("/Branch2", branch => SetupBranch(branch, "Branch2"));
app.UseStaticFiles();
@ -144,5 +123,15 @@ namespace RoutingWebSite
app.UseEndpoint();
}
private void SetupBranch(IApplicationBuilder app, string name)
{
app.UseEndpointRouting(routes =>
{
routes.MapGet("api/get/{id}", (context) => context.Response.WriteAsync($"{name} - API Get {context.GetRouteData().Values["id"]}"));
});
app.UseEndpoint();
}
}
}

View File

@ -38,6 +38,17 @@ namespace RoutingWebSite
defaults: new { lastName = "Doe" },
constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) });
});
app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));
app.Map("/Branch2", branch => SetupBranch(branch, "Branch2"));
}
private void SetupBranch(IApplicationBuilder app, string name)
{
app.UseRouter(routes =>
{
routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"{name} - API Get {routeData.Values["id"]}"));
});
}
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -1,12 +1,6 @@
<Project>
<Project>
<PropertyGroup>
<VersionPrefix>2.2.0</VersionPrefix>
<VersionSuffix>rtm</VersionSuffix>
<PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
<PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
<BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
<FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
<VersionPrefix>3.0.0</VersionPrefix>
<VersionSuffix Condition=" '$(VersionSuffix)' == '' ">dev</VersionSuffix>
</PropertyGroup>
</Project>