diff --git a/src/Routing/Directory.Build.targets b/src/Routing/Directory.Build.targets index 78626b773e..7e3f8df92e 100644 --- a/src/Routing/Directory.Build.targets +++ b/src/Routing/Directory.Build.targets @@ -1,10 +1,6 @@ - $(MicrosoftNETCoreApp20PackageVersion) - $(MicrosoftNETCoreApp21PackageVersion) - $(MicrosoftNETCoreApp22PackageVersion) + $(MicrosoftNETCoreAppPackageVersion) $(NETStandardLibrary20PackageVersion) - - 99.9 diff --git a/src/Routing/README.md b/src/Routing/README.md index b3202c355a..1f66dfdd7f 100644 --- a/src/Routing/README.md +++ b/src/Routing/README.md @@ -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 . 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. diff --git a/src/Routing/benchmarkapps/Benchmarks/Benchmarks.csproj b/src/Routing/benchmarkapps/Benchmarks/Benchmarks.csproj index 3c5753cc64..0db37a19dd 100644 --- a/src/Routing/benchmarkapps/Benchmarks/Benchmarks.csproj +++ b/src/Routing/benchmarkapps/Benchmarks/Benchmarks.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2 + netcoreapp3.0 $(BenchmarksTargetFramework) true diff --git a/src/Routing/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs b/src/Routing/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs index 2814badc32..f6062f0531 100644 --- a/src/Routing/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs +++ b/src/Routing/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs @@ -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)); - } - - public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app) - { - app.UseEndpointRouting(); + builder.DataSources.Add(endpointDataSource); + }); app.UseEndpoint(); } } -} \ No newline at end of file +} diff --git a/src/Routing/benchmarkapps/Benchmarks/benchmarks.json b/src/Routing/benchmarkapps/Benchmarks/benchmarks.json index 1779a80c8c..d69ee7afce 100644 --- a/src/Routing/benchmarkapps/Benchmarks/benchmarks.json +++ b/src/Routing/benchmarkapps/Benchmarks/benchmarks.json @@ -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 diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj index 625a26262a..ebe8f50b50 100644 --- a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj +++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2 + netcoreapp3.0 Exe true true diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RouteValueDictionaryBenchmark.cs b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RouteValueDictionaryBenchmark.cs deleted file mode 100644 index 522d8d3ed4..0000000000 --- a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RouteValueDictionaryBenchmark.cs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/src/Routing/build/dependencies.props b/src/Routing/build/dependencies.props index 3c93a7647d..15eca73927 100644 --- a/src/Routing/build/dependencies.props +++ b/src/Routing/build/dependencies.props @@ -4,43 +4,39 @@ 0.10.13 - 2.2.0-preview2-20181011.10 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.2.0-preview3-35496 - 2.0.9 - 2.1.3 - 2.2.0-preview3-27008-03 + 3.0.0-build-20181114.5 + 3.0.0-alpha1-10742 + 3.0.0-preview-181113-11 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview1-26907-05 15.6.1 4.10.0 2.0.3 11.0.2 - 4.3.0 - 4.3.0 0.10.0 2.3.1 2.4.0 diff --git a/src/Routing/build/repo.props b/src/Routing/build/repo.props index 9747864f3e..157a311580 100644 --- a/src/Routing/build/repo.props +++ b/src/Routing/build/repo.props @@ -4,14 +4,11 @@ Internal.AspNetCore.Universe.Lineup - 2.2.0-* https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json - - - + diff --git a/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs b/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs new file mode 100644 index 0000000000..1566f8d127 --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs @@ -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(); + } + } +} diff --git a/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMetadata.cs b/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMetadata.cs new file mode 100644 index 0000000000..0e492ddc24 --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMetadata.cs @@ -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 roles) + { + if (roles == null) + { + throw new ArgumentNullException(nameof(roles)); + } + + Roles = roles.ToArray(); + } + + public IReadOnlyList Roles { get; } + } +} diff --git a/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs b/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs new file mode 100644 index 0000000000..594b18c43b --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs @@ -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 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()?.Endpoint; + if (endpoint != null) + { + var metadata = endpoint.Metadata.GetMetadata(); + // 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); + } + } +} diff --git a/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/EndpointConventionBuilderExtensions .cs b/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/EndpointConventionBuilderExtensions .cs new file mode 100644 index 0000000000..51a5c9c469 --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/AuthorizationMiddleware/EndpointConventionBuilderExtensions .cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/Routing/samples/RoutingSandbox/HelloExtension/EndpointRouteBuilderExtensions.cs b/src/Routing/samples/RoutingSandbox/HelloExtension/EndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..fc26b50bf8 --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/HelloExtension/EndpointRouteBuilderExtensions.cs @@ -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); + } + } +} diff --git a/src/Routing/samples/RoutingSandbox/HelloExtension/HelloAppBuilderExtensions.cs b/src/Routing/samples/RoutingSandbox/HelloExtension/HelloAppBuilderExtensions.cs new file mode 100644 index 0000000000..e1a587d5ca --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/HelloExtension/HelloAppBuilderExtensions.cs @@ -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(Options.Create(new HelloOptions + { + Greeter = greeter + })); + } + } +} diff --git a/src/Routing/samples/RoutingSandbox/HelloExtension/HelloMiddleware.cs b/src/Routing/samples/RoutingSandbox/HelloExtension/HelloMiddleware.cs new file mode 100644 index 0000000000..6a4587e50c --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/HelloExtension/HelloMiddleware.cs @@ -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) + { + _next = next; + _helloOptions = helloOptions.Value; + + var payload = new List(); + 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); + } + } +} diff --git a/src/Routing/samples/RoutingSandbox/HelloExtension/HelloOptions.cs b/src/Routing/samples/RoutingSandbox/HelloExtension/HelloOptions.cs new file mode 100644 index 0000000000..49f8b5c5df --- /dev/null +++ b/src/Routing/samples/RoutingSandbox/HelloExtension/HelloOptions.cs @@ -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; } + } +} diff --git a/src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj b/src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj index 5f01b1bb1e..eaf327a420 100644 --- a/src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj +++ b/src/Routing/samples/RoutingSandbox/RoutingSandbox.csproj @@ -1,8 +1,7 @@ - + - netcoreapp2.2 - $(TargetFrameworks);net461 + netcoreapp3.0 diff --git a/src/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs b/src/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs index bfb345cba9..fdeabd02b9 100644 --- a/src/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs +++ b/src/Routing/samples/RoutingSandbox/UseEndpointRoutingStartup.cs @@ -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(); - var dataSource = httpContext.RequestServices.GetRequiredService(); - graphWriter.Write(dataSource, writer); - } - - return Task.CompletedTask; - }, - RoutePatternFactory.Parse("/graph"), - 0, - new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })), - "DFA Graph"), - }); - - services.TryAddEnumerable(ServiceDescriptor.Singleton(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(); + + var sb = new StringBuilder(); + sb.AppendLine("Endpoints:"); + foreach (var endpoint in dataSource.Endpoints.OfType().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(); + var dataSource = httpContext.RequestServices.GetRequiredService(); + graphWriter.Write(dataSource, writer); + } + + return Task.CompletedTask; + }); + }); app.UseStaticFiles(); - - // Imagine some more stuff here... + + app.UseAuthorization(); app.UseEndpoint(); } diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Endpoint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Endpoint.cs deleted file mode 100644 index 16202b45f5..0000000000 --- a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Endpoint.cs +++ /dev/null @@ -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 -{ - /// - /// Respresents a logical endpoint in an application. - /// - public class Endpoint - { - /// - /// Creates a new instance of . - /// - /// The delegate used to process requests for the endpoint. - /// - /// The endpoint . May be null. - /// - /// - /// The informational display name of the endpoint. May be null. - /// - public Endpoint( - RequestDelegate requestDelegate, - EndpointMetadataCollection metadata, - string displayName) - { - // All are allowed to be null - RequestDelegate = requestDelegate; - Metadata = metadata ?? EndpointMetadataCollection.Empty; - DisplayName = displayName; - } - - /// - /// Gets the informational display name of this endpoint. - /// - public string DisplayName { get; } - - /// - /// Gets the collection of metadata associated with this endpoint. - /// - public EndpointMetadataCollection Metadata { get; } - - /// - /// Gets the delegate used to process requests for the endpoint. - /// - public RequestDelegate RequestDelegate { get; } - - public override string ToString() => DisplayName ?? base.ToString(); - } -} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/EndpointMetadataCollection.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/EndpointMetadataCollection.cs deleted file mode 100644 index 137423a886..0000000000 --- a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/EndpointMetadataCollection.cs +++ /dev/null @@ -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 -{ - /// - /// A collection of arbitrary metadata associated with an endpoint. - /// - /// - /// 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. - /// - public sealed class EndpointMetadataCollection : IReadOnlyList - { - /// - /// An empty . - /// - public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty()); - - private readonly object[] _items; - private readonly ConcurrentDictionary _cache; - - /// - /// Creates a new instance of . - /// - /// The metadata items. - public EndpointMetadataCollection(IEnumerable items) - { - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } - - _items = items.ToArray(); - _cache = new ConcurrentDictionary(); - } - - /// - /// Creates a new instance of . - /// - /// The metadata items. - public EndpointMetadataCollection(params object[] items) - : this((IEnumerable)items) - { - } - - /// - /// Gets the item at . - /// - /// The index of the item to retrieve. - /// The item at . - public object this[int index] => _items[index]; - - /// - /// Gets the count of metadata items. - /// - public int Count => _items.Length; - - /// - /// Gets the most significant metadata item of type . - /// - /// The type of metadata to retrieve. - /// - /// The most significant metadata of type or null. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T GetMetadata() 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(); - } - - private T GetMetadataSlow() where T : class - { - var array = GetOrderedMetadataSlow(); - var length = array.Length; - return length > 0 ? array[length - 1] : default; - } - - /// - /// Gets the metadata items of type in ascending - /// order of precedence. - /// - /// The type of metadata. - /// A sequence of metadata items of . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IEnumerable GetOrderedMetadata() where T : class - { - if (_cache.TryGetValue(typeof(T), out var result)) - { - return (T[])result; - } - - return GetOrderedMetadataSlow(); - } - - private T[] GetOrderedMetadataSlow() where T : class - { - var items = new List(); - 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; - } - - /// - /// Gets an of all metadata items. - /// - /// An of all metadata items. - public Enumerator GetEnumerator() => new Enumerator(this); - - /// - /// Gets an of all metadata items. - /// - /// An of all metadata items. - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// - /// Gets an of all metadata items. - /// - /// An of all metadata items. - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// - /// Enumerates the elements of an . - /// - public struct Enumerator : IEnumerator - { - // 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; - } - - /// - /// Gets the element at the current position of the enumerator - /// - public object Current { get; private set; } - - /// - /// Releases all resources used by the . - /// - public void Dispose() - { - } - - /// - /// Advances the enumerator to the next element of the . - /// - /// - /// true if the enumerator was successfully advanced to the next element; - /// false if the enumerator has passed the end of the collection. - /// - public bool MoveNext() - { - if (_index < _items.Length) - { - Current = _items[_index++]; - return true; - } - - Current = null; - return false; - } - - /// - /// Sets the enumerator to its initial position, which is before the first element in the collection. - /// - public void Reset() - { - _index = 0; - Current = null; - } - } - } -} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IEndpointFeature.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IEndpointFeature.cs deleted file mode 100644 index ff0762bb20..0000000000 --- a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IEndpointFeature.cs +++ /dev/null @@ -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 -{ - /// - /// A feature interface for endpoint routing. Use - /// to access an instance associated with the current request. - /// - public interface IEndpointFeature - { - /// - /// Gets or sets the selected for the current - /// request. - /// - Endpoint Endpoint { get; set; } - } -} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteValuesFeature.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteValuesFeature.cs deleted file mode 100644 index 3ab74b6bc7..0000000000 --- a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteValuesFeature.cs +++ /dev/null @@ -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 - { - /// - /// Gets or sets the associated with the currrent - /// request. - /// - RouteValueDictionary RouteValues { get; set; } - } -} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj index 633a49d503..559550f7ed 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj @@ -5,7 +5,7 @@ Commonly used types: Microsoft.AspNetCore.Routing.IRouter Microsoft.AspNetCore.Routing.RouteData - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;routing diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs index fc39ed7ef2..fc9a32648f 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs deleted file mode 100644 index 8fdf6b8715..0000000000 --- a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs +++ /dev/null @@ -1,58 +0,0 @@ -// -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); - - /// - /// An element with the key '{0}' already exists in the {1}. - /// - internal static string RouteValueDictionary_DuplicateKey - { - get => GetString("RouteValueDictionary_DuplicateKey"); - } - - /// - /// An element with the key '{0}' already exists in the {1}. - /// - internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1) - => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1); - - /// - /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. - /// - internal static string RouteValueDictionary_DuplicatePropertyName - { - get => GetString("RouteValueDictionary_DuplicatePropertyName"); - } - - /// - /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. - /// - 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; - } - } -} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx deleted file mode 100644 index 40e651af14..0000000000 --- a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - An element with the key '{0}' already exists in the {1}. - - - The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. - - \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs deleted file mode 100644 index 55b2438e9e..0000000000 --- a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs +++ /dev/null @@ -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 -{ - /// - /// An type for route values. - /// - public class RouteValueDictionary : IDictionary, IReadOnlyDictionary - { - // 4 is a good default capacity here because that leaves enough space for area/controller/action/id - private const int DefaultCapacity = 4; - - internal KeyValuePair[] _arrayStorage; - internal PropertyStorage _propertyStorage; - private int _count; - - /// - /// Creates a new instance of from the provided array. - /// The new instance will take ownership of the array, and may mutate it. - /// - /// The items array. - /// A new . - public static RouteValueDictionary FromArray(KeyValuePair[] 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, - }; - } - - /// - /// Creates an empty . - /// - public RouteValueDictionary() - { - _arrayStorage = Array.Empty>(); - } - - /// - /// Creates a initialized with the specified . - /// - /// An object to initialize the dictionary. The value can be of type - /// or - /// or an object with public properties as key-value pairs. - /// - /// - /// If the value is a dictionary or other of , - /// 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. - /// - 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[other.Length]; - if (dictionary._count != 0) - { - Array.Copy(other, 0, storage, 0, dictionary._count); - } - - _arrayStorage = storage; - _count = dictionary._count; - return; - } - - if (values is IEnumerable> keyValueEnumerable) - { - foreach (var kvp in keyValueEnumerable) - { - Add(kvp.Key, kvp.Value); - } - - return; - } - - if (values is IEnumerable> 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; - } - } - - /// - 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(key, value); - } - else - { - _arrayStorage[index] = new KeyValuePair(key, value); - } - } - } - - /// - /// Gets the comparer for this dictionary. - /// - /// - /// This will always be a reference to - /// - public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; - - /// - public int Count => _count; - - /// - bool ICollection>.IsReadOnly => false; - - /// - public ICollection 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 IReadOnlyDictionary.Keys => Keys; - - /// - public ICollection 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 IReadOnlyDictionary.Values => Values; - - /// - void ICollection>.Add(KeyValuePair item) - { - Add(item.Key, item.Value); - } - - /// - 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(key, value); - _count++; - } - - /// - public void Clear() - { - if (_count == 0) - { - return; - } - - if (_propertyStorage != null) - { - _arrayStorage = Array.Empty>(); - _propertyStorage = null; - _count = 0; - return; - } - - Array.Clear(_arrayStorage, 0, _count); - _count = 0; - } - - /// - bool ICollection>.Contains(KeyValuePair item) - { - return TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); - } - - /// - public bool ContainsKey(string key) - { - if (key == null) - { - ThrowArgumentNullExceptionForKey(); - } - - return TryGetValue(key, out var _); - } - - /// - void ICollection>.CopyTo( - KeyValuePair[] 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); - } - - /// - public Enumerator GetEnumerator() - { - return new Enumerator(this); - } - - /// - IEnumerator> IEnumerable>.GetEnumerator() - { - return GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - bool ICollection>.Remove(KeyValuePair item) - { - if (Count == 0) - { - return false; - } - - EnsureCapacity(Count); - - var index = FindIndex(item.Key); - var array = _arrayStorage; - if (index >= 0 && EqualityComparer.Default.Equals(array[index].Value, item.Value)) - { - Array.Copy(array, index + 1, array, index, _count - index); - _count--; - array[_count] = default; - return true; - } - - return false; - } - - /// - 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; - } - - /// - /// Attempts to remove and return the value that has the specified key from the . - /// - /// The key of the element to remove and return. - /// When this method returns, contains the object removed from the , or null if key does not exist. - /// - /// true if the object was removed successfully; otherwise, false. - /// - 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; - } - - - /// - /// Attempts to the add the provided and to the dictionary. - /// - /// The key. - /// The value. - /// Returns true if the value was added. Returns false if the key was already present. - 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(key, value); - _count++; - return true; - } - - /// - 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[capacity]; - - for (var i = 0; i < storage.Properties.Length; i++) - { - var property = storage.Properties[i]; - array[i] = new KeyValuePair(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[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> - { - 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 Current { get; private set; } - - object IEnumerator.Current => Current; - - public void Dispose() - { - } - - // Similar to the design of List.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(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 _propertyCache = new ConcurrentDictionary(); - - 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(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); - } - } - } - } -} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRouteBuilderExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..0babbfc0e6 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRouteBuilderExtensions.cs @@ -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 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 httpMethods, + params object[] metadata) + { + if (httpMethods == null) + { + throw new ArgumentNullException(nameof(httpMethods)); + } + + var resolvedMetadata = new List(); + 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().FirstOrDefault(); + + if (modelEndpointDataSource == null) + { + modelEndpointDataSource = new ModelEndpointDataSource(); + builder.DataSources.Add(modelEndpointDataSource); + } + + return modelEndpointDataSource.AddEndpointModel(routeEndpointModel); + } + #endregion + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/EndpointRoutingApplicationBuilderExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRoutingApplicationBuilderExtensions.cs similarity index 65% rename from src/Routing/src/Microsoft.AspNetCore.Routing/Internal/EndpointRoutingApplicationBuilderExtensions.cs rename to src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRoutingApplicationBuilderExtensions.cs index 54a5d6a48f..acdad5b2af 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/EndpointRoutingApplicationBuilderExtensions.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Builder/EndpointRoutingApplicationBuilderExtensions.cs @@ -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 configure) { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + VerifyRoutingIsRegistered(builder); + var routeOptions = builder.ApplicationServices.GetRequiredService>(); + EndpointDataSource middlewareEndpointDataSource; + + var endpointRouteBuilder = builder.ApplicationServices.GetRequiredService(); + 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(); + return builder.UseMiddleware(middlewareEndpointDataSource); } public static IApplicationBuilder UseEndpoint(this IApplicationBuilder builder) @@ -51,4 +74,4 @@ namespace Microsoft.AspNetCore.Internal } } } -} +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs index 00a24ccfcd..d9bc0f559a 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs @@ -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 _dataSources; private IReadOnlyList _endpoints; private IChangeToken _consumerChangeToken; private CancellationTokenSource _cts; - internal CompositeEndpointDataSource(IEnumerable dataSources) + private CompositeEndpointDataSource() { - if (dataSources == null) - { - throw new ArgumentNullException(nameof(dataSources)); - } - CreateChangeToken(); - _dataSources = dataSources.ToArray(); _lock = new object(); } + internal CompositeEndpointDataSource(ObservableCollection dataSources) : this() + { + dataSources.CollectionChanged += OnDataSourcesChanged; + + _dataSources = dataSources; + } + + public CompositeEndpointDataSource(IEnumerable endpointDataSources) : this() + { + _dataSources = new List(); + + 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 DataSources => _dataSources; + /// /// Gets a used to signal invalidation of cached /// instances. diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/ConfigureEndpointOptions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/ConfigureRouteOptions.cs similarity index 62% rename from src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/ConfigureEndpointOptions.cs rename to src/Routing/src/Microsoft.AspNetCore.Routing/ConfigureRouteOptions.cs index ce6c069a22..f4ba549fe5 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/ConfigureEndpointOptions.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/ConfigureRouteOptions.cs @@ -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 + internal class ConfigureRouteOptions : IConfigureOptions { - private readonly IEnumerable _dataSources; + private readonly ICollection _dataSources; - public ConfigureEndpointOptions(IEnumerable dataSources) + public ConfigureRouteOptions(ICollection 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; } } } \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultEndpointRouteBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultEndpointRouteBuilder.cs new file mode 100644 index 0000000000..d5b09e20eb --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultEndpointRouteBuilder.cs @@ -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(); + } + + public IApplicationBuilder ApplicationBuilder { get; set; } + + public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New(); + + public ICollection DataSources { get; } + + public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices; + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs index 086d9bf026..2e04322698 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing public DefaultLinkGenerator( ParameterPolicyFactory parameterPolicyFactory, - CompositeEndpointDataSource dataSource, + EndpointDataSource dataSource, ObjectPool uriBuildingContextPool, IOptions routeOptions, ILogger logger, diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs index 1fa562966c..ba4e156ae0 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -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, ConfigureEndpointOptions>()); + // Setup global collection of endpoint data sources + var dataSources = new ObservableCollection(); + services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteOptions>( + serviceProvider => new ConfigureRouteOptions(dataSources))); // Allow global access to the list of endpoints. - services.TryAddSingleton(s => + services.TryAddSingleton(s => { - var options = s.GetRequiredService>(); - return new CompositeEndpointDataSource(options.Value.DataSources); + // Call internal ctor and pass global collection + return new CompositeEndpointDataSource(dataSources); }); + // + // Endpoint Infrastructure + // + services.TryAddTransient(); + // // Default matcher implementation // @@ -78,6 +87,11 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); + // + // Misc infrastructure + // + services.TryAddSingleton(); + return services; } diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointModel.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointModel.cs new file mode 100644 index 0000000000..6d9e085645 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointModel.cs @@ -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 Metadata { get; } = new List(); + + public abstract Endpoint Build(); + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointNameAddressScheme.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointNameAddressScheme.cs index 2716de36e8..5fd81b40c2 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointNameAddressScheme.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointNameAddressScheme.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Routing { private readonly DataSourceDependentCache> _cache; - public EndpointNameAddressScheme(CompositeEndpointDataSource dataSource) + public EndpointNameAddressScheme(EndpointDataSource dataSource) { _cache = new DataSourceDependentCache>(dataSource, Initialize); } diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointOptions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointOptions.cs deleted file mode 100644 index 8f364d6eaa..0000000000 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointOptions.cs +++ /dev/null @@ -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 DataSources { get; } = new List(); - } -} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointRoutingMiddleware.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointRoutingMiddleware.cs index 60efa46586..7b18916f29 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointRoutingMiddleware.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/EndpointRoutingMiddleware.cs @@ -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 _initializationTask; public EndpointRoutingMiddleware( MatcherFactory matcherFactory, - CompositeEndpointDataSource endpointDataSource, + EndpointDataSource endpointDataSource, ILogger logger, RequestDelegate next) { diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointModelConvention.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointModelConvention.cs new file mode 100644 index 0000000000..ee77b2cc63 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointModelConvention.cs @@ -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 convention); + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointRouteBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointRouteBuilder.cs new file mode 100644 index 0000000000..4d2d1eb69c --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IEndpointRouteBuilder.cs @@ -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 DataSources { get; } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/ArrayBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/ArrayBuilder.cs new file mode 100644 index 0000000000..868b3cfc27 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/ArrayBuilder.cs @@ -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 +{ + /// + /// Helper type for avoiding allocations while building arrays. + /// + /// The element type. + internal struct ArrayBuilder + { + 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. + + /// + /// Initializes the with a specified capacity. + /// + /// The capacity of the array to allocate. + public ArrayBuilder(int capacity) : this() + { + Debug.Assert(capacity >= 0); + if (capacity > 0) + { + _array = new T[capacity]; + } + } + + /// + /// Gets the number of items this instance can store without re-allocating, + /// or 0 if the backing array is null. + /// + public int Capacity => _array?.Length ?? 0; + + /// Gets the current underlying array. + public T[] Buffer => _array; + + /// + /// Gets the number of items in the array currently in use. + /// + public int Count => _count; + + /// + /// Gets or sets the item at a certain index in the array. + /// + /// The index into the array. + public T this[int index] + { + get + { + Debug.Assert(index >= 0 && index < _count); + return _array[index]; + } + } + + /// + /// Adds an item to the backing array, resizing it if necessary. + /// + /// The item to add. + public void Add(T item) + { + if (_count == Capacity) + { + EnsureCapacity(_count + 1); + } + + UncheckedAdd(item); + } + + /// + /// Gets the first item in this builder. + /// + public T First() + { + Debug.Assert(_count > 0); + return _array[0]; + } + + /// + /// Gets the last item in this builder. + /// + public T Last() + { + Debug.Assert(_count > 0); + return _array[_count - 1]; + } + + /// + /// Creates an array from the contents of this builder. + /// + /// + /// Do not call this method twice on the same builder. + /// + public T[] ToArray() + { + if (_count == 0) + { + return Array.Empty(); + } + + 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; + } + + /// + /// Adds an item to the backing array, without checking if there is room. + /// + /// The item to add. + /// + /// Use this method if you know there is enough space in the + /// for another item, and you are writing performance-sensitive code. + /// + 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; + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs index 61a4cd1960..1561ed868c 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs @@ -119,12 +119,7 @@ namespace Microsoft.AspNetCore.Routing.Internal private class OutboundMatchClassifier : IClassifier { - public OutboundMatchClassifier() - { - ValueComparer = new RouteValueEqualityComparer(); - } - - public IEqualityComparer ValueComparer { get; private set; } + public IEqualityComparer ValueComparer => RouteValueEqualityComparer.Default; public IDictionary GetCriteria(OutboundMatch item) { diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs index 3b3528932f..3cb240c2fa 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieFactory.cs @@ -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 diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs index 3bd29c9695..e59f283435 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/ILEmitTrieJumpTable.cs @@ -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 diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs index efee38f0f1..4545af9f3b 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs @@ -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 } } } diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj b/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj index a2ff4ab1ba..c76afe42c6 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj @@ -4,7 +4,7 @@ Commonly used types: Microsoft.AspNetCore.Routing.Route Microsoft.AspNetCore.Routing.RouteCollection - netstandard2.0;netcoreapp2.2 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;routing @@ -12,14 +12,10 @@ Microsoft.AspNetCore.Routing.RouteCollection - - true false - IL_EMIT;$(DefineConstants) IL_EMIT_SAVE_ASSEMBLIES;$(DefineConstants) diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/ModelEndpointDataSource.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/ModelEndpointDataSource.cs new file mode 100644 index 0000000000..5614310342 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/ModelEndpointDataSource.cs @@ -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 _endpointConventionBuilders; + + public ModelEndpointDataSource() + { + _endpointConventionBuilders = new List(); + } + + 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 Endpoints => _endpointConventionBuilders.Select(e => e.Build()).ToArray(); + + // for testing + internal IEnumerable EndpointModels => _endpointConventionBuilders.Select(b => b.EndpointModel); + + private class EndpointConventionBuilder : IEndpointConventionBuilder + { + internal EndpointModel EndpointModel { get; } + + private readonly List> _conventions; + + public EndpointConventionBuilder(EndpointModel endpointModel) + { + EndpointModel = endpointModel; + _conventions = new List>(); + } + + public void Apply(Action convention) + { + _conventions.Add(convention); + } + + public Endpoint Build() + { + foreach (var convention in _conventions) + { + convention(EndpointModel); + } + + return EndpointModel.Build(); + } + } + } +} \ No newline at end of file diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs new file mode 100644 index 0000000000..b1f313f7f9 --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/DefaultRoutePatternTransformer.cs @@ -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 updatedParameters = null; + List 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(original.PathSegments); + updatedParameters = new List(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 segments, List 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(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; + } + } + } + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs index 49860dcb0f..0cb7e48989 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs @@ -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(); + var constraints = new ArrayBuilder(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 ParameterPolicies; + public readonly RoutePatternParameterPolicyReference[] ParameterPolicies; - public ParameterPolicyParseResults(int currentIndex, IReadOnlyList parameterPolicies) + public ParameterPolicyParseResults(int currentIndex, RoutePatternParameterPolicyReference[] parameterPolicies) { CurrentIndex = currentIndex; ParameterPolicies = parameterPolicies; diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs index 1f1bf7ee0a..6852140bad 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePattern.cs @@ -23,17 +23,20 @@ namespace Microsoft.AspNetCore.Routing.Patterns string rawText, IReadOnlyDictionary defaults, IReadOnlyDictionary> parameterPolicies, + IReadOnlyDictionary requiredValues, IReadOnlyList parameters, IReadOnlyList 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 /// public IReadOnlyDictionary> ParameterPolicies { get; } + /// + /// Gets a collection of route values that must be provided for this route pattern to be considered + /// applicable. + /// + /// + /// + /// 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. + /// + /// This example shows how a route template can be used with required values to substitute known + /// route values for parameters. + /// + /// Route Template: "{controller=Home}/{action=Index}/{id?}" + /// Route Values: { controller = "Store", action = "Index" } + /// + /// + /// A route pattern produced in this way will match and generate URL paths like: /Store, + /// /Store/Index, and /Store/Index/17. + /// + /// + /// + public IReadOnlyDictionary RequiredValues { get; } + /// /// Gets the precedence value of the route pattern for URL matching. /// @@ -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())); } diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs index 2f755a08e9..7b577e25bd 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns /// public static class RoutePatternFactory { - private static readonly IReadOnlyDictionary EmptyDefaultsDictionary = + private static readonly IReadOnlyDictionary EmptyDictionary = new ReadOnlyDictionary(new Dictionary()); private static readonly IReadOnlyDictionary> 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); + } + + /// + /// Creates a from its string representation along + /// with provided default values and parameter policies. + /// + /// The route pattern string to parse. + /// + /// Additional default values to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the parsed route pattern. + /// + /// + /// Additional parameter policies to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the parsed route pattern. + /// + /// + /// Route values that can be substituted for parameters in the route pattern. See remarks on . + /// + /// The . + 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); } /// @@ -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); } /// @@ -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); } /// @@ -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); } /// /// Creates a from a collection of segments along /// with provided default values and parameter policies. /// - /// The raw text to associate with the route pattern. + /// The raw text to associate with the route pattern. May be null. /// /// Additional default values to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using @@ -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); } /// @@ -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); } /// @@ -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); } /// @@ -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); } /// @@ -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 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)kvp.Value.ToArray()) : EmptyPoliciesDictionary, + requiredValues ?? EmptyDictionary, (IReadOnlyList)parameters ?? Array.Empty(), 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); + } } } diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternTransformer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternTransformer.cs new file mode 100644 index 0000000000..bea4c610fc --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternTransformer.cs @@ -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 +{ + /// + /// A singleton service that provides transformations on . + /// + public abstract class RoutePatternTransformer + { + /// + /// Attempts to substitute the provided into the provided + /// . + /// + /// The original . + /// The required values to substitute. + /// + /// A new if substitution succeeds, otherwise null. + /// + /// + /// + /// 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 + /// can produce a derived route pattern + /// for each set of route values that corresponds to an endpoint. + /// + /// + /// The substitution process considers default values and implementations + /// when examining a required value. will + /// return null if any required value cannot be substituted. + /// + /// + public abstract RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues); + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteEndpointModel.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteEndpointModel.cs new file mode 100644 index 0000000000..8021c86b8f --- /dev/null +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteEndpointModel.cs @@ -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; + } + } +} diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs index efabde03a1..910ec514b7 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Routing { public class RouteOptions { + public ICollection EndpointDataSources { get; internal set; } + /// /// Gets or sets a value indicating whether all generated paths URLs are lower-case. /// Use to configure the behavior for query strings. diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs index 6f2a1eab45..f7cf0570c9 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs @@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Routing /// public class RouteValueEqualityComparer : IEqualityComparer { + public static readonly RouteValueEqualityComparer Default = new RouteValueEqualityComparer(); + /// public new bool Equals(object x, object y) { diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs index ae7bd66705..b31b59345e 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValuesAddressScheme.cs @@ -14,11 +14,11 @@ namespace Microsoft.AspNetCore.Routing { internal class RouteValuesAddressScheme : IEndpointAddressScheme { - private readonly CompositeEndpointDataSource _dataSource; + private readonly EndpointDataSource _dataSource; private LinkGenerationDecisionTree _allMatchesLinkGenerationTree; private Dictionary> _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(); + if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0) + { + continue; + } + if (endpoint.Metadata.GetMetadata()?.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 requiredValues, + string routeName) { - var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata(); 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; diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs index 4d7ba66b35..d529c511d6 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs @@ -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 diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs index ff41bf6af7..027020a02d 100644 --- a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs +++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs @@ -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(other.PathSegments.Select(p => new TemplateSegment(p))); Parameters = new List(); diff --git a/src/Routing/test/Directory.Build.props b/src/Routing/test/Directory.Build.props index c126684d3f..20029dfa54 100644 --- a/src/Routing/test/Directory.Build.props +++ b/src/Routing/test/Directory.Build.props @@ -1,13 +1,6 @@ - - netcoreapp2.2 - $(DeveloperBuildTestTfms) - $(StandardTestTfms) - $(StandardTestTfms);net461 - - false diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/EndpointMetadataCollectionTests.cs b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/EndpointMetadataCollectionTests.cs deleted file mode 100644 index 9f37f3ebec..0000000000 --- a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/EndpointMetadataCollectionTests.cs +++ /dev/null @@ -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 - { - 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(); - - // 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(); - - // 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(); - - // 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(); - - // 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 { } - - } -} \ No newline at end of file diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj index c5fa909119..c6388013e4 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj +++ b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs deleted file mode 100644 index 35b8031fe8..0000000000 --- a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs +++ /dev/null @@ -1,2063 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Testing; -using Xunit; - -namespace Microsoft.AspNetCore.Routing.Tests -{ - public class RouteValueDictionaryTests - { - [Fact] - public void DefaultCtor_UsesEmptyStorage() - { - // Arrange - // Act - var dict = new RouteValueDictionary(); - - // Assert - Assert.Empty(dict); - Assert.Empty(dict._arrayStorage); - Assert.Null(dict._propertyStorage); - } - - [Fact] - public void CreateFromNull_UsesEmptyStorage() - { - // Arrange - // Act - var dict = new RouteValueDictionary(null); - - // Assert - Assert.Empty(dict); - Assert.Empty(dict._arrayStorage); - Assert.Null(dict._propertyStorage); - } - - [Fact] - public void CreateFromRouteValueDictionary_WithArrayStorage_CopiesStorage() - { - // Arrange - var other = new RouteValueDictionary() - { - { "1", 1 } - }; - - // Act - var dict = new RouteValueDictionary(other); - - // Assert - Assert.Equal(other, dict); - - var storage = Assert.IsType[]>(dict._arrayStorage); - var otherStorage = Assert.IsType[]>(other._arrayStorage); - Assert.NotSame(otherStorage, storage); - } - - [Fact] - public void CreateFromRouteValueDictionary_WithPropertyStorage_CopiesStorage() - { - // Arrange - var other = new RouteValueDictionary(new { key = "value" }); - - // Act - var dict = new RouteValueDictionary(other); - - // Assert - Assert.Equal(other, dict); - - var storage = dict._propertyStorage; - var otherStorage = other._propertyStorage; - Assert.Same(otherStorage, storage); - } - - public static IEnumerable IEnumerableKeyValuePairData - { - get - { - var routeValues = new[] - { - new KeyValuePair("Name", "James"), - new KeyValuePair("Age", 30), - new KeyValuePair("Address", new Address() { City = "Redmond", State = "WA" }) - }; - - yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; - - yield return new object[] { routeValues.ToList() }; - - yield return new object[] { routeValues }; - } - } - - public static IEnumerable IEnumerableStringValuePairData - { - get - { - var routeValues = new[] - { - new KeyValuePair("First Name", "James"), - new KeyValuePair("Last Name", "Henrik"), - new KeyValuePair("Middle Name", "Bob") - }; - - yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; - - yield return new object[] { routeValues.ToList() }; - - yield return new object[] { routeValues }; - } - } - - [Theory] - [MemberData(nameof(IEnumerableKeyValuePairData))] - public void CreateFromIEnumerableKeyValuePair_CopiesValues(object values) - { - // Arrange & Act - var dict = new RouteValueDictionary(values); - - // Assert - Assert.IsType[]>(dict._arrayStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("Address", kvp.Key); - var address = Assert.IsType
(kvp.Value); - Assert.Equal("Redmond", address.City); - Assert.Equal("WA", address.State); - }, - kvp => { Assert.Equal("Age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("Name", kvp.Key); Assert.Equal("James", kvp.Value); }); - } - - [Theory] - [MemberData(nameof(IEnumerableStringValuePairData))] - public void CreateFromIEnumerableStringValuePair_CopiesValues(object values) - { - // Arrange & Act - var dict = new RouteValueDictionary(values); - - // Assert - Assert.IsType[]>(dict._arrayStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); }, - kvp => { Assert.Equal("Last Name", kvp.Key); Assert.Equal("Henrik", kvp.Value); }, - kvp => { Assert.Equal("Middle Name", kvp.Key); Assert.Equal("Bob", kvp.Value); }); - } - - [Fact] - public void CreateFromIEnumerableKeyValuePair_ThrowsExceptionForDuplicateKey() - { - // Arrange - var values = new List>() - { - new KeyValuePair("name", "Billy"), - new KeyValuePair("Name", "Joey"), - }; - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => new RouteValueDictionary(values), - "key", - $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); - } - - [Fact] - public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey() - { - // Arrange - var values = new List>() - { - new KeyValuePair("name", "Billy"), - new KeyValuePair("Name", "Joey"), - }; - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => new RouteValueDictionary(values), - "key", - $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); - } - - [Fact] - public void CreateFromObject_CopiesPropertiesFromAnonymousType() - { - // Arrange - var obj = new { cool = "beans", awesome = 123 }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); }, - kvp => { Assert.Equal("cool", kvp.Key); Assert.Equal("beans", kvp.Value); }); - } - - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType() - { - // Arrange - var obj = new RegularType() { CoolnessFactor = 73 }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("CoolnessFactor", kvp.Key); - Assert.Equal(73, kvp.Value); - }, - kvp => - { - Assert.Equal("IsAwesome", kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.False(value); - }); - } - - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_PublicOnly() - { - // Arrange - var obj = new Visibility() { IsPublic = true, ItsInternalDealWithIt = 5 }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("IsPublic", kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.True(value); - }); - } - - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresStatic() - { - // Arrange - var obj = new StaticProperty(); - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Empty(dict); - } - - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresSetOnly() - { - // Arrange - var obj = new SetterOnly() { CoolSetOnly = false }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Empty(dict); - } - - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_IncludesInherited() - { - // Arrange - var obj = new Derived() { TotallySweetProperty = true, DerivedProperty = false }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("DerivedProperty", kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.False(value); - }, - kvp => - { - Assert.Equal("TotallySweetProperty", kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.True(value); - }); - } - - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_WithHiddenProperty() - { - // Arrange - var obj = new DerivedHiddenProperty() { DerivedProperty = 5 }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); }); - } - - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_WithIndexerProperty() - { - // Arrange - var obj = new IndexerProperty(); - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Empty(dict); - } - - [Fact] - public void CreateFromObject_MixedCaseThrows() - { - // Arrange - var obj = new { controller = "Home", Controller = "Home" }; - - var message = - $"The type '{obj.GetType().FullName}' defines properties 'controller' and 'Controller' which differ " + - $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " + - $"case-insensitive comparisons."; - - // Act & Assert - var exception = Assert.Throws(() => - { - var dictionary = new RouteValueDictionary(obj); - }); - - // Ignoring case to make sure we're not testing reflection's ordering. - Assert.Equal(message, exception.Message, ignoreCase: true); - } - - // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what. - [Fact] - public void Comparer_IsOrdinalIgnoreCase() - { - // Arrange - // Act - var dict = new RouteValueDictionary(); - - // Assert - Assert.Same(StringComparer.OrdinalIgnoreCase, dict.Comparer); - } - - // Our comparer is hardcoded to be IsReadOnly==false no matter what. - [Fact] - public void IsReadOnly_False() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = ((ICollection>)dict).IsReadOnly; - - // Assert - Assert.False(result); - } - - [Fact] - public void IndexGet_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var value = dict[""]; - - // Assert - Assert.Null(value); - } - - [Fact] - public void IndexGet_EmptyStorage_ReturnsNull() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var value = dict["key"]; - - // Assert - Assert.Null(value); - } - - [Fact] - public void IndexGet_PropertyStorage_NoMatch_ReturnsNull() - { - // Arrange - var dict = new RouteValueDictionary(new { age = 30 }); - - // Act - var value = dict["key"]; - - // Assert - Assert.Null(value); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void IndexGet_PropertyStorage_Match_ReturnsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - var value = dict["key"]; - - // Assert - Assert.Equal("value", value); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void IndexGet_PropertyStorage_MatchIgnoreCase_ReturnsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - var value = dict["kEy"]; - - // Assert - Assert.Equal("value", value); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void IndexGet_ArrayStorage_NoMatch_ReturnsNull() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "age", 30 }, - }; - - // Act - var value = dict["key"]; - - // Assert - Assert.Null(value); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void IndexGet_ListStorage_Match_ReturnsValue() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var value = dict["key"]; - - // Assert - Assert.Equal("value", value); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void IndexGet_ListStorage_MatchIgnoreCase_ReturnsValue() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var value = dict["kEy"]; - - // Assert - Assert.Equal("value", value); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void IndexSet_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - dict[""] = "foo"; - - // Assert - Assert.Equal("foo", dict[""]); - } - - [Fact] - public void IndexSet_EmptyStorage_UpgradesToList() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - dict["key"] = "value"; - - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void IndexSet_PropertyStorage_NoMatch_AddsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { age = 30 }); - - // Act - dict["key"] = "value"; - - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void IndexSet_PropertyStorage_Match_SetsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - dict["key"] = "value"; - - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void IndexSet_PropertyStorage_MatchIgnoreCase_SetsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - dict["kEy"] = "value"; - - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void IndexSet_ListStorage_NoMatch_AddsValue() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "age", 30 }, - }; - - // Act - dict["key"] = "value"; - - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void IndexSet_ListStorage_Match_SetsValue() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - dict["key"] = "value"; - - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void IndexSet_ListStorage_MatchIgnoreCase_SetsValue() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - dict["key"] = "value"; - - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Count_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var count = dict.Count; - - // Assert - Assert.Equal(0, count); - } - - [Fact] - public void Count_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value", }); - - // Act - var count = dict.Count; - - // Assert - Assert.Equal(1, count); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void Count_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var count = dict.Count; - - // Assert - Assert.Equal(1, count); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Keys_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var keys = dict.Keys; - - // Assert - Assert.Empty(keys); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Keys_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value", }); - - // Act - var keys = dict.Keys; - - // Assert - Assert.Equal(new[] { "key" }, keys); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Keys_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var keys = dict.Keys; - - // Assert - Assert.Equal(new[] { "key" }, keys); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Values_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var values = dict.Values; - - // Assert - Assert.Empty(values); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Values_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value", }); - - // Act - var values = dict.Values; - - // Assert - Assert.Equal(new object[] { "value" }, values); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Values_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var values = dict.Values; - - // Assert - Assert.Equal(new object[] { "value" }, values); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Add_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - dict.Add("key", "value"); - - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Add_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - dict.Add("", "foo"); - - // Assert - Assert.Equal("foo", dict[""]); - } - - [Fact] - public void Add_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { age = 30 }); - - // Act - dict.Add("key", "value"); - - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - - // The upgrade from property -> array should make space for at least 4 entries - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("age", 30), kvp), - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } - - [Fact] - public void Add_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "age", 30 }, - }; - - // Act - dict.Add("key", "value"); - - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Add_DuplicateKey() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var message = $"An element with the key 'key' already exists in the {nameof(RouteValueDictionary)}"; - - // Act & Assert - ExceptionAssert.ThrowsArgument(() => dict.Add("key", "value2"), "key", message); - - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Add_DuplicateKey_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var message = $"An element with the key 'kEy' already exists in the {nameof(RouteValueDictionary)}"; - - // Act & Assert - ExceptionAssert.ThrowsArgument(() => dict.Add("kEy", "value2"), "key", message); - - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Add_KeyValuePair() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "age", 30 }, - }; - - // Act - ((ICollection>)dict).Add(new KeyValuePair("key", "value")); - - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Clear_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - dict.Clear(); - - // Assert - Assert.Empty(dict); - } - - [Fact] - public void Clear_PropertyStorage_AlreadyEmpty() - { - // Arrange - var dict = new RouteValueDictionary(new { }); - - // Act - dict.Clear(); - - // Assert - Assert.Empty(dict); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void Clear_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - dict.Clear(); - - // Assert - Assert.Empty(dict); - Assert.Null(dict._propertyStorage); - } - - [Fact] - public void Clear_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - dict.Clear(); - - // Assert - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Contains_KeyValuePair_True() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var input = new KeyValuePair("key", "value"); - - // Act - var result = ((ICollection>)dict).Contains(input); - - // Assert - Assert.True(result); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Contains_KeyValuePair_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var input = new KeyValuePair("KEY", "value"); - - // Act - var result = ((ICollection>)dict).Contains(input); - - // Assert - Assert.True(result); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Contains_KeyValuePair_False() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var input = new KeyValuePair("other", "value"); - - // Act - var result = ((ICollection>)dict).Contains(input); - - // Assert - Assert.False(result); - Assert.IsType[]>(dict._arrayStorage); - } - - // Value comparisons use the default equality comparer. - [Fact] - public void Contains_KeyValuePair_False_ValueComparisonIsDefault() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var input = new KeyValuePair("key", "valUE"); - - // Act - var result = ((ICollection>)dict).Contains(input); - - // Assert - Assert.False(result); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void ContainsKey_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.ContainsKey("key"); - - // Assert - Assert.False(result); - } - - [Fact] - public void ContainsKey_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.ContainsKey(""); - - // Assert - Assert.False(result); - } - - [Fact] - public void ContainsKey_PropertyStorage_False() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - var result = dict.ContainsKey("other"); - - // Assert - Assert.False(result); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void ContainsKey_PropertyStorage_True() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - var result = dict.ContainsKey("key"); - - // Assert - Assert.True(result); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void ContainsKey_PropertyStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - var result = dict.ContainsKey("kEy"); - - // Assert - Assert.True(result); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void ContainsKey_ListStorage_False() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var result = dict.ContainsKey("other"); - - // Assert - Assert.False(result); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void ContainsKey_ListStorage_True() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var result = dict.ContainsKey("key"); - - // Assert - Assert.True(result); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void ContainsKey_ListStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var result = dict.ContainsKey("kEy"); - - // Assert - Assert.True(result); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void CopyTo() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var array = new KeyValuePair[2]; - - // Act - ((ICollection>)dict).CopyTo(array, 1); - - // Assert - Assert.Equal( - new KeyValuePair[] - { - default(KeyValuePair), - new KeyValuePair("key", "value") - }, - array); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyValuePair_True() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var input = new KeyValuePair("key", "value"); - - // Act - var result = ((ICollection>)dict).Remove(input); - - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyValuePair_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var input = new KeyValuePair("KEY", "value"); - - // Act - var result = ((ICollection>)dict).Remove(input); - - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyValuePair_False() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var input = new KeyValuePair("other", "value"); - - // Act - var result = ((ICollection>)dict).Remove(input); - - // Assert - Assert.False(result); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - // Value comparisons use the default equality comparer. - [Fact] - public void Remove_KeyValuePair_False_ValueComparisonIsDefault() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - var input = new KeyValuePair("key", "valUE"); - - // Act - var result = ((ICollection>)dict).Remove(input); - - // Assert - Assert.False(result); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.Remove("key"); - - // Assert - Assert.False(result); - } - - [Fact] - public void Remove_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.Remove(""); - - // Assert - Assert.False(result); - } - - [Fact] - public void Remove_PropertyStorage_Empty() - { - // Arrange - var dict = new RouteValueDictionary(new { }); - - // Act - var result = dict.Remove("other"); - - // Assert - Assert.False(result); - Assert.Empty(dict); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void Remove_PropertyStorage_False() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - var result = dict.Remove("other"); - - // Assert - Assert.False(result); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_PropertyStorage_True() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - var result = dict.Remove("key"); - - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_PropertyStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - var result = dict.Remove("kEy"); - - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_ListStorage_False() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var result = dict.Remove("other"); - - // Assert - Assert.False(result); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_ListStorage_True() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var result = dict.Remove("key"); - - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_ListStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var result = dict.Remove("kEy"); - - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - - [Fact] - public void Remove_KeyAndOutValue_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.False(result); - Assert.Null(removedValue); - } - - [Fact] - public void Remove_KeyAndOutValue_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.Remove("", out var removedValue); - - // Assert - Assert.False(result); - Assert.Null(removedValue); - } - - [Fact] - public void Remove_KeyAndOutValue_PropertyStorage_Empty() - { - // Arrange - var dict = new RouteValueDictionary(new { }); - - // Act - var result = dict.Remove("other", out var removedValue); - - // Assert - Assert.False(result); - Assert.Null(removedValue); - Assert.Empty(dict); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void Remove_KeyAndOutValue_PropertyStorage_False() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - var result = dict.Remove("other", out var removedValue); - - // Assert - Assert.False(result); - Assert.Null(removedValue); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyAndOutValue_PropertyStorage_True() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary(new { key = value }); - - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyAndOutValue_PropertyStorage_True_CaseInsensitive() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary(new { key = value }); - - // Act - var result = dict.Remove("kEy", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyAndOutValue_ListStorage_False() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - var result = dict.Remove("other", out var removedValue); - - // Assert - Assert.False(result); - Assert.Null(removedValue); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyAndOutValue_ListStorage_True() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() - { - { "key", value } - }; - - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyAndOutValue_ListStorage_True_CaseInsensitive() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() - { - { "key", value } - }; - - // Act - var result = dict.Remove("kEy", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyAndOutValue_ListStorage_KeyExists_First() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() - { - { "key", value }, - { "other", 5 }, - { "dotnet", "rocks" } - }; - - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Equal(2, dict.Count); - Assert.False(dict.ContainsKey("key")); - Assert.True(dict.ContainsKey("other")); - Assert.True(dict.ContainsKey("dotnet")); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyAndOutValue_ListStorage_KeyExists_Middle() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() - { - { "other", 5 }, - { "key", value }, - { "dotnet", "rocks" } - }; - - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Equal(2, dict.Count); - Assert.False(dict.ContainsKey("key")); - Assert.True(dict.ContainsKey("other")); - Assert.True(dict.ContainsKey("dotnet")); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void Remove_KeyAndOutValue_ListStorage_KeyExists_Last() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() - { - { "other", 5 }, - { "dotnet", "rocks" }, - { "key", value } - }; - - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Equal(2, dict.Count); - Assert.False(dict.ContainsKey("key")); - Assert.True(dict.ContainsKey("other")); - Assert.True(dict.ContainsKey("dotnet")); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void TryAdd_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.TryAdd("", "foo"); - - // Assert - Assert.True(result); - } - - // We always 'upgrade' if you are trying to write to the dictionary. - [Fact] - public void TryAdd_ConvertsPropertyStorage_ToArrayStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value", }); - - // Act - var result = dict.TryAdd("key", "value"); - - // Assert - Assert.False(result); - Assert.Null(dict._propertyStorage); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } - - [Fact] - public void TryAdd_EmptyStorage_CanAdd() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.TryAdd("key", "value"); - - // Assert - Assert.True(result); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } - - [Fact] - public void TryAdd_ArrayStorage_CanAdd() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key0", "value0" }, - }; - - // Act - var result = dict.TryAdd("key1", "value1"); - - // Assert - Assert.True(result); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), - kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } - - [Fact] - public void TryAdd_ArrayStorage_CanAddWithResize() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key0", "value0" }, - { "key1", "value1" }, - { "key2", "value2" }, - { "key3", "value3" }, - }; - - // Act - var result = dict.TryAdd("key4", "value4"); - - // Assert - Assert.True(result); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), - kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), - kvp => Assert.Equal(new KeyValuePair("key2", "value2"), kvp), - kvp => Assert.Equal(new KeyValuePair("key3", "value3"), kvp), - kvp => Assert.Equal(new KeyValuePair("key4", "value4"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } - - [Fact] - public void TryAdd_ArrayStorage_DoesNotAddWhenKeyIsPresent() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key0", "value0" }, - }; - - // Act - var result = dict.TryAdd("key0", "value1"); - - // Assert - Assert.False(result); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } - - [Fact] - public void TryGetValue_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - object value; - var result = dict.TryGetValue("key", out value); - - // Assert - Assert.False(result); - Assert.Null(value); - } - - [Fact] - public void TryGetValue_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.TryGetValue("", out var value); - - // Assert - Assert.False(result); - Assert.Null(value); - } - - [Fact] - public void TryGetValue_PropertyStorage_False() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - object value; - var result = dict.TryGetValue("other", out value); - - // Assert - Assert.False(result); - Assert.Null(value); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void TryGetValue_PropertyStorage_True() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - object value; - var result = dict.TryGetValue("key", out value); - - // Assert - Assert.True(result); - Assert.Equal("value", value); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void TryGetValue_PropertyStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); - - // Act - object value; - var result = dict.TryGetValue("kEy", out value); - - // Assert - Assert.True(result); - Assert.Equal("value", value); - Assert.NotNull(dict._propertyStorage); - } - - [Fact] - public void TryGetValue_ListStorage_False() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - object value; - var result = dict.TryGetValue("other", out value); - - // Assert - Assert.False(result); - Assert.Null(value); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void TryGetValue_ListStorage_True() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - object value; - var result = dict.TryGetValue("key", out value); - - // Assert - Assert.True(result); - Assert.Equal("value", value); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void TryGetValue_ListStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() - { - { "key", "value" }, - }; - - // Act - object value; - var result = dict.TryGetValue("kEy", out value); - - // Assert - Assert.True(result); - Assert.Equal("value", value); - Assert.IsType[]>(dict._arrayStorage); - } - - [Fact] - public void ListStorage_DynamicallyAdjustsCapacity() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act 1 - dict.Add("key", "value"); - - // Assert 1 - var storage = Assert.IsType[]>(dict._arrayStorage); - Assert.Equal(4, storage.Length); - - // Act 2 - dict.Add("key2", "value2"); - dict.Add("key3", "value3"); - dict.Add("key4", "value4"); - dict.Add("key5", "value5"); - - // Assert 2 - storage = Assert.IsType[]>(dict._arrayStorage); - Assert.Equal(8, storage.Length); - } - - [Fact] - public void ListStorage_RemoveAt_RearrangesInnerArray() - { - // Arrange - var dict = new RouteValueDictionary(); - dict.Add("key", "value"); - dict.Add("key2", "value2"); - dict.Add("key3", "value3"); - - // Assert 1 - var storage = Assert.IsType[]>(dict._arrayStorage); - Assert.Equal(3, dict.Count); - - // Act - dict.Remove("key2"); - - // Assert 2 - storage = Assert.IsType[]>(dict._arrayStorage); - Assert.Equal(2, dict.Count); - Assert.Equal("key", storage[0].Key); - Assert.Equal("value", storage[0].Value); - Assert.Equal("key3", storage[1].Key); - Assert.Equal("value3", storage[1].Value); - } - - [Fact] - public void FromArray_TakesOwnershipOfArray() - { - // Arrange - var array = new KeyValuePair[] - { - new KeyValuePair("a", 0), - new KeyValuePair("b", 1), - new KeyValuePair("c", 2), - }; - - var dictionary = RouteValueDictionary.FromArray(array); - - // Act - modifying the array should modify the dictionary - array[0] = new KeyValuePair("aa", 10); - - // Assert - Assert.Equal(3, dictionary.Count); - Assert.Equal(10, dictionary["aa"]); - } - - [Fact] - public void FromArray_EmptyArray() - { - // Arrange - var array = Array.Empty>(); - - // Act - var dictionary = RouteValueDictionary.FromArray(array); - - // Assert - Assert.Empty(dictionary); - } - - [Fact] - public void FromArray_RemovesGapsInArray() - { - // Arrange - var array = new KeyValuePair[] - { - new KeyValuePair(null, null), - new KeyValuePair("a", 0), - new KeyValuePair(null, null), - new KeyValuePair(null, null), - new KeyValuePair("b", 1), - new KeyValuePair("c", 2), - new KeyValuePair("d", 3), - new KeyValuePair(null, null), - }; - - // Act - calling From should modify the array - var dictionary = RouteValueDictionary.FromArray(array); - - // Assert - Assert.Equal(4, dictionary.Count); - Assert.Equal( - new KeyValuePair[] - { - new KeyValuePair("d", 3), - new KeyValuePair("a", 0), - new KeyValuePair("c", 2), - new KeyValuePair("b", 1), - new KeyValuePair(null, null), - new KeyValuePair(null, null), - new KeyValuePair(null, null), - new KeyValuePair(null, null), - }, - array); - } - - private class RegularType - { - public bool IsAwesome { get; set; } - - public int CoolnessFactor { get; set; } - } - - private class Visibility - { - private string PrivateYo { get; set; } - - internal int ItsInternalDealWithIt { get; set; } - - public bool IsPublic { get; set; } - } - - private class StaticProperty - { - public static bool IsStatic { get; set; } - } - - private class SetterOnly - { - private bool _coolSetOnly; - - public bool CoolSetOnly { set { _coolSetOnly = value; } } - } - - private class Base - { - public bool DerivedProperty { get; set; } - } - - private class Derived : Base - { - public bool TotallySweetProperty { get; set; } - } - - private class DerivedHiddenProperty : Base - { - public new int DerivedProperty { get; set; } - } - - private class IndexerProperty - { - public bool this[string key] - { - get { return false; } - set { } - } - } - - private class Address - { - public string City { get; set; } - - public string State { get; set; } - } - } -} \ No newline at end of file diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs index 5e28fd8917..b8eea95253 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs @@ -207,13 +207,7 @@ namespace Microsoft.AspNetCore.Routing.DecisionTree private class ItemClassifier : IClassifier { - public IEqualityComparer ValueComparer - { - get - { - return new RouteValueEqualityComparer(); - } - } + public IEqualityComparer ValueComparer => RouteValueEqualityComparer.Default; public IDictionary GetCriteria(Item item) { diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj index 286e462426..21fee51bbf 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs index 2e39f5846c..b40da83fc9 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs @@ -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 \ No newline at end of file diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/RouterBenchmarkTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/RouterBenchmarkTest.cs index bb7300388b..aecbaaab8c 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/RouterBenchmarkTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Benchmarks/RouterBenchmarkTest.cs @@ -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 \ No newline at end of file +} \ No newline at end of file diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs index 6f5a978c5f..6201b01d7b 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs @@ -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); diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj index 0fecc56388..79adee4f98 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 @@ -10,7 +10,7 @@ - + diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RouterSampleTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RouterSampleTest.cs index 315dcc1b68..9388a03781 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RouterSampleTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RouterSampleTest.cs @@ -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() { diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/EndpointRoutingBuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs similarity index 54% rename from src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/EndpointRoutingBuilderExtensionsTest.cs rename to src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs index 106a47e27b..7d0cb9a191 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/EndpointRoutingBuilderExtensionsTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs @@ -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()); // Act - var ex = Assert.Throws(() => app.UseEndpointRouting()); + var ex = Assert.Throws(() => 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()); } - private IServiceProvider CreateServices(params Endpoint[] endpoints) + [Fact] + public void UseEndpointRouting_CallWithBuilder_SetsEndpointDataSource() + { + // Arrange + var matcherEndpointDataSources = new List(); + var matcherFactoryMock = new Mock(); + matcherFactoryMock + .Setup(m => m.CreateMatcher(It.IsAny())) + .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(); + + // 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); + } + services.AddLogging(); services.AddOptions(); services.AddRouting(); - services.AddSingleton(new DefaultEndpointDataSource(endpoints)); + var serviceProvder = services.BuildServiceProvider(); - return services.BuildServiceProvider(); + return serviceProvder; } } } diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/MapEndpointEndpointDataSourceBuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/MapEndpointEndpointDataSourceBuilderExtensionsTest.cs new file mode 100644 index 0000000000..ad714df63c --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Builder/MapEndpointEndpointDataSourceBuilderExtensionsTest.cs @@ -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(Assert.Single(endpointRouteBuilder.DataSources)); + } + + private RouteEndpointModel GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder) + { + return Assert.IsType(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)); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs index 4f6792b47f..de47be3873 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorProcessTemplateTest.cs @@ -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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(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 configure = (s) => + { + s.Configure(o => o.AppendTrailingSlash = false); + }; + var linkGenerator = CreateLinkGenerator( - new RouteOptions() { AppendTrailingSlash = false }, + configure, endpoints: new[] { endpoint }); var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index 3c20c704a0..9b423891d8 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -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 configureServices = s => + { + s.Configure(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 configureServices = s => + { + s.Configure(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 configure = (s) => { - s.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform()); + s.Configure(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 configure = (s) => { - s.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform()); + s.Configure(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 { - private readonly CompositeEndpointDataSource _dataSource; + private readonly EndpointDataSource _dataSource; - public IntAddressScheme(CompositeEndpointDataSource dataSource) + public IntAddressScheme(EndpointDataSource dataSource) { _dataSource = dataSource; } diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs index e3a5b0f802..99ac7752f5 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs @@ -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 { diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs index 48861da09b..df5c703fb2 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs @@ -151,10 +151,9 @@ namespace Microsoft.AspNetCore.Routing logger = logger ?? new Logger(NullLoggerFactory.Instance); matcherFactory = matcherFactory ?? new TestMatcherFactory(true); - var options = Options.Create(new EndpointOptions()); var middleware = new EndpointRoutingMiddleware( matcherFactory, - new CompositeEndpointDataSource(Array.Empty()), + new DefaultEndpointDataSource(), logger, next); diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs index a2a9c22db9..e9c3880262 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorTestBase.cs @@ -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 configureServices, params Endpoint[] endpoints) { - return CreateLinkGenerator(routeOptions, configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty()) }); + return CreateLinkGenerator(configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty()) }); } 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 configureServices, EndpointDataSource[] dataSources) { @@ -74,25 +68,25 @@ namespace Microsoft.AspNetCore.Routing AddAdditionalServices(services); configureServices?.Invoke(services); - routeOptions = routeOptions ?? new RouteOptions(); - dataSources = dataSources ?? Array.Empty(); - - services.Configure((o) => + services.Configure(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>(); return new DefaultLinkGenerator( - new DefaultParameterPolicyFactory(options, serviceProvider), - serviceProvider.GetRequiredService(), + new DefaultParameterPolicyFactory(routeOptions, serviceProvider), + new CompositeEndpointDataSource(routeOptions.Value.EndpointDataSources), new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), - options, + routeOptions, NullLogger.Instance, serviceProvider); } diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieFactoryTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieFactoryTest.cs index 738646dc11..f2eb13df81 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieFactoryTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieFactoryTest.cs @@ -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 diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieJumpTableTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieJumpTableTest.cs index 5106b40beb..4a435514ab 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieJumpTableTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/ILEmitTrieJumpTableTest.cs @@ -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 diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs index de2f525a21..c7615bc19c 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs @@ -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(context); httpContext.Features.Set(context); diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs index 3e5b59baf6..36927d0e75 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs @@ -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 diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/VectorizedILEmitTrieJumpTableTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/VectorizedILEmitTrieJumpTableTest.cs index fc5e7c555a..3661b158ab 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/VectorizedILEmitTrieJumpTableTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Matching/VectorizedILEmitTrieJumpTableTest.cs @@ -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 diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj index 540f151fed..035506e996 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj @@ -1,23 +1,11 @@  - $(StandardTestTfms) + netcoreapp3.0 Microsoft.AspNetCore.Routing true - - - true - IL_EMIT;$(DefineConstants) - - - - - diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs new file mode 100644 index 0000000000..ab30b0349f --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/DefaultRoutePatternTransformerTest.cs @@ -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(); + } + + 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("a", null), kvp), + kvp => Assert.Equal(new KeyValuePair("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("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("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("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("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("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("area", "Admin"), kvp), + kvp => Assert.Equal(new KeyValuePair("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("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("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("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("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("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("action", null), kvp), + kvp => Assert.Equal(new KeyValuePair("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("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("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("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("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("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("area", "Admin"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs index b56465dd7a..ef4ac2fc82 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternFactoryTest.cs @@ -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(() => + { + 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() { diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteEndpointModelTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteEndpointModelTest.cs new file mode 100644 index 0000000000..cf75832ddd --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteEndpointModelTest.cs @@ -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(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)); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs index fc92eeab27..80f79f9172 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressSchemeTest.cs @@ -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(); - 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); diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePatternPrecedenceTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePatternPrecedenceTests.cs new file mode 100644 index 0000000000..049263444e --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePatternPrecedenceTests.cs @@ -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 func) + { + var parsed = RoutePatternFactory.Parse(template); + return func(parsed); + } + } +} diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTestsBase.cs similarity index 74% rename from src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs rename to src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTestsBase.cs index 7dd04454e2..b5c2f87db1 100644 --- a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTestsBase.cs @@ -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(() => + { + // 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 func) + [Fact] + public void ComputeMatched_TooManySegments_ThrowHumaneError() { - var options = new Mock>(); - options.SetupGet(o => o.Value).Returns(new RouteOptions()); + var ex = Assert.Throws(() => + { + // 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); } } diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplatePrecedenceTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplatePrecedenceTests.cs new file mode 100644 index 0000000000..98157fa669 --- /dev/null +++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RouteTemplatePrecedenceTests.cs @@ -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 func) + { + var parsed = TemplateParser.Parse(template); + return func(parsed); + } + } +} diff --git a/src/Routing/test/WebSites/Directory.Build.props b/src/Routing/test/WebSites/Directory.Build.props index 2ff6b1fb54..c1b2f8c824 100644 --- a/src/Routing/test/WebSites/Directory.Build.props +++ b/src/Routing/test/WebSites/Directory.Build.props @@ -1,11 +1,4 @@ - - - netcoreapp2.2 - $(DeveloperBuildTestWebsiteTfms) - netcoreapp2.2 - $(StandardTestWebsiteTfms);net461 - diff --git a/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/EndpointRouteBuilderExtensions.cs b/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/EndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..fc26b50bf8 --- /dev/null +++ b/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/EndpointRouteBuilderExtensions.cs @@ -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); + } + } +} diff --git a/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloAppBuilderExtensions.cs b/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloAppBuilderExtensions.cs new file mode 100644 index 0000000000..195bec2496 --- /dev/null +++ b/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloAppBuilderExtensions.cs @@ -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(Options.Create(new HelloOptions + { + Greeter = greeter + })); + } + } +} diff --git a/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloMiddleware.cs b/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloMiddleware.cs new file mode 100644 index 0000000000..75b2c42749 --- /dev/null +++ b/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloMiddleware.cs @@ -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) + { + _next = next; + _helloOptions = helloOptions.Value; + + var payload = new List(); + 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); + } + } +} diff --git a/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloOptions.cs b/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloOptions.cs new file mode 100644 index 0000000000..aedad8fb9e --- /dev/null +++ b/src/Routing/test/WebSites/RoutingWebSite/HelloExtension/HelloOptions.cs @@ -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; } + } +} diff --git a/src/Routing/test/WebSites/RoutingWebSite/RoutingWebSite.csproj b/src/Routing/test/WebSites/RoutingWebSite/RoutingWebSite.csproj index d462c5fbbc..35ac135e33 100644 --- a/src/Routing/test/WebSites/RoutingWebSite/RoutingWebSite.csproj +++ b/src/Routing/test/WebSites/RoutingWebSite/RoutingWebSite.csproj @@ -1,8 +1,7 @@  - netcoreapp2.2 - $(TargetFrameworks);net461 + netcoreapp3.0 diff --git a/src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs b/src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs index 57bc673573..fb067a3fe4 100644 --- a/src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs +++ b/src/Routing/test/WebSites/RoutingWebSite/UseEndpointRoutingStartup.cs @@ -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(); - var dataSource = httpContext.RequestServices.GetRequiredService(); - 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(); - - 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(); - - 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)); } public void Configure(IApplicationBuilder app) { - app.UseEndpointRouting(); + app.UseEndpointRouting(routes => + { + routes.MapHello("/helloworld", "World"); + + routes.MapGet( + "/", + (httpContext) => + { + var dataSource = httpContext.RequestServices.GetRequiredService(); + + var sb = new StringBuilder(); + sb.AppendLine("Endpoints:"); + foreach (var endpoint in dataSource.Endpoints.OfType().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(); + + 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(); + + 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(); + } } } diff --git a/src/Routing/test/WebSites/RoutingWebSite/UseRouterStartup.cs b/src/Routing/test/WebSites/RoutingWebSite/UseRouterStartup.cs index fb27ce550f..f09f359acf 100644 --- a/src/Routing/test/WebSites/RoutingWebSite/UseRouterStartup.cs +++ b/src/Routing/test/WebSites/RoutingWebSite/UseRouterStartup.cs @@ -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"]}")); + }); } } } diff --git a/src/Routing/tools/Swaggatherer/Swaggatherer.csproj b/src/Routing/tools/Swaggatherer/Swaggatherer.csproj index 6874d1d5da..35103913f2 100644 --- a/src/Routing/tools/Swaggatherer/Swaggatherer.csproj +++ b/src/Routing/tools/Swaggatherer/Swaggatherer.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.0 diff --git a/src/Routing/version.props b/src/Routing/version.props index 4889a26987..d977b437b6 100644 --- a/src/Routing/version.props +++ b/src/Routing/version.props @@ -1,12 +1,6 @@ - + - 2.2.0 - rtm - $(VersionPrefix) - $(VersionPrefix)-$(VersionSuffix)-final - t000 - a- - $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) - $(VersionSuffix)-$(BuildNumber) + 3.0.0 + dev