Code dump of dispatcher prototype code
Here's a code dump of the parts of the Dispatcher prototype codebase that are needed to get us off the ground. This first cut attempts to use part of routing where possible, and not all of those changes will be long-lasting. I'll leave comments through thoughout the PR for education.
This commit is contained in:
parent
acea87a617
commit
0ea16ddd57
15
Routing.sln
15
Routing.sln
|
|
@ -43,6 +43,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6DC6B416
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.Performance", "benchmarks\Microsoft.AspNetCore.Routing.Performance\Microsoft.AspNetCore.Routing.Performance.csproj", "{F3D86714-4E64-41A6-9B36-A47B3683CF5D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DispatcherSample.Web", "samples\DispatcherSample.Web\DispatcherSample.Web.csproj", "{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{D5F39F59-5725-4127-82E7-67028D006185}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarkapps", "benchmarkapps", "{7F5914E2-C63F-4759-898E-462804357C90}"
|
||||
|
|
@ -161,6 +163,18 @@ Global
|
|||
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -175,6 +189,7 @@ Global
|
|||
{5C73140B-41F3-466F-A07B-3614E4D80DF9} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
|
||||
{F3D86714-4E64-41A6-9B36-A47B3683CF5D} = {D5F39F59-5725-4127-82E7-67028D006185}
|
||||
{91F47A60-9A78-4968-B10D-157D9BFAC37F} = {7F5914E2-C63F-4759-898E-462804357C90}
|
||||
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
|
||||
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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 Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace DispatcherSample.Web
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var webHost = GetWebHostBuilder().Build();
|
||||
webHost.Run();
|
||||
}
|
||||
|
||||
// For unit testing
|
||||
public static IWebHostBuilder GetWebHostBuilder()
|
||||
{
|
||||
return new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseIISIntegration()
|
||||
.UseStartup<Startup>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DispatcherSample.Web
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddRouting();
|
||||
services.AddDispatcher(options =>
|
||||
{
|
||||
options.DataSources.Add(new DefaultEndpointDataSource(new[]
|
||||
{
|
||||
new MatcherEndpoint((next) => (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);
|
||||
},
|
||||
"/plaintext", new { }, 0, EndpointMetadataCollection.Empty, "Plaintext"),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseDispatcher();
|
||||
|
||||
// Imagine some more stuff here...
|
||||
|
||||
app.UseEndpoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +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.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Constraints;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace RoutingSample.Web
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var webHost = GetWebHostBuilder().Build();
|
||||
|
|
@ -28,23 +20,7 @@ namespace RoutingSample.Web
|
|||
return new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseIISIntegration()
|
||||
.ConfigureServices(services => services.AddRouting())
|
||||
.Configure(app => app.UseRouter(routes =>
|
||||
{
|
||||
routes.DefaultHandler = new RouteHandler((httpContext) =>
|
||||
{
|
||||
var request = httpContext.Request;
|
||||
return httpContext.Response.WriteAsync($"Verb = {request.Method.ToUpperInvariant()} - Path = {request.Path} - Route values - {string.Join(", ", httpContext.GetRouteData().Values)}");
|
||||
});
|
||||
|
||||
routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"API Get {routeData.Values["id"]}"))
|
||||
.MapMiddlewareRoute("api/middleware", (appBuilder) => appBuilder.Use((httpContext, next) => httpContext.Response.WriteAsync("Middleware!")))
|
||||
.MapRoute(
|
||||
name: "AllVerbs",
|
||||
template: "api/all/{name}/{lastName?}",
|
||||
defaults: new { lastName = "Doe" },
|
||||
constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) });
|
||||
}));
|
||||
.UseStartup<Startup>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Constraints;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace RoutingSample.Web
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddRouting();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseRouter(routes =>
|
||||
{
|
||||
routes.DefaultHandler = new RouteHandler((httpContext) =>
|
||||
{
|
||||
var request = httpContext.Request;
|
||||
return httpContext.Response.WriteAsync($"Verb = {request.Method.ToUpperInvariant()} - Path = {request.Path} - Route values - {string.Join(", ", httpContext.GetRouteData().Values)}");
|
||||
});
|
||||
|
||||
routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"API Get {routeData.Values["id"]}"))
|
||||
.MapMiddlewareRoute("api/middleware", (appBuilder) => appBuilder.Use((httpContext, next) => httpContext.Response.WriteAsync("Middleware!")))
|
||||
.MapRoute(
|
||||
name: "AllVerbs",
|
||||
template: "api/all/{name}/{lastName?}",
|
||||
defaults: new { lastName = "Doe" },
|
||||
constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// 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.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
[DebuggerDisplay("{DisplayName,nq}")]
|
||||
public abstract class Endpoint
|
||||
{
|
||||
protected Endpoint(EndpointMetadataCollection metadata, string displayName)
|
||||
{
|
||||
// Both allowed to be null
|
||||
Metadata = metadata ?? EndpointMetadataCollection.Empty;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
public string DisplayName { get; }
|
||||
|
||||
public EndpointMetadataCollection Metadata { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public interface IEndpointFeature
|
||||
{
|
||||
Endpoint Endpoint { get; set; }
|
||||
|
||||
Func<RequestDelegate, RequestDelegate> Invoker { get; set; }
|
||||
|
||||
RouteValueDictionary Values { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class EndpointMetadataCollection : IReadOnlyList<object>
|
||||
{
|
||||
public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty<object>());
|
||||
|
||||
private readonly object[] _items;
|
||||
|
||||
public EndpointMetadataCollection(IEnumerable<object> items)
|
||||
{
|
||||
if (items == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
}
|
||||
|
||||
_items = items.ToArray();
|
||||
}
|
||||
|
||||
public object this[int index] => _items[index];
|
||||
|
||||
public int Count => _items.Length;
|
||||
|
||||
public T GetMetadata<T>() where T : class
|
||||
{
|
||||
for (var i = _items.Length -1; i >= 0; i--)
|
||||
{
|
||||
var item = _items[i] as T;
|
||||
if (item !=null)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetOrderedMetadata<T>() where T : class
|
||||
{
|
||||
for (var i = 0; i < _items.Length; i++)
|
||||
{
|
||||
var item = _items[i] as T;
|
||||
if (item != null)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator() => new Enumerator(this);
|
||||
|
||||
IEnumerator<object> IEnumerable<object>.GetEnumerator() => GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public struct Enumerator : IEnumerator<object>
|
||||
{
|
||||
private object[] _items;
|
||||
private int _index;
|
||||
private object _current;
|
||||
|
||||
internal Enumerator(EndpointMetadataCollection collection)
|
||||
{
|
||||
_items = collection._items;
|
||||
_index = 0;
|
||||
_current = null;
|
||||
}
|
||||
|
||||
public object Current => _current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index < _items.Length)
|
||||
{
|
||||
_current = _items[_index++];
|
||||
return true;
|
||||
}
|
||||
|
||||
_current = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_index = 0;
|
||||
_current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// 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.Builder
|
||||
{
|
||||
public static class DispatcherApplicationBuilderExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseDispatcher(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseMiddleware<DispatcherMiddleware>();
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UseEndpoint(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseMiddleware<EndpointMiddleware>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// 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.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class CompositeEndpointDataSource : EndpointDataSource
|
||||
{
|
||||
private readonly EndpointDataSource[] _dataSources;
|
||||
private readonly object _lock;
|
||||
|
||||
private IChangeToken _changeToken;
|
||||
private IReadOnlyList<Endpoint> _endpoints;
|
||||
|
||||
internal CompositeEndpointDataSource(IEnumerable<EndpointDataSource> dataSources)
|
||||
{
|
||||
if (dataSources == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSources));
|
||||
}
|
||||
|
||||
_dataSources = dataSources.ToArray();
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
public override IChangeToken ChangeToken
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _changeToken;
|
||||
}
|
||||
}
|
||||
|
||||
public override IReadOnlyList<Endpoint> Endpoints
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _endpoints;
|
||||
}
|
||||
}
|
||||
|
||||
// Defer initialization to avoid doing lots of reflection on startup.
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_changeToken == null)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
// Note: we can't use DataSourceDependantCache here because we also need to handle a list of change
|
||||
// tokens, which is a complication most of our code doesn't have.
|
||||
private void Initialize()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_changeToken = new CompositeChangeToken(_dataSources.Select(d => d.ChangeToken).ToArray());
|
||||
_endpoints = _dataSources.SelectMany(d => d.Endpoints).ToArray();
|
||||
|
||||
_changeToken.RegisterChangeCallback((state) => Initialize(), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class DefaultEndpointDataSource : EndpointDataSource
|
||||
{
|
||||
private readonly List<Endpoint> _endpoints;
|
||||
|
||||
public DefaultEndpointDataSource(IEnumerable<Endpoint> endpoints)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
_endpoints = new List<Endpoint>();
|
||||
_endpoints.AddRange(endpoints);
|
||||
}
|
||||
|
||||
public override IChangeToken ChangeToken { get; } = NullChangeToken.Singleton;
|
||||
|
||||
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// 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.Routing;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
internal class ConfigureDispatcherOptions : IConfigureOptions<DispatcherOptions>
|
||||
{
|
||||
private readonly IEnumerable<EndpointDataSource> _dataSources;
|
||||
|
||||
public ConfigureDispatcherOptions(IEnumerable<EndpointDataSource> dataSources)
|
||||
{
|
||||
if (dataSources == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSources));
|
||||
}
|
||||
|
||||
_dataSources = dataSources;
|
||||
}
|
||||
|
||||
public void Configure(DispatcherOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
foreach (var dataSource in _dataSources)
|
||||
{
|
||||
options.DataSources.Add(dataSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// 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;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public static class DispatcherServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddDispatcher(this IServiceCollection services)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
// Collect all data sources from DI.
|
||||
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<DispatcherOptions>, ConfigureDispatcherOptions>());
|
||||
|
||||
// Allow global access to the list of endpoints.
|
||||
services.TryAddSingleton<CompositeEndpointDataSource>(s =>
|
||||
{
|
||||
var options = s.GetRequiredService<IOptions<DispatcherOptions>>();
|
||||
return new CompositeEndpointDataSource(options.Value.DataSources);
|
||||
});
|
||||
|
||||
//
|
||||
// Default matcher implementation
|
||||
//
|
||||
services.TryAddSingleton<MatcherFactory, TreeMatcherFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddDispatcher(this IServiceCollection services, Action<DispatcherOptions> configuration)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
services.AddDispatcher();
|
||||
if (configuration != null)
|
||||
{
|
||||
services.Configure<DispatcherOptions>(configuration);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Matchers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal sealed class DispatcherMiddleware
|
||||
{
|
||||
private readonly MatcherFactory _matcherFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IOptions<DispatcherOptions> _options;
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
private Task<Matcher> _initializationTask;
|
||||
|
||||
public DispatcherMiddleware(
|
||||
MatcherFactory matcherFactory,
|
||||
IOptions<DispatcherOptions> options,
|
||||
ILogger<DispatcherMiddleware> logger,
|
||||
RequestDelegate next)
|
||||
{
|
||||
if (matcherFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(matcherFactory));
|
||||
}
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
if (next == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(next));
|
||||
}
|
||||
|
||||
_matcherFactory = matcherFactory;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
var feature = new EndpointFeature();
|
||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
||||
|
||||
// There's an inherit race condition between waiting for init and accessing the matcher
|
||||
// this is OK because once `_matcher` is initialized, it will not be set to null again.
|
||||
var matcher = await InitializeAsync();
|
||||
|
||||
await matcher.MatchAsync(httpContext, feature);
|
||||
if (feature.Endpoint != null)
|
||||
{
|
||||
Log.EndpointMatched(_logger, feature);
|
||||
}
|
||||
|
||||
await _next(httpContext);
|
||||
}
|
||||
|
||||
// Initialization is async to avoid blocking threads while reflection and things
|
||||
// of that nature take place.
|
||||
//
|
||||
// We've seen cases where startup is very slow if we allow multiple threads to race
|
||||
// while initializing the set of endpoints/routes. Doing CPU intensive work is a
|
||||
// blocking operation if you have a low core count and enough work to do.
|
||||
private Task<Matcher> InitializeAsync()
|
||||
{
|
||||
if (_initializationTask != null)
|
||||
{
|
||||
return _initializationTask;
|
||||
}
|
||||
|
||||
var initializationTask = new TaskCompletionSource<Matcher>();
|
||||
if (Interlocked.CompareExchange<Task<Matcher>>(
|
||||
ref _initializationTask,
|
||||
initializationTask.Task,
|
||||
null) == null)
|
||||
{
|
||||
// This thread won the race, do the initialization.
|
||||
var dataSource = new CompositeEndpointDataSource(_options.Value.DataSources);
|
||||
var matcher = _matcherFactory.CreateMatcher(dataSource);
|
||||
initializationTask.SetResult(matcher);
|
||||
}
|
||||
|
||||
return _initializationTask;
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string, Exception> _matchSuccess = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(0, "MatchSuccess"),
|
||||
"Request matched endpoint '{EndpointName}'.");
|
||||
|
||||
public static void EndpointMatched(ILogger logger, EndpointFeature feature)
|
||||
{
|
||||
_matchSuccess(logger, feature.Endpoint.DisplayName, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class DispatcherOptions
|
||||
{
|
||||
public IList<EndpointDataSource> DataSources { get; } = new List<EndpointDataSource>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public abstract class EndpointDataSource
|
||||
{
|
||||
public abstract IChangeToken ChangeToken { get; }
|
||||
|
||||
public abstract IReadOnlyList<Endpoint> Endpoints { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public sealed class EndpointFeature : IEndpointFeature
|
||||
{
|
||||
public Endpoint Endpoint { get; set; }
|
||||
|
||||
public Func<RequestDelegate, RequestDelegate> Invoker { get; set; }
|
||||
|
||||
public RouteValueDictionary Values { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal sealed class EndpointMiddleware
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public EndpointMiddleware(ILogger<EndpointMiddleware> 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 feature = httpContext.Features.Get<IEndpointFeature>();
|
||||
if (feature.Invoker != null)
|
||||
{
|
||||
Log.ExecutingEndpoint(_logger, feature.Endpoint);
|
||||
|
||||
try
|
||||
{
|
||||
await feature.Invoker(_next)(httpContext);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.ExecutedEndpoint(_logger, feature.Endpoint);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await _next(httpContext);
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string, Exception> _executingEndpoint = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(0, "ExecutingEndpoint"),
|
||||
"Executing endpoint '{EndpointName}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _executedEndpoint = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(1, "ExecutedEndpoint"),
|
||||
"Executed endpoint '{EndpointName}'.");
|
||||
|
||||
public static void ExecutingEndpoint(ILogger logger, Endpoint endpoint)
|
||||
{
|
||||
_executingEndpoint(logger, endpoint.DisplayName, null);
|
||||
}
|
||||
|
||||
public static void ExecutedEndpoint(ILogger logger, Endpoint endpoint)
|
||||
{
|
||||
_executingEndpoint(logger, endpoint.DisplayName, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// 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.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Internal
|
||||
{
|
||||
internal class DataSourceDependantCache<T> where T : class
|
||||
{
|
||||
private readonly EndpointDataSource _dataSource;
|
||||
private readonly Func<IReadOnlyList<Endpoint>, T> _initializeCore;
|
||||
private readonly Func<T> _initializer;
|
||||
private readonly Action<object> _initializerWithState;
|
||||
|
||||
private object _lock;
|
||||
private bool _initialized;
|
||||
private T _value;
|
||||
|
||||
public DataSourceDependantCache(EndpointDataSource dataSource, Func<IReadOnlyList<Endpoint>, T> initialize)
|
||||
{
|
||||
if (dataSource == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
_dataSource = dataSource;
|
||||
_initializeCore = initialize;
|
||||
|
||||
_initializer = Initialize;
|
||||
_initializerWithState = (state) => Initialize();
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
public T Value => _value;
|
||||
|
||||
public T EnsureInitialized()
|
||||
{
|
||||
return LazyInitializer.EnsureInitialized<T>(ref _value, ref _initialized, ref _lock, _initializer);
|
||||
}
|
||||
|
||||
private T Initialize()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var changeToken = _dataSource.ChangeToken;
|
||||
_value = _initializeCore(_dataSource.Endpoints);
|
||||
|
||||
changeToken.RegisterChangeCallback(_initializerWithState, null);
|
||||
return _value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Runtime.Serialization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
/// <summary>
|
||||
/// An exception which indicates multiple matches in endpoint selection.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class AmbiguousMatchException : Exception
|
||||
{
|
||||
public AmbiguousMatchException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
protected AmbiguousMatchException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal abstract class MatcherFactory
|
||||
{
|
||||
public abstract Matcher CreateMatcher(EndpointDataSource dataSource);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for components that can select an <see cref="Endpoint"/> given the current request, as part
|
||||
/// of the execution of <see cref="DispatcherMiddleware"/>.
|
||||
/// </summary>
|
||||
internal abstract class Matcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to asynchronously select an <see cref="Endpoint"/> for the current request.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
|
||||
/// <param name="feature">
|
||||
/// The <see cref="IEndpointFeature"/> associated with the current request. The
|
||||
/// <see cref="IEndpointFeature"/> will be mutated to contain the result of the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> which represents the asynchronous completion of the operation.</returns>
|
||||
public abstract Task MatchAsync(HttpContext httpContext, IEndpointFeature feature);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal sealed class MatcherEndpoint : Endpoint
|
||||
{
|
||||
public MatcherEndpoint(
|
||||
Func<RequestDelegate, RequestDelegate> invoker,
|
||||
string template,
|
||||
object values,
|
||||
int order,
|
||||
EndpointMetadataCollection metadata,
|
||||
string displayName)
|
||||
: base(metadata, displayName)
|
||||
{
|
||||
if (invoker == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(invoker));
|
||||
}
|
||||
|
||||
if (template == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(template));
|
||||
}
|
||||
|
||||
Invoker = invoker;
|
||||
Template = template;
|
||||
Values = new RouteValueDictionary(values);
|
||||
}
|
||||
|
||||
public int Order { get; }
|
||||
public Func<RequestDelegate, RequestDelegate> Invoker { get; }
|
||||
public string Template { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, object> Values { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
// 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.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class TreeMatcher : Matcher
|
||||
{
|
||||
private readonly IInlineConstraintResolver _constraintFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly DataSourceDependantCache<UrlMatchingTree[]> _cache;
|
||||
|
||||
public TreeMatcher(
|
||||
IInlineConstraintResolver constraintFactory,
|
||||
ILogger logger,
|
||||
EndpointDataSource dataSource)
|
||||
{
|
||||
if (constraintFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraintFactory));
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
if (dataSource == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
_constraintFactory = constraintFactory;
|
||||
_logger = logger;
|
||||
_cache = new DataSourceDependantCache<UrlMatchingTree[]>(dataSource, CreateTrees);
|
||||
_cache.EnsureInitialized();
|
||||
}
|
||||
|
||||
public override async Task MatchAsync(HttpContext httpContext, IEndpointFeature feature)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpContext));
|
||||
}
|
||||
|
||||
if (feature == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(feature));
|
||||
}
|
||||
|
||||
var values = new RouteValueDictionary();
|
||||
feature.Values = values;
|
||||
|
||||
var cache = _cache.Value;
|
||||
for (var i = 0; i < cache.Length; i++)
|
||||
{
|
||||
var tree = cache[i];
|
||||
var tokenizer = new PathTokenizer(httpContext.Request.Path);
|
||||
|
||||
var treenumerator = new Treenumerator(tree.Root, tokenizer);
|
||||
|
||||
while (treenumerator.MoveNext())
|
||||
{
|
||||
var node = treenumerator.Current;
|
||||
foreach (var item in node.Matches)
|
||||
{
|
||||
var entry = item.Entry;
|
||||
var matcher = item.TemplateMatcher;
|
||||
|
||||
values.Clear();
|
||||
if (!matcher.TryMatch(httpContext.Request.Path, values))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.MatchedTemplate(_logger, httpContext, entry.RouteTemplate);
|
||||
|
||||
if (!MatchConstraints(httpContext, values, entry.Constraints))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await SelectEndpointAsync(httpContext, feature, (MatcherEndpoint[])entry.Tag);
|
||||
|
||||
if (feature.Endpoint != null)
|
||||
{
|
||||
if (feature.Endpoint is MatcherEndpoint endpoint)
|
||||
{
|
||||
foreach (var kvp in endpoint.Values)
|
||||
{
|
||||
if (!feature.Values.ContainsKey(kvp.Key))
|
||||
{
|
||||
feature.Values[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchConstraints(
|
||||
HttpContext httpContext,
|
||||
RouteValueDictionary values,
|
||||
IDictionary<string, IRouteConstraint> constraints)
|
||||
{
|
||||
if (constraints != null)
|
||||
{
|
||||
foreach (var kvp in constraints)
|
||||
{
|
||||
var constraint = kvp.Value;
|
||||
if (!constraint.Match(httpContext, null, kvp.Key, values, RouteDirection.IncomingRequest))
|
||||
{
|
||||
values.TryGetValue(kvp.Key, out var value);
|
||||
|
||||
Log.ConstraintFailed(_logger, value, kvp.Key, kvp.Value);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Task SelectEndpointAsync(HttpContext httpContext, IEndpointFeature feature, IReadOnlyList<MatcherEndpoint> endpoints)
|
||||
{
|
||||
switch (endpoints.Count)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
Log.MatchFailed(_logger, httpContext);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
case 1:
|
||||
{
|
||||
var endpoint = endpoints[0];
|
||||
Log.MatchSuccess(_logger, httpContext, endpoint);
|
||||
|
||||
feature.Endpoint = endpoint;
|
||||
feature.Invoker = endpoint.Invoker;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
Log.MatchAmbiguous(_logger, httpContext, endpoints);
|
||||
var message = Resources.FormatAmbiguousEndpoints(
|
||||
Environment.NewLine,
|
||||
string.Join(Environment.NewLine, endpoints.Select(a => a.DisplayName)));
|
||||
throw new AmbiguousMatchException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UrlMatchingTree[] CreateTrees(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
var groups = new Dictionary<Key, List<MatcherEndpoint>>();
|
||||
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var endpoint = endpoints[i] as MatcherEndpoint;
|
||||
if (endpoint == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var order = endpoint.Order;
|
||||
if (!groups.TryGetValue(new Key(order, endpoint.Template), out var group))
|
||||
{
|
||||
group = new List<MatcherEndpoint>();
|
||||
groups.Add(new Key(order, endpoint.Template), group);
|
||||
}
|
||||
|
||||
group.Add(endpoint);
|
||||
}
|
||||
|
||||
var entries = new List<InboundRouteEntry>();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var template = TemplateParser.Parse(group.Key.Template);
|
||||
var entryExists = entries.Any(item => item.RouteTemplate.TemplateText == template.TemplateText);
|
||||
if (!entryExists)
|
||||
{
|
||||
entries.Add(MapInbound(template, group.Value.ToArray(), group.Key.Order));
|
||||
}
|
||||
}
|
||||
|
||||
var trees = new List<UrlMatchingTree>();
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
|
||||
while (trees.Count <= entry.Order)
|
||||
{
|
||||
trees.Add(new UrlMatchingTree(entry.Order));
|
||||
}
|
||||
|
||||
var tree = trees[entry.Order];
|
||||
tree.AddEntry(entry);
|
||||
}
|
||||
|
||||
return trees.ToArray();
|
||||
}
|
||||
|
||||
private InboundRouteEntry MapInbound(RouteTemplate template, Endpoint[] endpoints, int order)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(template));
|
||||
}
|
||||
|
||||
var entry = new InboundRouteEntry()
|
||||
{
|
||||
Precedence = RoutePrecedence.ComputeInbound(template),
|
||||
RouteTemplate = template,
|
||||
Order = order,
|
||||
Tag = endpoints,
|
||||
};
|
||||
|
||||
var constraintBuilder = new RouteConstraintBuilder(_constraintFactory, template.TemplateText);
|
||||
foreach (var parameter in template.Parameters)
|
||||
{
|
||||
if (parameter.InlineConstraints != null)
|
||||
{
|
||||
if (parameter.IsOptional)
|
||||
{
|
||||
constraintBuilder.SetOptional(parameter.Name);
|
||||
}
|
||||
|
||||
foreach (var constraint in parameter.InlineConstraints)
|
||||
{
|
||||
constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry.Constraints = constraintBuilder.Build();
|
||||
|
||||
entry.Defaults = new RouteValueDictionary();
|
||||
foreach (var parameter in entry.RouteTemplate.Parameters)
|
||||
{
|
||||
if (parameter.DefaultValue != null)
|
||||
{
|
||||
entry.Defaults.Add(parameter.Name, parameter.DefaultValue);
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private struct Key : IEquatable<Key>
|
||||
{
|
||||
public readonly int Order;
|
||||
public readonly string Template;
|
||||
|
||||
public Key(int order, string routePattern)
|
||||
{
|
||||
Order = order;
|
||||
Template = routePattern;
|
||||
}
|
||||
|
||||
public bool Equals(Key other)
|
||||
{
|
||||
return Order == other.Order && string.Equals(Template, other.Template, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is Key ? Equals((Key)obj) : false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCodeCombiner();
|
||||
hash.Add(Order);
|
||||
hash.Add(Template, StringComparer.OrdinalIgnoreCase);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string, PathString, Exception> _matchSuccess = LoggerMessage.Define<string, PathString>(
|
||||
LogLevel.Debug,
|
||||
new EventId(0, "MatchSuccess"),
|
||||
"Request matched endpoint '{EndpointName}' for request path '{Path}'.");
|
||||
|
||||
private static readonly Action<ILogger, PathString, Exception> _matchFailed = LoggerMessage.Define<PathString>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1, "MatchFailed"),
|
||||
"No endpoints matched request path '{Path}'.");
|
||||
|
||||
private static readonly Action<ILogger, PathString, IEnumerable<string>, Exception> _matchAmbiguous = LoggerMessage.Define<PathString, IEnumerable<string>>(
|
||||
LogLevel.Error,
|
||||
new EventId(2, "MatchAmbiguous"),
|
||||
"Request matched multiple endpoints for request path '{Path}'. Matching endpoints: {AmbiguousEndpoints}");
|
||||
|
||||
private static readonly Action<ILogger, object, string, IRouteConstraint, Exception> _constraintFailed = LoggerMessage.Define<object, string, IRouteConstraint>(
|
||||
LogLevel.Debug,
|
||||
new EventId(3, "ContraintFailed"),
|
||||
"Route value '{RouteValue}' with key '{RouteKey}' did not match the constraint '{RouteConstraint}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, PathString, Exception> _matchedTemplate = LoggerMessage.Define<string, PathString>(
|
||||
LogLevel.Debug,
|
||||
new EventId(4, "MatchedTemplate"),
|
||||
"Request matched the route pattern '{RouteTemplate}' for request path '{Path}'.");
|
||||
|
||||
public static void MatchSuccess(ILogger logger, HttpContext httpContext, Endpoint endpoint)
|
||||
{
|
||||
_matchSuccess(logger, endpoint.DisplayName, httpContext.Request.Path, null);
|
||||
}
|
||||
|
||||
public static void MatchFailed(ILogger logger, HttpContext httpContext)
|
||||
{
|
||||
_matchFailed(logger, httpContext.Request.Path, null);
|
||||
}
|
||||
|
||||
public static void MatchAmbiguous(ILogger logger, HttpContext httpContext, IEnumerable<Endpoint> endpoints)
|
||||
{
|
||||
_matchAmbiguous(logger, httpContext.Request.Path, endpoints.Select(e => e.DisplayName), null);
|
||||
}
|
||||
|
||||
public static void ConstraintFailed(ILogger logger, object routeValue, string routeKey, IRouteConstraint routeConstraint)
|
||||
{
|
||||
_constraintFailed(logger, routeValue, routeKey, routeConstraint, null);
|
||||
}
|
||||
|
||||
public static void MatchedTemplate(ILogger logger, HttpContext httpContext, RouteTemplate template)
|
||||
{
|
||||
_matchedTemplate(logger, httpContext.Request.Path, template.TemplateText, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||
{
|
||||
internal class TreeMatcherFactory : MatcherFactory
|
||||
{
|
||||
private readonly IInlineConstraintResolver _constraintFactory;
|
||||
private readonly ILogger<TreeMatcher> _logger;
|
||||
|
||||
public TreeMatcherFactory(IInlineConstraintResolver constraintFactory, ILogger<TreeMatcher> logger)
|
||||
{
|
||||
if (constraintFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraintFactory));
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
_constraintFactory = constraintFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override Matcher CreateMatcher(EndpointDataSource dataSource)
|
||||
{
|
||||
if (dataSource == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
return new TreeMatcher(_constraintFactory, _logger, dataSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,3 +4,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Routing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("DispatcherSample.Web, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
|
|||
|
|
@ -402,6 +402,20 @@ namespace Microsoft.AspNetCore.Routing
|
|||
internal static string FormatTemplateRoute_Exception(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// The request matched multiple endpoints. Matches: {0}{0}{1}
|
||||
/// </summary>
|
||||
internal static string AmbiguousEndpoints
|
||||
{
|
||||
get => GetString("AmbiguousEndpoints");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The request matched multiple endpoints. Matches: {0}{0}{1}
|
||||
/// </summary>
|
||||
internal static string FormatAmbiguousEndpoints(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousEndpoints"), p0, p1);
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -201,4 +201,7 @@
|
|||
<data name="TemplateRoute_Exception" xml:space="preserve">
|
||||
<value>An error occurred while creating the route with name '{0}' and template '{1}'.</value>
|
||||
</data>
|
||||
<data name="AmbiguousEndpoints" xml:space="preserve">
|
||||
<value>The request matched multiple endpoints. Matches: {0}{0}{1}</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -52,5 +52,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
/// Gets or sets the <see cref="RouteTemplate"/>.
|
||||
/// </summary>
|
||||
public RouteTemplate RouteTemplate { get; set; }
|
||||
|
||||
public object Tag { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
trees.Add(entry.Order, tree);
|
||||
}
|
||||
|
||||
AddEntryToTree(tree, entry);
|
||||
tree.AddEntry(entry);
|
||||
}
|
||||
|
||||
return new TreeRouter(
|
||||
|
|
@ -288,167 +288,5 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
InboundEntries.Clear();
|
||||
OutboundEntries.Clear();
|
||||
}
|
||||
|
||||
private void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry)
|
||||
{
|
||||
// The url matching tree represents all the routes asociated with a given
|
||||
// order. Each node in the tree represents all the different categories
|
||||
// a segment can have for which there is a defined inbound route entry.
|
||||
// Each node contains a set of Matches that indicate all the routes for which
|
||||
// a URL is a potential match. This list contains the routes with the same
|
||||
// number of segments and the routes with the same number of segments plus an
|
||||
// additional catch all parameter (as it can be empty).
|
||||
// For example, for a set of routes like:
|
||||
// 'Customer/Index/{id}'
|
||||
// '{Controller}/{Action}/{*parameters}'
|
||||
//
|
||||
// The route tree will look like:
|
||||
// Root ->
|
||||
// Literals: Customer ->
|
||||
// Literals: Index ->
|
||||
// Parameters: {id}
|
||||
// Matches: 'Customer/Index/{id}'
|
||||
// Parameters: {Controller} ->
|
||||
// Parameters: {Action} ->
|
||||
// Matches: '{Controller}/{Action}/{*parameters}'
|
||||
// CatchAlls: {*parameters}
|
||||
// Matches: '{Controller}/{Action}/{*parameters}'
|
||||
//
|
||||
// When the tree router tries to match a route, it iterates the list of url matching trees
|
||||
// in ascending order. For each tree it traverses each node starting from the root in the
|
||||
// following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls.
|
||||
// When it gets to a node of the same length as the route its trying to match, it simply looks at the list of
|
||||
// candidates (which is in precence order) and tries to match the url against it.
|
||||
//
|
||||
|
||||
var current = tree.Root;
|
||||
var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults);
|
||||
|
||||
for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++)
|
||||
{
|
||||
var segment = entry.RouteTemplate.Segments[i];
|
||||
if (!segment.IsSimple)
|
||||
{
|
||||
// Treat complex segments as a constrained parameter
|
||||
if (current.ConstrainedParameters == null)
|
||||
{
|
||||
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
|
||||
}
|
||||
|
||||
current = current.ConstrainedParameters;
|
||||
continue;
|
||||
}
|
||||
|
||||
Debug.Assert(segment.Parts.Count == 1);
|
||||
var part = segment.Parts[0];
|
||||
if (part.IsLiteral)
|
||||
{
|
||||
UrlMatchingNode next;
|
||||
if (!current.Literals.TryGetValue(part.Text, out next))
|
||||
{
|
||||
next = new UrlMatchingNode(length: i + 1);
|
||||
current.Literals.Add(part.Text, next);
|
||||
}
|
||||
|
||||
current = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We accept templates that have intermediate optional values, but we ignore
|
||||
// those values for route matching. For that reason, we need to add the entry
|
||||
// to the list of matches, only if the remaining segments are optional. For example:
|
||||
// /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id}
|
||||
// for the purposes of route matching.
|
||||
if (part.IsParameter &&
|
||||
RemainingSegmentsAreOptional(entry.RouteTemplate.Segments, i))
|
||||
{
|
||||
current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
|
||||
}
|
||||
|
||||
if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll)
|
||||
{
|
||||
if (current.ConstrainedParameters == null)
|
||||
{
|
||||
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
|
||||
}
|
||||
|
||||
current = current.ConstrainedParameters;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.IsParameter && !part.IsCatchAll)
|
||||
{
|
||||
if (current.Parameters == null)
|
||||
{
|
||||
current.Parameters = new UrlMatchingNode(length: i + 1);
|
||||
}
|
||||
|
||||
current = current.Parameters;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll)
|
||||
{
|
||||
if (current.ConstrainedCatchAlls == null)
|
||||
{
|
||||
current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true };
|
||||
}
|
||||
|
||||
current = current.ConstrainedCatchAlls;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.IsParameter && part.IsCatchAll)
|
||||
{
|
||||
if (current.CatchAlls == null)
|
||||
{
|
||||
current.CatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true };
|
||||
}
|
||||
|
||||
current = current.CatchAlls;
|
||||
continue;
|
||||
}
|
||||
|
||||
Debug.Fail("We shouldn't get here.");
|
||||
}
|
||||
|
||||
current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
|
||||
current.Matches.Sort((x, y) =>
|
||||
{
|
||||
var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence);
|
||||
return result == 0 ? x.Entry.RouteTemplate.TemplateText.CompareTo(y.Entry.RouteTemplate.TemplateText) : result;
|
||||
});
|
||||
}
|
||||
|
||||
private static bool RemainingSegmentsAreOptional(IList<TemplateSegment> segments, int currentParameterIndex)
|
||||
{
|
||||
for (var i = currentParameterIndex; i < segments.Count; i++)
|
||||
{
|
||||
if (!segments[i].IsSimple)
|
||||
{
|
||||
// /{complex}-{segment}
|
||||
return false;
|
||||
}
|
||||
|
||||
var part = segments[i].Parts[0];
|
||||
if (!part.IsParameter)
|
||||
{
|
||||
// /literal
|
||||
return false;
|
||||
}
|
||||
|
||||
var isOptionlCatchAllOrHasDefaultValue = part.IsOptional ||
|
||||
part.IsCatchAll ||
|
||||
part.DefaultValue != null;
|
||||
|
||||
if (!isOptionlCatchAllOrHasDefaultValue)
|
||||
{
|
||||
// /{parameter}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
|
|
@ -179,7 +177,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
var tokenizer = new PathTokenizer(context.HttpContext.Request.Path);
|
||||
var root = tree.Root;
|
||||
|
||||
var treeEnumerator = new TreeEnumerator(root, tokenizer);
|
||||
var treeEnumerator = new Treenumerator(root, tokenizer);
|
||||
|
||||
// Create a snapshot before processing the route. We'll restore this snapshot before running each
|
||||
// to restore the state. This is likely an "empty" snapshot, which doesn't allocate.
|
||||
|
|
@ -233,108 +231,6 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
}
|
||||
}
|
||||
|
||||
private struct TreeEnumerator : IEnumerator<UrlMatchingNode>
|
||||
{
|
||||
private readonly Stack<UrlMatchingNode> _stack;
|
||||
private readonly PathTokenizer _tokenizer;
|
||||
|
||||
public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer)
|
||||
{
|
||||
_stack = new Stack<UrlMatchingNode>();
|
||||
_tokenizer = tokenizer;
|
||||
Current = null;
|
||||
|
||||
_stack.Push(root);
|
||||
}
|
||||
|
||||
public UrlMatchingNode Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_stack == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
while (_stack.Count > 0)
|
||||
{
|
||||
var next = _stack.Pop();
|
||||
|
||||
// In case of wild card segment, the request path segment length can be greater
|
||||
// Example:
|
||||
// Template: a/{*path}
|
||||
// Request Url: a/b/c/d
|
||||
if (next.IsCatchAll && next.Matches.Count > 0)
|
||||
{
|
||||
Current = next;
|
||||
return true;
|
||||
}
|
||||
// Next template has the same length as the url we are trying to match
|
||||
// The only possible matching segments are either our current matches or
|
||||
// any catch-all segment after this segment in which the catch all is empty.
|
||||
else if (next.Depth == _tokenizer.Count)
|
||||
{
|
||||
if (next.Matches.Count > 0)
|
||||
{
|
||||
Current = next;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can stop looking as any other child node from this node will be
|
||||
// either a literal, a constrained parameter or a parameter.
|
||||
// (Catch alls and constrained catch alls will show up as candidate matches).
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (next.CatchAlls != null)
|
||||
{
|
||||
_stack.Push(next.CatchAlls);
|
||||
}
|
||||
|
||||
if (next.ConstrainedCatchAlls != null)
|
||||
{
|
||||
_stack.Push(next.ConstrainedCatchAlls);
|
||||
}
|
||||
|
||||
if (next.Parameters != null)
|
||||
{
|
||||
_stack.Push(next.Parameters);
|
||||
}
|
||||
|
||||
if (next.ConstrainedParameters != null)
|
||||
{
|
||||
_stack.Push(next.ConstrainedParameters);
|
||||
}
|
||||
|
||||
if (next.Literals.Count > 0)
|
||||
{
|
||||
UrlMatchingNode node;
|
||||
Debug.Assert(next.Depth < _tokenizer.Count);
|
||||
if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out node))
|
||||
{
|
||||
_stack.Push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_stack.Clear();
|
||||
Current = null;
|
||||
}
|
||||
}
|
||||
|
||||
private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context)
|
||||
{
|
||||
OutboundMatch match;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Tree
|
||||
{
|
||||
internal struct Treenumerator : IEnumerator<UrlMatchingNode>
|
||||
{
|
||||
private readonly Stack<UrlMatchingNode> _stack;
|
||||
private readonly PathTokenizer _tokenizer;
|
||||
|
||||
public Treenumerator(UrlMatchingNode root, PathTokenizer tokenizer)
|
||||
{
|
||||
_stack = new Stack<UrlMatchingNode>();
|
||||
_tokenizer = tokenizer;
|
||||
Current = null;
|
||||
|
||||
_stack.Push(root);
|
||||
}
|
||||
|
||||
public UrlMatchingNode Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_stack == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
while (_stack.Count > 0)
|
||||
{
|
||||
var next = _stack.Pop();
|
||||
|
||||
// In case of wild card segment, the request path segment length can be greater
|
||||
// Example:
|
||||
// Template: a/{*path}
|
||||
// Request Url: a/b/c/d
|
||||
if (next.IsCatchAll && next.Matches.Count > 0)
|
||||
{
|
||||
Current = next;
|
||||
return true;
|
||||
}
|
||||
// Next template has the same length as the url we are trying to match
|
||||
// The only possible matching segments are either our current matches or
|
||||
// any catch-all segment after this segment in which the catch all is empty.
|
||||
else if (next.Depth == _tokenizer.Count)
|
||||
{
|
||||
if (next.Matches.Count > 0)
|
||||
{
|
||||
Current = next;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can stop looking as any other child node from this node will be
|
||||
// either a literal, a constrained parameter or a parameter.
|
||||
// (Catch alls and constrained catch alls will show up as candidate matches).
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (next.CatchAlls != null)
|
||||
{
|
||||
_stack.Push(next.CatchAlls);
|
||||
}
|
||||
|
||||
if (next.ConstrainedCatchAlls != null)
|
||||
{
|
||||
_stack.Push(next.ConstrainedCatchAlls);
|
||||
}
|
||||
|
||||
if (next.Parameters != null)
|
||||
{
|
||||
_stack.Push(next.Parameters);
|
||||
}
|
||||
|
||||
if (next.ConstrainedParameters != null)
|
||||
{
|
||||
_stack.Push(next.ConstrainedParameters);
|
||||
}
|
||||
|
||||
if (next.Literals.Count > 0)
|
||||
{
|
||||
UrlMatchingNode node;
|
||||
Debug.Assert(next.Depth < _tokenizer.Count);
|
||||
if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out node))
|
||||
{
|
||||
_stack.Push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_stack.Clear();
|
||||
Current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +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 System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Tree
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -26,5 +31,167 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
|||
/// Gets the root of the <see cref="UrlMatchingTree"/>.
|
||||
/// </summary>
|
||||
public UrlMatchingNode Root { get; } = new UrlMatchingNode(length: 0);
|
||||
|
||||
internal void AddEntry(InboundRouteEntry entry)
|
||||
{
|
||||
// The url matching tree represents all the routes asociated with a given
|
||||
// order. Each node in the tree represents all the different categories
|
||||
// a segment can have for which there is a defined inbound route entry.
|
||||
// Each node contains a set of Matches that indicate all the routes for which
|
||||
// a URL is a potential match. This list contains the routes with the same
|
||||
// number of segments and the routes with the same number of segments plus an
|
||||
// additional catch all parameter (as it can be empty).
|
||||
// For example, for a set of routes like:
|
||||
// 'Customer/Index/{id}'
|
||||
// '{Controller}/{Action}/{*parameters}'
|
||||
//
|
||||
// The route tree will look like:
|
||||
// Root ->
|
||||
// Literals: Customer ->
|
||||
// Literals: Index ->
|
||||
// Parameters: {id}
|
||||
// Matches: 'Customer/Index/{id}'
|
||||
// Parameters: {Controller} ->
|
||||
// Parameters: {Action} ->
|
||||
// Matches: '{Controller}/{Action}/{*parameters}'
|
||||
// CatchAlls: {*parameters}
|
||||
// Matches: '{Controller}/{Action}/{*parameters}'
|
||||
//
|
||||
// When the tree router tries to match a route, it iterates the list of url matching trees
|
||||
// in ascending order. For each tree it traverses each node starting from the root in the
|
||||
// following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls.
|
||||
// When it gets to a node of the same length as the route its trying to match, it simply looks at the list of
|
||||
// candidates (which is in precence order) and tries to match the url against it.
|
||||
//
|
||||
|
||||
var current = Root;
|
||||
var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults);
|
||||
|
||||
for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++)
|
||||
{
|
||||
var segment = entry.RouteTemplate.Segments[i];
|
||||
if (!segment.IsSimple)
|
||||
{
|
||||
// Treat complex segments as a constrained parameter
|
||||
if (current.ConstrainedParameters == null)
|
||||
{
|
||||
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
|
||||
}
|
||||
|
||||
current = current.ConstrainedParameters;
|
||||
continue;
|
||||
}
|
||||
|
||||
Debug.Assert(segment.Parts.Count == 1);
|
||||
var part = segment.Parts[0];
|
||||
if (part.IsLiteral)
|
||||
{
|
||||
UrlMatchingNode next;
|
||||
if (!current.Literals.TryGetValue(part.Text, out next))
|
||||
{
|
||||
next = new UrlMatchingNode(length: i + 1);
|
||||
current.Literals.Add(part.Text, next);
|
||||
}
|
||||
|
||||
current = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We accept templates that have intermediate optional values, but we ignore
|
||||
// those values for route matching. For that reason, we need to add the entry
|
||||
// to the list of matches, only if the remaining segments are optional. For example:
|
||||
// /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id}
|
||||
// for the purposes of route matching.
|
||||
if (part.IsParameter &&
|
||||
RemainingSegmentsAreOptional(entry.RouteTemplate.Segments, i))
|
||||
{
|
||||
current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
|
||||
}
|
||||
|
||||
if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll)
|
||||
{
|
||||
if (current.ConstrainedParameters == null)
|
||||
{
|
||||
current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
|
||||
}
|
||||
|
||||
current = current.ConstrainedParameters;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.IsParameter && !part.IsCatchAll)
|
||||
{
|
||||
if (current.Parameters == null)
|
||||
{
|
||||
current.Parameters = new UrlMatchingNode(length: i + 1);
|
||||
}
|
||||
|
||||
current = current.Parameters;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll)
|
||||
{
|
||||
if (current.ConstrainedCatchAlls == null)
|
||||
{
|
||||
current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true };
|
||||
}
|
||||
|
||||
current = current.ConstrainedCatchAlls;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.IsParameter && part.IsCatchAll)
|
||||
{
|
||||
if (current.CatchAlls == null)
|
||||
{
|
||||
current.CatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true };
|
||||
}
|
||||
|
||||
current = current.CatchAlls;
|
||||
continue;
|
||||
}
|
||||
|
||||
Debug.Fail("We shouldn't get here.");
|
||||
}
|
||||
|
||||
current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
|
||||
current.Matches.Sort((x, y) =>
|
||||
{
|
||||
var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence);
|
||||
return result == 0 ? x.Entry.RouteTemplate.TemplateText.CompareTo(y.Entry.RouteTemplate.TemplateText) : result;
|
||||
});
|
||||
}
|
||||
|
||||
private static bool RemainingSegmentsAreOptional(IList<TemplateSegment> segments, int currentParameterIndex)
|
||||
{
|
||||
for (var i = currentParameterIndex; i < segments.Count; i++)
|
||||
{
|
||||
if (!segments[i].IsSimple)
|
||||
{
|
||||
// /{complex}-{segment}
|
||||
return false;
|
||||
}
|
||||
|
||||
var part = segments[i].Parts[0];
|
||||
if (!part.IsParameter)
|
||||
{
|
||||
// /literal
|
||||
return false;
|
||||
}
|
||||
|
||||
var isOptionlCatchAllOrHasDefaultValue = part.IsOptional ||
|
||||
part.IsCatchAll ||
|
||||
part.DefaultValue != null;
|
||||
|
||||
if (!isOptionlCatchAllOrHasDefaultValue)
|
||||
{
|
||||
// /{parameter}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue