diff --git a/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs b/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs index c04f28decc..2c5fa5a7d9 100644 --- a/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs +++ b/benchmarkapps/Benchmarks/StartupUsingEndpointRouting.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Benchmarks { @@ -18,9 +19,7 @@ namespace Benchmarks { services.AddRouting(); - services.Configure(options => - { - options.DataSources.Add(new DefaultEndpointDataSource(new[] + var endpointDataSource = new DefaultEndpointDataSource(new[] { new MatcherEndpoint( invoker: (next) => (httpContext) => @@ -33,12 +32,12 @@ namespace Benchmarks return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength); }, routePattern: RoutePatternFactory.Parse("/plaintext"), - requiredValues: new RouteValueDictionary(), order: 0, metadata: EndpointMetadataCollection.Empty, displayName: "Plaintext"), - })); - }); + }); + + services.TryAddEnumerable(ServiceDescriptor.Singleton(endpointDataSource)); } public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app) diff --git a/benchmarkapps/Benchmarks/benchmarks.json b/benchmarkapps/Benchmarks/benchmarks.json index e3eae96731..d69ee7afce 100644 --- a/benchmarkapps/Benchmarks/benchmarks.json +++ b/benchmarkapps/Benchmarks/benchmarks.json @@ -22,7 +22,7 @@ "PlaintextDispatcher": { "Path": "/plaintext" }, - "PlaintextGlobalRouting": { + "PlaintextEndpointRouting": { "Path": "/plaintext" } } diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/MatcherBenchmarkBase.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/MatcherBenchmarkBase.cs index 76802c758d..4b030f84dc 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/MatcherBenchmarkBase.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/MatcherBenchmarkBase.cs @@ -7,7 +7,6 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; @@ -44,7 +43,6 @@ namespace Microsoft.AspNetCore.Routing.Matching return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template), - new RouteValueDictionary(), 0, new EndpointMetadataCollection(metadata), template); diff --git a/samples/RoutingSample.Web/EndsWithStringMatchProcessor.cs b/samples/RoutingSample.Web/EndsWithStringMatchProcessor.cs index ea50812015..f0201f79d9 100644 --- a/samples/RoutingSample.Web/EndsWithStringMatchProcessor.cs +++ b/samples/RoutingSample.Web/EndsWithStringMatchProcessor.cs @@ -3,12 +3,14 @@ using System; using System.Globalization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.Logging; namespace RoutingSample.Web { - internal class EndsWithStringMatchProcessor : MatchProcessorBase + internal class EndsWithStringMatchProcessor : MatchProcessor { private readonly ILogger _logger; @@ -17,15 +19,34 @@ namespace RoutingSample.Web _logger = logger; } - public override bool Process(object value) + public string ParameterName { get; private set; } + + public string ConstraintArgument { get; private set; } + + public override void Initialize(string parameterName, string constraintArgument) { - if (value == null) + ParameterName = parameterName; + ConstraintArgument = constraintArgument; + } + + public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary values) + { + return Process(values); + } + + public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values) + { + return Process(values); + } + + private bool Process(RouteValueDictionary values) + { + if (!values.TryGetValue(ParameterName, out var value) || value == null) { return false; } var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - var endsWith = valueString.EndsWith(ConstraintArgument, StringComparison.OrdinalIgnoreCase); if (!endsWith) diff --git a/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs b/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs index 06dab7eddb..77b41e87a8 100644 --- a/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs +++ b/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace RoutingSample.Web { @@ -25,11 +26,9 @@ namespace RoutingSample.Web { options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor)); }); - - services.Configure(options => - { - options.DataSources.Add(new DefaultEndpointDataSource(new[] - { + + var endpointDataSource = new DefaultEndpointDataSource(new[] + { new MatcherEndpoint((next) => (httpContext) => { var response = httpContext.Response; @@ -40,7 +39,6 @@ namespace RoutingSample.Web return response.Body.WriteAsync(_homePayload, 0, payloadLength); }, RoutePatternFactory.Parse("/"), - new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, "Home"), @@ -54,7 +52,6 @@ namespace RoutingSample.Web return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength); }, RoutePatternFactory.Parse("/plaintext"), - new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, "Plaintext"), @@ -66,7 +63,6 @@ namespace RoutingSample.Web return response.WriteAsync("WithConstraints"); }, RoutePatternFactory.Parse("/withconstraints/{id:endsWith(_001)}"), - new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, "withconstraints"), @@ -78,12 +74,12 @@ namespace RoutingSample.Web return response.WriteAsync("withoptionalconstraints"); }, RoutePatternFactory.Parse("/withoptionalconstraints/{id:endsWith(_001)?}"), - new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, "withoptionalconstraints"), - })); - }); + }); + + services.TryAddEnumerable(ServiceDescriptor.Singleton(endpointDataSource)); } public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app) diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs new file mode 100644 index 0000000000..ec3736228a --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerationTemplate.cs @@ -0,0 +1,29 @@ +// 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 +{ + /// + /// Defines a contract to generate a URL from a template. + /// + public abstract class LinkGenerationTemplate + { + /// + /// Generates a URL with an absolute path from the specified route values. + /// + /// An object that contains route values. + /// The generated URL. + public string MakeUrl(object values) + { + return MakeUrl(values, options: null); + } + + /// + /// Generates a URL with an absolute path from the specified route values and link options. + /// + /// An object that contains route values. + /// The . + /// The generated URL. + public abstract string MakeUrl(object values, LinkOptions options); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs index df5c1b5910..35b9456bf6 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGenerator.cs @@ -1,13 +1,463 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing { + /// + /// Defines a contract to generate URLs to endpoints. + /// public abstract class LinkGenerator { - public abstract bool TryGetLink(LinkGeneratorContext context, out string link); + /// + /// Generates a URL with an absolute path from the specified route values. + /// + /// An object that contains route values. + /// The generated URL. + public string GetLink(object values) + { + return GetLink(httpContext: null, routeName: null, values, options: null); + } - public abstract string GetLink(LinkGeneratorContext context); + /// + /// Generates a URL with an absolute path from the specified route values and link options. + /// + /// An object that contains route values. + /// The . + /// The generated URL. + public string GetLink(object values, LinkOptions options) + { + return GetLink(httpContext: null, routeName: null, values, options); + } + + /// + /// Generates a URL with an absolute path from the specified route values. + /// A return value indicates whether the operation succeeded. + /// + /// An object that contains route values. + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLink(object values, out string link) + { + return TryGetLink(httpContext: null, routeName: null, values, options: null, out link); + } + + /// + /// Generates a URL with an absolute path from the specified route values and link options. + /// A return value indicates whether the operation succeeded. + /// + /// An object that contains route values. + /// The . + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLink(object values, LinkOptions options, out string link) + { + return TryGetLink(httpContext: null, routeName: null, values, options, out link); + } + + /// + /// Generates a URL with an absolute path from the specified route values. + /// + /// The associated with current request. + /// An object that contains route values. + /// The generated URL. + public string GetLink(HttpContext httpContext, object values) + { + return GetLink(httpContext, routeName: null, values, options: null); + } + + /// + /// Generates a URL with an absolute path from the specified route values. + /// A return value indicates whether the operation succeeded. + /// + /// The associated with current request. + /// An object that contains route values. + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLink(HttpContext httpContext, object values, out string link) + { + return TryGetLink(httpContext, routeName: null, values, options: null, out link); + } + + /// + /// Generates a URL with an absolute path from the specified route values and link options. + /// + /// The associated with current request. + /// An object that contains route values. + /// The . + /// The generated URL. + public string GetLink(HttpContext httpContext, object values, LinkOptions options) + { + return GetLink(httpContext, routeName: null, values, options); + } + + /// + /// Generates a URL with an absolute path from the specified route values and link options. + /// A return value indicates whether the operation succeeded. + /// + /// The associated with current request. + /// An object that contains route values. + /// The . + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLink(HttpContext httpContext, object values, LinkOptions options, out string link) + { + return TryGetLink(httpContext, routeName: null, values, options, out link); + } + + /// + /// Generates a URL with an absolute path from the specified route name and route values. + /// + /// The name of the route to generate the URL to. + /// An object that contains route values. + /// The generated URL. + public string GetLink(string routeName, object values) + { + return GetLink(httpContext: null, routeName, values, options: null); + } + + /// + /// Generates a URL with an absolute path from the specified route name and route values. + /// A return value indicates whether the operation succeeded. + /// + /// The name of the route to generate the URL to. + /// An object that contains route values. + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLink(string routeName, object values, out string link) + { + return TryGetLink(httpContext: null, routeName, values, options: null, out link); + } + + /// + /// Generates a URL with an absolute path from the specified route name and route values. + /// + /// The name of the route to generate the URL to. + /// An object that contains route values. + /// The . + /// The generated URL. + public string GetLink(string routeName, object values, LinkOptions options) + { + return GetLink(httpContext: null, routeName, values, options); + } + + /// + /// Generates a URL with an absolute path from the specified route name, route values and link options. + /// A return value indicates whether the operation succeeded. + /// + /// The name of the route to generate the URL to. + /// An object that contains route values. + /// The . + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLink(string routeName, object values, LinkOptions options, out string link) + { + return TryGetLink(httpContext: null, routeName, values, options, out link); + } + + /// + /// Generates a URL with an absolute path from the specified route name and route values. + /// + /// The name of the route to generate the URL to. + /// The associated with current request. + /// An object that contains route values. + /// The generated URL. + public string GetLink(HttpContext httpContext, string routeName, object values) + { + return GetLink(httpContext, routeName, values, options: null); + } + + /// + /// Generates a URL with an absolute path from the specified route name and route values. + /// A return value indicates whether the operation succeeded. + /// + /// The associated with current request. + /// The name of the route to generate the URL to. + /// An object that contains route values. + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLink(HttpContext httpContext, string routeName, object values, out string link) + { + return TryGetLink(httpContext, routeName, values, options: null, out link); + } + + /// + /// Generates a URL with an absolute path from the specified route name, route values and link options. + /// + /// The name of the route to generate the URL to. + /// The associated with current request. + /// An object that contains route values. + /// The . + /// The generated URL. + public string GetLink(HttpContext httpContext, string routeName, object values, LinkOptions options) + { + if (TryGetLink(httpContext, routeName, values, options, out var link)) + { + return link; + } + + throw new InvalidOperationException("Could not find a matching endpoint to generate a link."); + } + + /// + /// Generates a URL with an absolute path from the specified route name, route values and link options. + /// A return value indicates whether the operation succeeded. + /// + /// The associated with current request. + /// The name of the route to generate the URL to. + /// An object that contains route values. + /// The . + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public abstract bool TryGetLink( + HttpContext httpContext, + string routeName, + object values, + LinkOptions options, + out string link); + + /// + /// Generates a URL with an absolute path from the specified lookup information and route values. + /// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for generating a URL. + /// An object that contains route values. + /// The generated URL. + public string GetLinkByAddress(TAddress address, object values) + { + return GetLinkByAddress(httpContext: null, address, values, options: null); + } + + /// + /// Generates a URL with an absolute path from the specified lookup information and route values. + /// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// A return value indicates whether the operation succeeded. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for generating a URL. + /// An object that contains route values. + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLinkByAddress(TAddress address, object values, out string link) + { + return TryGetLinkByAddress(address, values, options: null, out link); + } + + /// + /// Generates a URL with an absolute path from the specified lookup information, route values and link options. + /// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for generating a URL. + /// An object that contains route values. + /// The . + /// The generated URL. + public string GetLinkByAddress(TAddress address, object values, LinkOptions options) + { + return GetLinkByAddress(httpContext: null, address, values, options); + } + + /// + /// Generates a URL with an absolute path from the specified lookup information, route values and link options. + /// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// A return value indicates whether the operation succeeded. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for generating a URL. + /// An object that contains route values. + /// The . + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLinkByAddress( + TAddress address, + object values, + LinkOptions options, + out string link) + { + return TryGetLinkByAddress(httpContext: null, address, values, options, out link); + } + + /// + /// Generates a URL with an absolute path from the specified lookup information, route values and link options. + /// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for generating a URL. + /// The associated with current request. + /// An object that contains route values. + /// The generated URL. + public string GetLinkByAddress(HttpContext httpContext, TAddress address, object values) + { + return GetLinkByAddress(httpContext, address, values, options: null); + } + + /// + /// Generates a URL with an absolute path from the specified lookup information and route values. + /// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// A return value indicates whether the operation succeeded. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for generating a URL. + /// The associated with current request. + /// An object that contains route values. + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public bool TryGetLinkByAddress( + HttpContext httpContext, + TAddress address, + object values, + out string link) + { + return TryGetLinkByAddress(httpContext, address, values, options: null, out link); + } + + /// + /// Generates a URL with an absolute path from the specified lookup information, route values and link options. + /// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for generating a URL. + /// The associated with current request. + /// An object that contains route values. + /// The . + /// The generated URL. + public string GetLinkByAddress( + HttpContext httpContext, + TAddress address, + object values, + LinkOptions options) + { + if (TryGetLinkByAddress(httpContext, address, values, options, out var link)) + { + return link; + } + + throw new InvalidOperationException("Could not find a matching endpoint to generate a link."); + } + + /// + /// Generates a URL with an absolute path from the specified lookup information, route values and link options. + /// This lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// A return value indicates whether the operation succeeded. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for generating a URL. + /// The associated with current request. + /// An object that contains route values. + /// The . + /// The generated URL. + /// true if a URL was generated successfully; otherwise, false. + public abstract bool TryGetLinkByAddress( + HttpContext httpContext, + TAddress address, + object values, + LinkOptions options, + out string link); + + /// + /// Gets a to generate a URL from the specified route values. + /// This template object holds information of the endpoint(s) that were found and which can later be used to + /// generate a URL using the api. + /// + /// + /// An object that contains route values. These values are used to lookup endpoint(s). + /// + /// + /// If an endpoint(s) was found successfully, then this returns a template object representing that, + /// null otherwise. + /// + public LinkGenerationTemplate GetTemplate(object values) + { + return GetTemplate(httpContext: null, routeName: null, values); + } + + /// + /// Gets a to generate a URL from the specified route name and route values. + /// This template object holds information of the endpoint(s) that were found and which can later be used to + /// generate a URL using the api. + /// + /// The name of the route to generate the URL to. + /// + /// An object that contains route values. These values are used to lookup for endpoint(s). + /// + /// + /// If an endpoint(s) was found successfully, then this returns a template object representing that, + /// null otherwise. + /// + public LinkGenerationTemplate GetTemplate(string routeName, object values) + { + return GetTemplate(httpContext: null, routeName, values); + } + + /// + /// Gets a to generate a URL from the specified route values. + /// This template object holds information of the endpoint(s) that were found and which can later be used to + /// generate a URL using the api. + /// + /// The associated with current request. + /// + /// An object that contains route values. These values are used to lookup for endpoint(s). + /// + /// + /// If an endpoint(s) was found successfully, then this returns a template object representing that, + /// null otherwise. + /// + public LinkGenerationTemplate GetTemplate(HttpContext httpContext, object values) + { + return GetTemplate(httpContext, routeName: null, values); + } + + /// + /// Gets a to generate a URL from the specified route name and route values. + /// This template object holds information of the endpoint(s) that were found and which can later be used to + /// generate a URL using the api. + /// + /// The associated with current request. + /// The name of the route to generate the URL to. + /// + /// An object that contains route values. These values are used to lookup for endpoint(s). + /// + /// + /// If an endpoint(s) was found successfully, then this returns a template object representing that, + /// null otherwise. + /// + public abstract LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values); + + /// + /// Gets a to generate a URL from the specified lookup information. + /// This template object holds information of the endpoint(s) that were found and which can later be used to + /// generate a URL using the api. + /// The lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for creating a template. + /// + /// If an endpoint(s) was found successfully, then this returns a template object representing that, + /// null otherwise. + /// + public LinkGenerationTemplate GetTemplateByAddress(TAddress address) + { + return GetTemplateByAddress(httpContext: null, address); + } + + /// + /// Gets a to generate a URL from the specified lookup information. + /// This template object holds information of the endpoint(s) that were found and which can later be used to + /// generate a URL using the api. + /// The lookup information is used to find endpoints using a registered 'IEndpointFinder<TAddress>'. + /// + /// The address type to look up endpoints. + /// The information used to look up endpoints for creating a template. + /// The associated with current request. + /// + /// If an endpoint(s) was found successfully, then this returns a template object representing that, + /// null otherwise. + /// + public abstract LinkGenerationTemplate GetTemplateByAddress( + HttpContext httpContext, + TAddress address); } } diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGeneratorContext.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkOptions.cs similarity index 74% rename from src/Microsoft.AspNetCore.Routing.Abstractions/LinkGeneratorContext.cs rename to src/Microsoft.AspNetCore.Routing.Abstractions/LinkOptions.cs index c1e04c17e0..a5bd0547f0 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/LinkGeneratorContext.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/LinkOptions.cs @@ -1,21 +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. -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; - namespace Microsoft.AspNetCore.Routing { - public class LinkGeneratorContext + public class LinkOptions { - public HttpContext HttpContext { get; set; } - - public IEnumerable Endpoints { get; set; } - - public RouteValueDictionary ExplicitValues { get; set; } - - public RouteValueDictionary AmbientValues { get; 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/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs index e9157ea578..b247291db4 100644 --- a/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Routing/CompositeEndpointDataSource.cs @@ -8,11 +8,13 @@ using System.Linq; using System.Text; using System.Threading; using Microsoft.AspNetCore.Routing.Matching; -using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Routing { + /// + /// Represents an whose values come from a collection of instances. + /// [DebuggerDisplay("{DebuggerDisplayString,nq}")] public sealed class CompositeEndpointDataSource : EndpointDataSource { @@ -34,12 +36,20 @@ namespace Microsoft.AspNetCore.Routing _lock = new object(); } + /// + /// Gets a used to signal invalidation of cached + /// instances. + /// + /// The . public override IChangeToken GetChangeToken() { EnsureInitialized(); return _consumerChangeToken; } + /// + /// Returns a read-only collection of instances. + /// public override IReadOnlyList Endpoints { get @@ -127,15 +137,18 @@ namespace Microsoft.AspNetCore.Routing var template = matcherEndpoint.RoutePattern.RawText; template = string.IsNullOrEmpty(template) ? "\"\"" : template; sb.Append(template); - sb.Append(", Required Values: new { "); - sb.Append(string.Join(", ", FormatValues(matcherEndpoint.RequiredValues))); - sb.Append(" }"); sb.Append(", Defaults: new { "); sb.Append(string.Join(", ", FormatValues(matcherEndpoint.RoutePattern.Defaults))); sb.Append(" }"); - var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); + var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata(); sb.Append(", Route Name: "); - sb.Append(routeNameMetadata?.Name); + sb.Append(routeValuesAddressMetadata?.Name); + if (routeValuesAddressMetadata?.RequiredValues != null) + { + sb.Append(", Required Values: new { "); + sb.Append(string.Join(", ", FormatValues(routeValuesAddressMetadata.RequiredValues))); + sb.Append(" }"); + } sb.Append(", Order: "); sb.Append(matcherEndpoint.Order); diff --git a/src/Microsoft.AspNetCore.Routing/DefaultEndpointDataSource.cs b/src/Microsoft.AspNetCore.Routing/DefaultEndpointDataSource.cs index b73b8fcc0b..7ec3f4c6e6 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultEndpointDataSource.cs @@ -8,15 +8,26 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Routing { + /// + /// Provides a collection of instances. + /// public sealed class DefaultEndpointDataSource : EndpointDataSource { private readonly List _endpoints; + /// + /// Initializes a new instance of the class. + /// + /// The instances that the data source will return. public DefaultEndpointDataSource(params Endpoint[] endpoints) : this((IEnumerable) endpoints) { } + /// + /// Initializes a new instance of the class. + /// + /// The instances that the data source will return. public DefaultEndpointDataSource(IEnumerable endpoints) { if (endpoints == null) @@ -28,8 +39,16 @@ namespace Microsoft.AspNetCore.Routing _endpoints.AddRange(endpoints); } + /// + /// Gets a used to signal invalidation of cached + /// instances. + /// + /// The . public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; + /// + /// Returns a read-only collection of instances. + /// public override IReadOnlyList Endpoints => _endpoints; } } diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs new file mode 100644 index 0000000000..2b432db71f --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerationTemplate.cs @@ -0,0 +1,62 @@ +// 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; +using Microsoft.AspNetCore.Routing.Matching; + +namespace Microsoft.AspNetCore.Routing +{ + internal class DefaultLinkGenerationTemplate : LinkGenerationTemplate + { + public DefaultLinkGenerationTemplate( + DefaultLinkGenerator linkGenerator, + IEnumerable endpoints, + HttpContext httpContext, + RouteValueDictionary explicitValues, + RouteValueDictionary ambientValues) + { + LinkGenerator = linkGenerator; + Endpoints = endpoints; + HttpContext = httpContext; + EarlierExplicitValues = explicitValues; + AmbientValues = ambientValues; + } + + internal DefaultLinkGenerator LinkGenerator { get; } + + internal IEnumerable Endpoints { get; } + + internal HttpContext HttpContext { get; } + + internal RouteValueDictionary EarlierExplicitValues { get; } + + internal RouteValueDictionary AmbientValues { get; } + + public override string MakeUrl(object values, LinkOptions options) + { + var currentValues = new RouteValueDictionary(values); + var mergedValuesDictionary = new RouteValueDictionary(EarlierExplicitValues); + + foreach (var kvp in currentValues) + { + mergedValuesDictionary[kvp.Key] = kvp.Value; + } + + foreach (var endpoint in Endpoints) + { + var link = LinkGenerator.MakeLink( + HttpContext, + endpoint, + AmbientValues, + mergedValuesDictionary, + options); + if (link != null) + { + return link; + } + } + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs index b515643139..f8e82f074f 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs @@ -2,12 +2,14 @@ // 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.Encodings.Web; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Template; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; @@ -19,60 +21,163 @@ namespace Microsoft.AspNetCore.Routing private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' }; private readonly MatchProcessorFactory _matchProcessorFactory; private readonly ObjectPool _uriBuildingContextPool; - private readonly RouteOptions _options; private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly RouteOptions _options; public DefaultLinkGenerator( MatchProcessorFactory matchProcessorFactory, ObjectPool uriBuildingContextPool, IOptions routeOptions, - ILogger logger) + ILogger logger, + IServiceProvider serviceProvider) { _matchProcessorFactory = matchProcessorFactory; _uriBuildingContextPool = uriBuildingContextPool; _options = routeOptions.Value; _logger = logger; + _serviceProvider = serviceProvider; } - public override string GetLink(LinkGeneratorContext context) + public override bool TryGetLink( + HttpContext httpContext, + string routeName, + object values, + LinkOptions options, + out string link) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (TryGetLink(context, out var link)) - { - return link; - } - - throw new InvalidOperationException("Could not find a matching endpoint to generate a link."); + return TryGetLinkByRouteValues( + httpContext, + routeName, + values, + options, + out link); } - public override bool TryGetLink(LinkGeneratorContext context, out string link) + public override bool TryGetLinkByAddress( + HttpContext httpContext, + TAddress address, + object values, + LinkOptions options, + out string link) { - if (context == null) + return TryGetLinkByAddressInternal( + httpContext, + address, + explicitValues: values, + ambientValues: GetAmbientValues(httpContext), + options, + out link); + } + + public override LinkGenerationTemplate GetTemplate(HttpContext httpContext, string routeName, object values) + { + var ambientValues = GetAmbientValues(httpContext); + var explicitValues = new RouteValueDictionary(values); + + return GetTemplateInternal( + httpContext, + new RouteValuesAddress + { + RouteName = routeName, + ExplicitValues = explicitValues, + AmbientValues = ambientValues + }, + ambientValues, + explicitValues, + values); + } + + public override LinkGenerationTemplate GetTemplateByAddress( + HttpContext httpContext, + TAddress address) + { + return GetTemplateInternal(httpContext, address, values: null); + } + + internal string MakeLink( + HttpContext httpContext, + MatcherEndpoint endpoint, + RouteValueDictionary ambientValues, + RouteValueDictionary explicitValues, + LinkOptions options) + { + var templateBinder = new TemplateBinder( + UrlEncoder.Default, + _uriBuildingContextPool, + new RouteTemplate(endpoint.RoutePattern), + new RouteValueDictionary(endpoint.RoutePattern.Defaults)); + + var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata(); + var templateValuesResult = templateBinder.GetValues( + ambientValues: ambientValues, + explicitValues: explicitValues, + requiredKeys: routeValuesAddressMetadata?.RequiredValues.Keys); + if (templateValuesResult == null) { - throw new ArgumentNullException(nameof(context)); + // We're missing one of the required values for this route. + return null; } + if (!MatchesConstraints(httpContext, endpoint, templateValuesResult.CombinedValues)) + { + return null; + } + + var url = templateBinder.BindValues(templateValuesResult.AcceptedValues); + return Normalize(url, options); + } + + private bool TryGetLinkByRouteValues( + HttpContext httpContext, + string routeName, + object values, + LinkOptions options, + out string link) + { + var ambientValues = GetAmbientValues(httpContext); + + var address = new RouteValuesAddress + { + RouteName = routeName, + ExplicitValues = new RouteValueDictionary(values), + AmbientValues = ambientValues + }; + + return TryGetLinkByAddressInternal( + httpContext, + address, + explicitValues: values, + ambientValues: ambientValues, + options, + out link); + } + + private bool TryGetLinkByAddressInternal( + HttpContext httpContext, + TAddress address, + object explicitValues, + RouteValueDictionary ambientValues, + LinkOptions options, + out string link) + { link = null; - if (context.Endpoints == null) + var endpoints = FindEndpoints(address); + if (endpoints == null) { return false; } - var matcherEndpoints = context.Endpoints.OfType(); - if (!matcherEndpoints.Any()) + foreach (var endpoint in endpoints) { - //todo:log here - return false; - } + link = MakeLink( + httpContext, + endpoint, + ambientValues, + new RouteValueDictionary(explicitValues), + options); - foreach (var endpoint in matcherEndpoints) - { - link = GetLink(endpoint, context); if (link != null) { return true; @@ -82,32 +187,47 @@ namespace Microsoft.AspNetCore.Routing return false; } - private string GetLink(MatcherEndpoint endpoint, LinkGeneratorContext context) + private LinkGenerationTemplate GetTemplateInternal( + HttpContext httpContext, + TAddress address, + object values) { - var templateBinder = new TemplateBinder( - UrlEncoder.Default, - _uriBuildingContextPool, - new RouteTemplate(endpoint.RoutePattern), - new RouteValueDictionary(endpoint.RoutePattern.Defaults)); - - var templateValuesResult = templateBinder.GetValues( - ambientValues: context.AmbientValues, - explicitValues: context.ExplicitValues, - endpoint.RequiredValues.Keys); - - if (templateValuesResult == null) - { - // We're missing one of the required values for this route. - return null; - } - - if (!MatchesConstraints(context.HttpContext, endpoint, templateValuesResult.CombinedValues)) + var endpoints = FindEndpoints(address); + if (endpoints == null) { return null; } - var url = templateBinder.BindValues(templateValuesResult.AcceptedValues); - return Normalize(context, url); + var ambientValues = GetAmbientValues(httpContext); + var explicitValues = new RouteValueDictionary(values); + + return new DefaultLinkGenerationTemplate( + this, + endpoints, + httpContext, + explicitValues, + ambientValues); + } + + private LinkGenerationTemplate GetTemplateInternal( + HttpContext httpContext, + TAddress address, + RouteValueDictionary ambientValues, + RouteValueDictionary explicitValues, + object values) + { + var endpoints = FindEndpoints(address); + if (endpoints == null) + { + return null; + } + + return new DefaultLinkGenerationTemplate( + this, + endpoints, + httpContext, + explicitValues, + ambientValues); } private bool MatchesConstraints( @@ -138,13 +258,11 @@ namespace Microsoft.AspNetCore.Routing return true; } - private string Normalize(LinkGeneratorContext context, string url) + private string Normalize(string url, LinkOptions options) { - var lowercaseUrls = context.LowercaseUrls.HasValue ? context.LowercaseUrls.Value : _options.LowercaseUrls; - var lowercaseQueryStrings = context.LowercaseQueryStrings.HasValue ? - context.LowercaseQueryStrings.Value : _options.LowercaseQueryStrings; - var appendTrailingSlash = context.AppendTrailingSlash.HasValue ? - context.AppendTrailingSlash.Value : _options.AppendTrailingSlash; + var lowercaseUrls = options?.LowercaseUrls ?? _options.LowercaseUrls; + var lowercaseQueryStrings = options?.LowercaseQueryStrings ?? _options.LowercaseQueryStrings; + var appendTrailingSlash = options?.AppendTrailingSlash ?? _options.AppendTrailingSlash; if (!string.IsNullOrEmpty(url) && (lowercaseUrls || appendTrailingSlash)) { @@ -179,5 +297,36 @@ namespace Microsoft.AspNetCore.Routing return url; } + + private RouteValueDictionary GetAmbientValues(HttpContext httpContext) + { + if (httpContext != null) + { + var feature = httpContext.Features.Get(); + if (feature != null) + { + return feature.Values; + } + } + return new RouteValueDictionary(); + } + + private IEnumerable FindEndpoints(TAddress address) + { + var finder = _serviceProvider.GetRequiredService>(); + var endpoints = finder.FindEndpoints(address); + if (endpoints == null) + { + return null; + } + + var matcherEndpoints = endpoints.OfType(); + if (!matcherEndpoints.Any()) + { + return null; + } + + return matcherEndpoints; + } } } diff --git a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs index 42433f64c0..19f3d0e77a 100644 --- a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -67,7 +67,6 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddTransient(); // Link generation related services - services.TryAddSingleton, NameBasedEndpointFinder>(); services.TryAddSingleton, RouteValuesBasedEndpointFinder>(); services.TryAddSingleton(); diff --git a/src/Microsoft.AspNetCore.Routing/EndpointDataSource.cs b/src/Microsoft.AspNetCore.Routing/EndpointDataSource.cs index b5a1c40ebe..6ae887d0a0 100644 --- a/src/Microsoft.AspNetCore.Routing/EndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Routing/EndpointDataSource.cs @@ -6,10 +6,21 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Routing { + /// + /// Provides a collection of instances. + /// public abstract class EndpointDataSource { + /// + /// Gets a used to signal invalidation of cached + /// instances. + /// + /// The . public abstract IChangeToken GetChangeToken(); + /// + /// Returns a read-only collection of instances. + /// public abstract IReadOnlyList Endpoints { get; } } } diff --git a/src/Microsoft.AspNetCore.Routing/EndpointOptions.cs b/src/Microsoft.AspNetCore.Routing/EndpointOptions.cs index bd03d3c7a2..8f364d6eaa 100644 --- a/src/Microsoft.AspNetCore.Routing/EndpointOptions.cs +++ b/src/Microsoft.AspNetCore.Routing/EndpointOptions.cs @@ -5,7 +5,8 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Routing { - public class EndpointOptions + // 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/Microsoft.AspNetCore.Routing/HttpMethodMetadata.cs b/src/Microsoft.AspNetCore.Routing/HttpMethodMetadata.cs new file mode 100644 index 0000000000..f01bf2a2a5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/HttpMethodMetadata.cs @@ -0,0 +1,64 @@ +// 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.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Represents HTTP method metadata used during routing. + /// + [DebuggerDisplay("{DebuggerToString(),nq}")] + public sealed class HttpMethodMetadata : IHttpMethodMetadata + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The HTTP methods used during routing. + /// An empty collection means any HTTP method will be accepted. + /// + public HttpMethodMetadata(IEnumerable httpMethods) + : this(httpMethods, acceptCorsPreflight: false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The HTTP methods used during routing. + /// An empty collection means any HTTP method will be accepted. + /// + /// A value indicating whether routing accepts CORS preflight requests. + public HttpMethodMetadata(IEnumerable httpMethods, bool acceptCorsPreflight) + { + if (httpMethods == null) + { + throw new ArgumentNullException(nameof(httpMethods)); + } + + HttpMethods = httpMethods.ToArray(); + AcceptCorsPreflight = acceptCorsPreflight; + } + + /// + /// Returns a value indicating whether the associated endpoint should accept CORS preflight requests. + /// + public bool AcceptCorsPreflight { get; } + + /// + /// Returns a read-only collection of HTTP methods used during routing. + /// An empty collection means any HTTP method will be accepted. + /// + public IReadOnlyList HttpMethods { get; } + + private string DebuggerToString() + { + return $"HttpMethods: {string.Join(",", HttpMethods)} - Cors: {AcceptCorsPreflight}"; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/IEndpointFinderOfT.cs b/src/Microsoft.AspNetCore.Routing/IEndpointFinderOfT.cs index ba450f1c7b..618220a6e9 100644 --- a/src/Microsoft.AspNetCore.Routing/IEndpointFinderOfT.cs +++ b/src/Microsoft.AspNetCore.Routing/IEndpointFinderOfT.cs @@ -5,8 +5,17 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Routing { + /// + /// Defines a contract to find endpoints based on the supplied lookup information. + /// + /// The address type to look up endpoints. public interface IEndpointFinder { + /// + /// Finds endpoints based on the supplied lookup information. + /// + /// The information used to look up endpoints. + /// A collection of . IEnumerable FindEndpoints(TAddress address); } } diff --git a/src/Microsoft.AspNetCore.Routing/IHttpMethodMetadata.cs b/src/Microsoft.AspNetCore.Routing/IHttpMethodMetadata.cs new file mode 100644 index 0000000000..af67c6e952 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/IHttpMethodMetadata.cs @@ -0,0 +1,24 @@ +// 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 +{ + /// + /// Represents HTTP method metadata used during routing. + /// + public interface IHttpMethodMetadata + { + /// + /// Returns a value indicating whether the associated endpoint should accept CORS preflight requests. + /// + bool AcceptCorsPreflight { get; } + + /// + /// Returns a read-only collection of HTTP methods used during routing. + /// An empty collection means any HTTP method will be accepted. + /// + IReadOnlyList HttpMethods { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/IRouteNameMetadata.cs b/src/Microsoft.AspNetCore.Routing/IRouteNameMetadata.cs deleted file mode 100644 index efc1136904..0000000000 --- a/src/Microsoft.AspNetCore.Routing/IRouteNameMetadata.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Routing -{ - public interface IRouteNameMetadata - { - string Name { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/INameMetadata.cs b/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs similarity index 62% rename from src/Microsoft.AspNetCore.Routing/INameMetadata.cs rename to src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs index c38ae7f0e5..0d60e5db92 100644 --- a/src/Microsoft.AspNetCore.Routing/INameMetadata.cs +++ b/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs @@ -1,10 +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.Collections.Generic; + namespace Microsoft.AspNetCore.Routing { - public interface INameMetadata + public interface IRouteValuesAddressMetadata { string Name { get; } + IReadOnlyDictionary RequiredValues { get; } } } diff --git a/src/Microsoft.AspNetCore.Routing/ISuppressLinkGenerationMetadata.cs b/src/Microsoft.AspNetCore.Routing/ISuppressLinkGenerationMetadata.cs index 2e6a7da5d8..4725b1a8c2 100644 --- a/src/Microsoft.AspNetCore.Routing/ISuppressLinkGenerationMetadata.cs +++ b/src/Microsoft.AspNetCore.Routing/ISuppressLinkGenerationMetadata.cs @@ -3,7 +3,11 @@ namespace Microsoft.AspNetCore.Routing { + /// + /// Represents metadata used during link generation. + /// The associated endpoint will not be considered for link generation. + /// public interface ISuppressLinkGenerationMetadata { } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing/Matching/CandidateSet.cs b/src/Microsoft.AspNetCore.Routing/Matching/CandidateSet.cs index dc51752923..be134bf6ce 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/CandidateSet.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/CandidateSet.cs @@ -6,6 +6,11 @@ using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// Represents a set of candidates that have been matched + /// by the routing system. Used by implementations of + /// and . + /// public sealed class CandidateSet { // We inline storage for 4 candidates here to avoid allocations in common @@ -18,8 +23,18 @@ namespace Microsoft.AspNetCore.Routing.Matching private CandidateState[] _additionalCandidates; - // Provided to make testing possible/easy for someone implementing - // an EndpointSelector. + /// + /// + /// Initializes a new instances of the candidate set structure with the provided list of endpoints + /// and associated scores. + /// + /// + /// The constructor is provided to enable unit tests of implementations of + /// and . + /// + /// + /// The list of endpoints, sorted in descending priority order. + /// The list of endpoint scores. . public CandidateSet(MatcherEndpoint[] endpoints, int[] scores) { Count = endpoints.Length; @@ -112,12 +127,25 @@ namespace Microsoft.AspNetCore.Routing.Matching } } + /// + /// Gets the count of candidates in the set. + /// public int Count { get; } - // Note that this is a ref-return because of both mutability and performance. - // We don't want to copy these fat structs if it can be avoided. + /// + /// Gets the associated with the candidate + /// at . + /// + /// The candidate index. + /// + /// A reference to the . The result is returned by reference + /// and intended to be mutated. + /// public ref CandidateState this[int index] { + // Note that this is a ref-return because of both mutability and performance. + // We don't want to copy these fat structs if it can be avoided. + // PERF: Force inlining [MethodImpl(MethodImplOptions.AggressiveInlining)] get diff --git a/src/Microsoft.AspNetCore.Routing/Matching/CandidateState.cs b/src/Microsoft.AspNetCore.Routing/Matching/CandidateState.cs index 4cb525bfac..cf27c04c78 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/CandidateState.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/CandidateState.cs @@ -3,6 +3,9 @@ namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// The mutable state associated with a candidate in a . + /// public struct CandidateState { internal CandidateState(MatcherEndpoint endpoint, int score) @@ -14,12 +17,39 @@ namespace Microsoft.AspNetCore.Routing.Matching Values = null; } + /// + /// Gets the . + /// public MatcherEndpoint Endpoint { get; } + /// + /// Gets the score of the within the current + /// . + /// + /// + /// + /// Candidates within a set are ordered in priority order and then assigned a + /// sequential score value based on that ordering. Candiates with the same + /// score are considered to have equal priority. + /// + /// + /// The score values are used in the to determine + /// whether a set of matching candidates is an ambiguous match. + /// + /// public int Score { get; } + /// + /// Gets or sets a value which indicates where the is considered + /// a valid candiate for the current request. Set this value to false to exclude an + /// from consideration. + /// public bool IsValidCandidate { get; set; } + /// + /// Gets or sets the associated with the + /// and the current request. + /// public RouteValueDictionary Values { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matching/EndpointMetadataComparer.cs b/src/Microsoft.AspNetCore.Routing/Matching/EndpointMetadataComparer.cs index 6c617f5d03..b2a7b9ec00 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/EndpointMetadataComparer.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/EndpointMetadataComparer.cs @@ -6,10 +6,30 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// A base class for implementations that use + /// a specific type of metadata from for comparison. + /// Useful for implementing . + /// + /// + /// The type of metadata to compare. Typically this is a type of metadata related + /// to the application concern being handled. + /// public abstract class EndpointMetadataComparer : IComparer where TMetadata : class { public static readonly EndpointMetadataComparer Default = new DefaultComparer(); + /// + /// Compares two objects and returns a value indicating whether one is less than, equal to, + /// or greater than the other. + /// + /// The first object to compare. + /// The second object to compare. + /// + /// An implementation of this method must return a value less than zero if + /// x is less than y, zero if x is equal to y, or a value greater than zero if x is + /// greater than y. + /// public int Compare(Endpoint x, Endpoint y) { if (x == null) @@ -25,11 +45,32 @@ namespace Microsoft.AspNetCore.Routing.Matching return CompareMetadata(GetMetadata(x), GetMetadata(y)); } + /// + /// Gets the metadata of type from the provided endpoint. + /// + /// The . + /// The instance or null. protected virtual TMetadata GetMetadata(Endpoint endpoint) { return endpoint.Metadata.GetMetadata(); } + /// + /// Compares two instances. + /// + /// The first object to compare. + /// The second object to compare. + /// + /// An implementation of this method must return a value less than zero if + /// x is less than y, zero if x is equal to y, or a value greater than zero if x is + /// greater than y. + /// + /// + /// The base-class implementation of this method will compare metadata based on whether + /// or not they are null. The effect of this is that when endpoints are being + /// compared, the endpoint that defines an instance of + /// will be considered higher priority. + /// protected virtual int CompareMetadata(TMetadata x, TMetadata y) { // The default policy is that if x endpoint defines TMetadata, and diff --git a/src/Microsoft.AspNetCore.Routing/Matching/EndpointSelector.cs b/src/Microsoft.AspNetCore.Routing/Matching/EndpointSelector.cs index 0af172bdca..436c3df6f5 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/EndpointSelector.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/EndpointSelector.cs @@ -6,8 +6,25 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// A service that is responsible for the final selection + /// decision. To use a custom register an implementation + /// of in the dependency injection container as a singleton. + /// public abstract class EndpointSelector { + /// + /// Asynchronously selects an from the . + /// + /// The associated with the current request. + /// The associated with the current request. + /// The . + /// A that completes asynchronously once endpoint selection is complete. + /// + /// An should assign the , + /// , and properties + /// once an endpoint is selected. + /// public abstract Task SelectAsync( HttpContext httpContext, IEndpointFeature feature, diff --git a/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs index 315c3d8409..0be502b1a2 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs @@ -6,13 +6,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// An that implements filtering and selection by + /// the HTTP method of a request. + /// public sealed class HttpMethodMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy { // Used in tests @@ -26,12 +29,23 @@ namespace Microsoft.AspNetCore.Routing.Matching // Used in tests internal const string AnyMethod = "*"; + /// + /// For framework use only. + /// public IComparer Comparer => new HttpMethodMetadataEndpointComparer(); // The order value is chosen to be less than 0, so that it comes before naively // written policies. + /// + /// For framework use only. + /// public override int Order => -1000; + /// + /// For framework use only. + /// + /// + /// public bool AppliesToNode(IReadOnlyList endpoints) { if (endpoints == null) @@ -50,6 +64,11 @@ namespace Microsoft.AspNetCore.Routing.Matching return false; } + /// + /// For framework use only. + /// + /// + /// public IReadOnlyList GetEdges(IReadOnlyList endpoints) { // The algorithm here is designed to be preserve the order of the endpoints @@ -181,6 +200,12 @@ namespace Microsoft.AspNetCore.Routing.Matching } } + /// + /// For framework use only. + /// + /// + /// + /// public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) { var destinations = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -232,7 +257,6 @@ namespace Microsoft.AspNetCore.Routing.Matching return Task.CompletedTask; }, RoutePatternFactory.Parse("/"), - new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, Http405EndpointDisplayName); diff --git a/src/Microsoft.AspNetCore.Routing/Matching/IEndpointComparerPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matching/IEndpointComparerPolicy.cs index 9c187a6fec..a1010c181c 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/IEndpointComparerPolicy.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/IEndpointComparerPolicy.cs @@ -5,8 +5,30 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// A interface that can be implemented to sort + /// endpoints. Implementations of must + /// inherit from and should be registered in + /// the dependency injection container as singleton services of type . + /// + /// + /// + /// Candidates in a are sorted based on their priority. Defining + /// a adds an additional criterion to the sorting + /// operation used to order candidates. + /// + /// + /// As an example, the implementation of implements + /// to ensure that endpoints matching specific HTTP + /// methods are sorted with a higher priority than endpoints without a specific HTTP method + /// requirement. + /// + /// public interface IEndpointComparerPolicy { + /// + /// Gets an that will be used to sort the endpoints. + /// IComparer Comparer { get; } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matching/IEndpointSelectorPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matching/IEndpointSelectorPolicy.cs index 63fe7dd8fd..7dfd04b7bd 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/IEndpointSelectorPolicy.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/IEndpointSelectorPolicy.cs @@ -5,8 +5,26 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// A interface that can implemented to filter endpoints + /// in a . Implementations of must + /// inherit from and should be registered in + /// the dependency injection container as singleton services of type . + /// public interface IEndpointSelectorPolicy { + /// + /// Applies the policy to the . + /// + /// + /// The associated with the current request. + /// + /// The . + /// + /// Implementations of should implement this method + /// and filter the set of candidates in the by setting + /// to false where desired. + /// void Apply(HttpContext httpContext, CandidateSet candidates); } } diff --git a/src/Microsoft.AspNetCore.Routing/Matching/MatchProcessorBase.cs b/src/Microsoft.AspNetCore.Routing/Matching/MatchProcessorBase.cs deleted file mode 100644 index 9ac2196671..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Matching/MatchProcessorBase.cs +++ /dev/null @@ -1,42 +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.Http; - -namespace Microsoft.AspNetCore.Routing.Matching -{ - public abstract class MatchProcessorBase : MatchProcessor - { - public string ParameterName { get; private set; } - - public string ConstraintArgument { get; private set; } - - public override void Initialize(string parameterName, string constraintArgument) - { - ParameterName = parameterName; - ConstraintArgument = constraintArgument; - } - - public abstract bool Process(object value); - - public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary values) - { - return Process(values); - } - - public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values) - { - return Process(values); - } - - private bool Process(RouteValueDictionary values) - { - if (!values.TryGetValue(ParameterName, out var value) || value == null) - { - return false; - } - - return Process(value); - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/Matching/MatcherEndpoint.cs b/src/Microsoft.AspNetCore.Routing/Matching/MatcherEndpoint.cs index 9dc34cac6a..ff5067cb81 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/MatcherEndpoint.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/MatcherEndpoint.cs @@ -9,6 +9,9 @@ using Microsoft.AspNetCore.Routing.Patterns; namespace Microsoft.AspNetCore.Routing.Matching { + /// + /// Represents an that can be used in URL matching or URL generation. + /// public sealed class MatcherEndpoint : Endpoint { internal static readonly Func EmptyInvoker = (next) => @@ -16,10 +19,19 @@ namespace Microsoft.AspNetCore.Routing.Matching return (context) => Task.CompletedTask; }; + /// + /// Initializes a new instance of the class. + /// + /// The delegate to invoke to create a . + /// The to use in URL matching. + /// The order assigned to the endpoint. + /// + /// The or metadata associated with the endpoint. + /// + /// The informational display name of the endpoint. public MatcherEndpoint( Func invoker, RoutePattern routePattern, - RouteValueDictionary requiredValues, int order, EndpointMetadataCollection metadata, string displayName) @@ -37,17 +49,26 @@ namespace Microsoft.AspNetCore.Routing.Matching Invoker = invoker; RoutePattern = routePattern; - RequiredValues = requiredValues; Order = order; } + /// + /// Gets the invoker. The invoker is a delegate used to create a . + /// public Func Invoker { get; } + /// + /// Gets the order value of endpoint. + /// + /// + /// The order value provides absolute control over the priority + /// of an endpoint. Endpoints with a lower numeric value of order have higher priority. + /// public int Order { get; } - // Values required by an endpoint for it to be successfully matched on link generation - public IReadOnlyDictionary RequiredValues { get; } - + /// + /// Gets the associated with the endpoint. + /// public RoutePattern RoutePattern { get; } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matching/MatcherPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matching/MatcherPolicy.cs index 92d715c936..05a654efd9 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/MatcherPolicy.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/MatcherPolicy.cs @@ -1,10 +1,28 @@ // 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.Matching; + namespace Microsoft.AspNetCore.Routing { + /// + /// Defines a policy that applies behaviors to the URL matcher. Implementations + /// of and related interfaces must be registered + /// in the dependency injection container as singleton services of type + /// . + /// + /// + /// implementations can implement the following + /// interfaces , , + /// and . + /// public abstract class MatcherPolicy { + /// + /// Gets a value that determines the order the should + /// be applied. Policies are applied in ascending numeric value of the + /// property. + /// public abstract int Order { get; } } } diff --git a/src/Microsoft.AspNetCore.Routing/Metadata/HttpMethodMetadata.cs b/src/Microsoft.AspNetCore.Routing/Metadata/HttpMethodMetadata.cs deleted file mode 100644 index af342a2f36..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Metadata/HttpMethodMetadata.cs +++ /dev/null @@ -1,39 +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.Diagnostics; -using System.Linq; - -namespace Microsoft.AspNetCore.Routing.Metadata -{ - [DebuggerDisplay("{DebuggerToString(),nq}")] - public sealed class HttpMethodMetadata : IHttpMethodMetadata - { - public HttpMethodMetadata(IEnumerable httpMethods) - : this(httpMethods, acceptCorsPreflight: false) - { - } - - public HttpMethodMetadata(IEnumerable httpMethods, bool acceptCorsPreflight) - { - if (httpMethods == null) - { - throw new ArgumentNullException(nameof(httpMethods)); - } - - HttpMethods = httpMethods.ToArray(); - AcceptCorsPreflight = acceptCorsPreflight; - } - - public bool AcceptCorsPreflight { get; } - - public IReadOnlyList HttpMethods { get; } - - private string DebuggerToString() - { - return $"HttpMethods: {string.Join(",", HttpMethods)} - Cors: {AcceptCorsPreflight}"; - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/Metadata/IHttpMethodMetadata.cs b/src/Microsoft.AspNetCore.Routing/Metadata/IHttpMethodMetadata.cs deleted file mode 100644 index 9d6447d1e0..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Metadata/IHttpMethodMetadata.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information - -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Routing.Metadata -{ - public interface IHttpMethodMetadata - { - bool AcceptCorsPreflight { get; } - - IReadOnlyList HttpMethods { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/NameBasedEndpointFinder.cs b/src/Microsoft.AspNetCore.Routing/NameBasedEndpointFinder.cs deleted file mode 100644 index b10c2b7934..0000000000 --- a/src/Microsoft.AspNetCore.Routing/NameBasedEndpointFinder.cs +++ /dev/null @@ -1,46 +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.Routing.Matching; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Routing -{ - internal class NameBasedEndpointFinder : IEndpointFinder - { - private readonly CompositeEndpointDataSource _endpointDatasource; - private readonly ILogger _logger; - - public NameBasedEndpointFinder( - CompositeEndpointDataSource endpointDataSource, - ILogger logger) - { - _endpointDatasource = endpointDataSource; - _logger = logger; - } - - public IEnumerable FindEndpoints(string endpointName) - { - if (string.IsNullOrEmpty(endpointName)) - { - return Array.Empty(); - } - - var endpoints = _endpointDatasource.Endpoints.OfType(); - - foreach (var endpoint in endpoints) - { - var nameMetadata = endpoint.Metadata.GetMetadata(); - if (nameMetadata != null && - string.Equals(endpointName, nameMetadata.Name, StringComparison.OrdinalIgnoreCase)) - { - return new[] { endpoint }; - } - } - return Array.Empty(); - } - } -} diff --git a/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs b/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs new file mode 100644 index 0000000000..6d110a70d6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs @@ -0,0 +1,48 @@ +// 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.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Routing +{ + [DebuggerDisplay("{DebuggerToString(),nq}")] + public sealed class RouteValuesAddressMetadata : IRouteValuesAddressMetadata + { + public RouteValuesAddressMetadata(string name, IReadOnlyDictionary requiredValues) + { + Name = name; + RequiredValues = requiredValues; + } + + public string Name { get; } + + public IReadOnlyDictionary RequiredValues { get; } + + internal string DebuggerToString() + { + return $"Name: {Name} - Required values: {string.Join(", ", FormatValues(RequiredValues))}"; + + IEnumerable FormatValues(IEnumerable> values) + { + if (values == null) + { + return Array.Empty(); + } + + return values.Select( + kvp => + { + var value = "null"; + if (kvp.Value != null) + { + value = "\"" + kvp.Value.ToString() + "\""; + } + return kvp.Key + " = " + value; + }); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs b/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs index 19a24fe439..2821c73aaf 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteValuesBasedEndpointFinder.cs @@ -137,16 +137,16 @@ namespace Microsoft.AspNetCore.Routing private OutboundRouteEntry CreateOutboundRouteEntry(MatcherEndpoint endpoint) { - var routeNameMetadata = endpoint.Metadata.GetMetadata(); + var routeValuesAddressMetadata = endpoint.Metadata.GetMetadata(); var entry = new OutboundRouteEntry() { Handler = NullRouter.Instance, Order = endpoint.Order, Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern), - RequiredLinkValues = new RouteValueDictionary(endpoint.RequiredValues), + RequiredLinkValues = new RouteValueDictionary(routeValuesAddressMetadata?.RequiredValues), RouteTemplate = new RouteTemplate(endpoint.RoutePattern), Data = endpoint, - RouteName = routeNameMetadata?.Name, + RouteName = routeValuesAddressMetadata?.Name, }; entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); return entry; diff --git a/src/Microsoft.AspNetCore.Routing/SuppressLinkGenerationMetadata.cs b/src/Microsoft.AspNetCore.Routing/SuppressLinkGenerationMetadata.cs new file mode 100644 index 0000000000..88efd9077f --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/SuppressLinkGenerationMetadata.cs @@ -0,0 +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. + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Represents metadata used during link generation. + /// The associated endpoint will not be considered for link generation. + /// + public sealed class SuppressLinkGenerationMetadata : ISuppressLinkGenerationMetadata + { + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs index dad43417ab..d04f0d96de 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/CompositeEndpointDataSourceTest.cs @@ -149,14 +149,12 @@ namespace Microsoft.AspNetCore.Routing private MatcherEndpoint CreateEndpoint( string template, object defaults = null, - object requiredValues = null, int order = 0, string routeName = null) { return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template, defaults, constraints: null), - new RouteValueDictionary(requiredValues), order, EndpointMetadataCollection.Empty, null); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index 6c5a47116e..84e9701cd8 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -9,6 +9,8 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.TestObjects; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; @@ -24,17 +26,10 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress(new { controller = "Home" }); + var linkGenerator = CreateLinkGenerator(endpoint); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(new { controller = "Home" }); // Assert Assert.Equal("/Home", link); @@ -46,18 +41,11 @@ namespace Microsoft.AspNetCore.Routing // Arrange var expectedMessage = "Could not find a matching endpoint to generate a link."; var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress(new { controller = "Home" }); + var linkGenerator = CreateLinkGenerator(endpoint); // Act & Assert var exception = Assert.Throws( - () => linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - })); + () => linkGenerator.GetLink(new { controller = "Home" })); Assert.Equal(expectedMessage, exception.Message); } @@ -66,17 +54,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress(new { controller = "Home" }); + var linkGenerator = CreateLinkGenerator(endpoint); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + new { controller = "Home" }, out var link); // Assert @@ -91,17 +73,10 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{id?}"); var endpoint2 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var endpoint3 = EndpointFactory.CreateMatcherEndpoint("{controller}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress(new { controller = "Home", action = "Index", id = "10" }); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2, endpoint3); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint1, endpoint2, endpoint3 }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(new { controller = "Home", action = "Index", id = "10" }); // Assert Assert.Equal("/Home/Index/10", link); @@ -114,17 +89,10 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{id}"); var endpoint2 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var endpoint3 = EndpointFactory.CreateMatcherEndpoint("{controller}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress(new { controller = "Home", action = "Index" }); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2, endpoint3); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint1, endpoint2, endpoint3 }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(new { controller = "Home", action = "Index" }); // Assert Assert.Equal("/Home/Index", link); @@ -135,19 +103,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { name = "name with %special #characters" }, - ambientValues: new { controller = "Home", action = "Index" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { name = "name with %special #characters" }); // Assert Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", link); @@ -158,19 +118,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - new { color = new List { "red", "green", "blue" } }, - new { controller = "Home", action = "Index" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var context = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(context, new { color = new List { "red", "green", "blue" } }); // Assert Assert.Equal("/Home/Index?color=red&color=green&color=blue", link); @@ -181,19 +133,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - new { items = new List { 10, 20, 30 } }, - new { controller = "Home", action = "Index" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { items = new List { 10, 20, 30 } }); // Assert Assert.Equal("/Home/Index?items=10&items=20&items=30", link); @@ -204,19 +148,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - new { color = new List { } }, - new { controller = "Home", action = "Index" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { color = new List { } }); // Assert Assert.Equal("/Home/Index", link); @@ -227,19 +163,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }, - new { controller = "Home", action = "Index" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }); // Assert Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", link); @@ -250,19 +180,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index" }, - ambientValues: new { controller = "Home" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { action = "Index" }); // Assert Assert.Equal("/Home/Index", link); @@ -273,19 +195,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index" }, - ambientValues: new { controller = "Home" }); + var linkGenerator = CreateLinkGenerator(new[] { endpoint }, new RouteOptions() { LowercaseUrls = true }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { action = "Index" }); // Assert Assert.Equal("/home/index", link); @@ -297,19 +211,14 @@ namespace Microsoft.AspNetCore.Routing // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( + new[] { endpoint }, new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, - ambientValues: new { controller = "Home" }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }); // Assert Assert.Equal("/home/index?showstatus=true&info=detailed", link); @@ -321,19 +230,14 @@ namespace Microsoft.AspNetCore.Routing // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( + new[] { endpoint }, new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = true }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, - ambientValues: new { controller = "Home" }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }); // Assert Assert.Equal("/Home/Index?ShowStatus=True&INFO=DETAILED", link); @@ -344,19 +248,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(new RouteOptions() { AppendTrailingSlash = true }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index" }, - ambientValues: new { controller = "Home" }); + var linkGenerator = CreateLinkGenerator( + new[] { endpoint }, + new RouteOptions() { AppendTrailingSlash = true }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { action = "Index" }); // Assert Assert.Equal("/Home/Index/", link); @@ -368,19 +266,14 @@ namespace Microsoft.AspNetCore.Routing // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( + new[] { endpoint }, new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true, AppendTrailingSlash = true }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, - ambientValues: new { controller = "Home" }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }); // Assert Assert.Equal("/home/index/?showstatus=true&info=detailed", link); @@ -391,18 +284,17 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "InDex" }, - ambientValues: new { controller = "HoMe" }); + var linkGenerator = CreateLinkGenerator( + new[] { endpoint }, + new RouteOptions() { LowercaseUrls = true }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext + httpContext, + values: new { action = "InDex" }, + new LinkOptions { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues, LowercaseUrls = false }); @@ -415,18 +307,17 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = false }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "InDex" }, - ambientValues: new { controller = "HoMe" }); + var linkGenerator = CreateLinkGenerator( + new[] { endpoint }, + new RouteOptions() { LowercaseUrls = false }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext + httpContext, + values: new { action = "InDex" }, + new LinkOptions { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues, LowercaseUrls = true }); @@ -440,18 +331,16 @@ namespace Microsoft.AspNetCore.Routing // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( + new[] { endpoint }, new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, - ambientValues: new { controller = "Home" }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext + httpContext, + values: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + new LinkOptions { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues, LowercaseUrls = false, LowercaseQueryStrings = false }); @@ -466,18 +355,16 @@ namespace Microsoft.AspNetCore.Routing // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( + new[] { endpoint }, new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = false }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, - ambientValues: new { controller = "Home" }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext + httpContext, + values: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + new LinkOptions { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues, LowercaseUrls = true, LowercaseQueryStrings = true }); @@ -491,18 +378,17 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(new RouteOptions() { AppendTrailingSlash = false }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index" }, - ambientValues: new { controller = "Home" }); + var linkGenerator = CreateLinkGenerator( + new[] { endpoint }, + new RouteOptions() { AppendTrailingSlash = false }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext + httpContext, + values: new { action = "Index" }, + new LinkOptions { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues, AppendTrailingSlash = true }); @@ -514,23 +400,17 @@ namespace Microsoft.AspNetCore.Routing public void RouteGenerationRejectsConstraints() { // Arrange - var address = CreateRouteValuesAddress(new { p1 = "abcd" }); - var linkGenerator = CreateLinkGenerator(); - var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = "\\d{4}" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { p1 = "abcd" }, out var link); // Assert @@ -541,23 +421,17 @@ namespace Microsoft.AspNetCore.Routing public void RouteGenerationAcceptsConstraints() { // Arrange - var address = CreateRouteValuesAddress(new { p1 = "hello", p2 = "1234" }); - var linkGenerator = CreateLinkGenerator(); - var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = new RegexRouteConstraint("\\d{4}"), }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { p1 = "hello", p2 = "1234" }, out var link); // Assert @@ -569,23 +443,17 @@ namespace Microsoft.AspNetCore.Routing public void RouteWithCatchAllRejectsConstraints() { // Arrange - var address = CreateRouteValuesAddress(new { p1 = "abcd" }); - var linkGenerator = CreateLinkGenerator(); - var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{*p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = new RegexRouteConstraint("\\d{4}") }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { p1 = "abcd" }, out var link); // Assert @@ -596,23 +464,17 @@ namespace Microsoft.AspNetCore.Routing public void RouteWithCatchAllAcceptsConstraints() { // Arrange - var address = CreateRouteValuesAddress(new { p1 = "hello", p2 = "1234" }); - var linkGenerator = CreateLinkGenerator(); - var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{*p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = new RegexRouteConstraint("\\d{4}") }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { p1 = "hello", p2 = "1234" }, out var link); // Assert @@ -624,8 +486,6 @@ namespace Microsoft.AspNetCore.Routing public void GetLinkWithNonParameterConstraintReturnsUrlWithoutQueryString() { // Arrange - var address = CreateRouteValuesAddress(new { p1 = "hello", p2 = "1234" }); - var linkGenerator = CreateLinkGenerator(); var target = new Mock(); target .Setup( @@ -637,21 +497,17 @@ namespace Microsoft.AspNetCore.Routing It.IsAny())) .Returns(true) .Verifiable(); - var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = target.Object }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { p1 = "hello", p2 = "1234" }, out var link); // Assert @@ -667,28 +523,20 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var constraint = new CapturingConstraint(); - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store" }, constraints: new { c = constraint }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Store" }, + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext( ambientValues: new { controller = "Home", action = "Blog", extra = "42" }); - var expectedValues = new RouteValueDictionary( new { controller = "Home", action = "Store", extra = "42" }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { action = "Store" }, out var link); // Assert @@ -704,28 +552,17 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var constraint = new CapturingConstraint(); - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store", otherthing = "17" }, constraints: new { c = constraint }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Store" }, - ambientValues: new { controller = "Home", action = "Blog" }); - + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" }); var expectedValues = new RouteValueDictionary( new { controller = "Home", action = "Store" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { action = "Store" }); // Assert Assert.Equal("/slug/Home/Store", link); @@ -738,28 +575,17 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var constraint = new CapturingConstraint(); - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/{controller}/{action}", defaults: new { action = "Index" }, constraints: new { c = constraint, }); - - var address = CreateRouteValuesAddress( - explicitValues: new { controller = "Shopping" }, - ambientValues: new { controller = "Home", action = "Blog" }); - + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" }); var expectedValues = new RouteValueDictionary( new { controller = "Shopping", action = "Index" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { controller = "Shopping" }); // Assert Assert.Equal("/slug/Shopping", link); @@ -773,14 +599,12 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var constraint = new CapturingConstraint(); - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }, constraints: new { c = constraint, }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Store", thirdthing = "13" }, + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext( ambientValues: new { controller = "Home", action = "Blog", otherthing = "17" }); var expectedValues = new RouteValueDictionary( @@ -788,13 +612,8 @@ namespace Microsoft.AspNetCore.Routing // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Store", thirdthing = "13" }); // Assert Assert.Equal("/slug/Home/Store", link); @@ -805,24 +624,17 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_InlineConstraints_Success() { // Arrange - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", id = 4 }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(new { }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", controller = "Home", id = 4 }); // Assert Assert.Equal("/Home/Index/4", link); @@ -832,24 +644,17 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_InlineConstraints_NonMatchingvalue() { // Arrange - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id}", defaults: new { controller = "Home", action = "Index" }, constraints: new { id = "int" }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { action = "Index", controller = "Home", id = "not-an-integer" }, out var link); // Assert @@ -860,23 +665,15 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_InlineConstraints_OptionalParameter_ValuePresent() { // Arrange - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int?}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", id = 98 }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { action = "Index", controller = "Home", id = 98 }); // Assert Assert.Equal("/Home/Index/98", link); @@ -886,24 +683,15 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_InlineConstraints_OptionalParameter_ValueNotPresent() { // Arrange - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index" }, constraints: new { id = "int" }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { action = "Index", controller = "Home" }); // Assert Assert.Equal("/Home/Index", link); @@ -913,24 +701,17 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails() { // Arrange - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index" }, constraints: new { id = "int" }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { action = "Index", controller = "Home", id = "not-an-integer" }, out var link); // Assert @@ -941,24 +722,17 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_InlineConstraints_MultipleInlineConstraints() { // Arrange - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int:range(1,20)}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", id = 14 }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", controller = "Home", id = 14 }); // Assert Assert.Equal("/Home/Index/14", link); @@ -968,24 +742,17 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_InlineConstraints_CompositeInlineConstraint_Fails() { // Arrange - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int:range(1,20)}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", id = 50 }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { action = "Index", controller = "Home", id = 50 }, out var link); // Assert @@ -997,24 +764,17 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var constraint = new MaxLengthRouteConstraint(20); - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{name}", defaults: new { controller = "Home", action = "Index" }, constraints: new { name = constraint }); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", name = "products" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", controller = "Home", name = "products" }); // Assert Assert.Equal("/Home/Index/products", link); @@ -1025,18 +785,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name?}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", name = "products" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", controller = "Home", name = "products" }); // Assert Assert.Equal("/Home/Index/products", link); @@ -1047,18 +802,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name?}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", controller = "Home" }); // Assert Assert.Equal("/Home/Index", link); @@ -1071,18 +821,13 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "{controller}/{action}/{name}", defaults: new { name = "default-products" }); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", name = "products" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", controller = "Home", name = "products" }); // Assert Assert.Equal("/Home/Index/products", link); @@ -1095,18 +840,13 @@ namespace Microsoft.AspNetCore.Routing var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "{controller}/{action}/{name}", defaults: new { name = "products" }); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", controller = "Home" }); // Assert Assert.Equal("/Home/Index", link); @@ -1117,18 +857,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", name = "products", format = "json" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", controller = "Home", name = "products", format = "json" }); // Assert Assert.Equal("/Home/Index/products?format=json", link); @@ -1138,22 +873,15 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent() { // Arrange - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "{controller}/{action}/.{name?}"); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home", name = "products" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + httpContext, + new { action = "Index", controller = "Home", name = "products" }); // Assert Assert.Equal("/Home/Index/.products", link); @@ -1163,21 +891,12 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent() { // Arrange - var linkGenerator = CreateLinkGenerator(); var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/.{name?}"); - - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - HttpContext = new DefaultHttpContext(), - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { action = "Index", controller = "Home" }); // Assert Assert.Equal("/Home/Index/", link); @@ -1188,18 +907,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name?}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { action = "Index", controller = "Home" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { action = "Index", controller = "Home" }); // Assert Assert.Equal("/Home/Index", link); @@ -1210,19 +922,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("a/{b=15}/{c?}/{d?}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { }, - ambientValues: new { c = "17" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "17" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { }); // Assert Assert.Equal("/a/15/17", link); @@ -1233,19 +937,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("a/{b=15}/{c?}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { }, - ambientValues: new { c = "17" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "17" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { }); // Assert Assert.Equal("/a/15/17", link); @@ -1256,19 +952,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var endpoint = EndpointFactory.CreateMatcherEndpoint("a/{b=15}/{c?}/{d?}"); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { }, - ambientValues: new { d = "17" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { d = "17" }); // Act - var link = linkGenerator.GetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }); + var link = linkGenerator.GetLink(httpContext, new { }); // Assert Assert.Equal("/a", link); @@ -1360,19 +1048,13 @@ namespace Microsoft.AspNetCore.Routing "Products/Edit/{id}", requiredValues: requiredValues, defaults: defaults); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: explicitValues, - ambientValues: ambientValues); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new RouteValueDictionary(explicitValues), out var link); // Assert @@ -1390,19 +1072,13 @@ namespace Microsoft.AspNetCore.Routing "Products/Edit/{id}", requiredValues: new { c = "Products", a = "Edit" }, defaults: new { c = "Products", a = "Edit" }); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { c = "Products", a = "Edit" }, - ambientValues: new { c = "Products", a = "Edit", id = 10 }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { c = "Products", a = "Edit" }, out var link); // Assert @@ -1420,19 +1096,13 @@ namespace Microsoft.AspNetCore.Routing "Products/Edit/{id}", requiredValues: new { c = "Products", a = "Edit" }, defaults: new { c = "Products", a = "Edit" }); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: new { c = "Products", a = "List" }, - ambientValues: new { c = "Products", a = "Edit", id = 10 }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new { c = "Products", a = "List" }, out var link); // Assert @@ -1532,19 +1202,13 @@ namespace Microsoft.AspNetCore.Routing "Products/Edit/{id}", requiredValues: requiredValues, defaults: defaults); - var linkGenerator = CreateLinkGenerator(); - var address = CreateRouteValuesAddress( - explicitValues: explicitValues, - ambientValues: ambientValues); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues); // Act var canGenerateLink = linkGenerator.TryGetLink( - new LinkGeneratorContext - { - Endpoints = new[] { endpoint }, - ExplicitValues = address.ExplicitValues, - AmbientValues = address.AmbientValues - }, + httpContext, + new RouteValueDictionary(explicitValues), out var link); // Assert @@ -1552,27 +1216,333 @@ namespace Microsoft.AspNetCore.Routing Assert.Null(link); } - private RouteValuesAddress CreateRouteValuesAddress( - object explicitValues, - object ambientValues = null) + [Fact] + public void TryGetLinkByAddress_WithCustomAddress_CanGenerateLink() { - var address = new RouteValuesAddress(); - address.ExplicitValues = new RouteValueDictionary(explicitValues); - address.AmbientValues = new RouteValueDictionary(ambientValues); - return address; + // Arrange + var services = GetBasicServices(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton, EndpointFinderByName>()); + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "Products/Details/{id}", + requiredValues: new { controller = "Products", action = "Details" }, + defaults: new { controller = "Products", action = "Details" }); + var endpoint2 = EndpointFactory.CreateMatcherEndpoint( + "Customers/Details/{id}", + requiredValues: new { controller = "Customers", action = "Details" }, + defaults: new { controller = "Customers", action = "Details" }, + metadata: new NameMetadata("CustomerDetails")); + var linkGenerator = CreateLinkGenerator(new[] { endpoint1, endpoint2 }, new RouteOptions(), services); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var canGenerateLink = linkGenerator.TryGetLinkByAddress( + httpContext, + address: new NameMetadata("CustomerDetails"), + values: new { id = 10 }, + out var link); + + // Assert + Assert.True(canGenerateLink); + Assert.Equal("/Customers/Details/10", link); } - private LinkGenerator CreateLinkGenerator(RouteOptions routeOptions = null) + [Fact] + public void TryGetLinkByAddress_WithCustomAddress_CanGenerateLink_RespectsLinkOptions_SuppliedAtCallSite() { + // Arrange + var services = GetBasicServices(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton, EndpointFinderByName>()); + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "Products/Details/{id}", + requiredValues: new { controller = "Products", action = "Details" }, + defaults: new { controller = "Products", action = "Details" }); + var endpoint2 = EndpointFactory.CreateMatcherEndpoint( + "Customers/Details/{id}", + requiredValues: new { controller = "Customers", action = "Details" }, + defaults: new { controller = "Customers", action = "Details" }, + metadata: new NameMetadata("CustomerDetails")); + var linkGenerator = CreateLinkGenerator(new[] { endpoint1, endpoint2 }, new RouteOptions(), services); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var canGenerateLink = linkGenerator.TryGetLinkByAddress( + httpContext, + address: new NameMetadata("CustomerDetails"), + values: new { id = 10 }, + new LinkOptions + { + LowercaseUrls = true + }, + out var link); + + // Assert + Assert.True(canGenerateLink); + Assert.Equal("/customers/details/10", link); + } + + [Fact] + public void GetTemplate_ByRouteValues_ReturnsTemplate() + { + // Arrange + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "Product/Edit/{id}", + requiredValues: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, + defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }); + var linkGenerator = CreateLinkGenerator(endpoint1); + var values = new RouteValueDictionary(new { controller = "Product", action = "Edit" }); + + // Act + var template = linkGenerator.GetTemplate(values); + + // Assert + var defaultTemplate = Assert.IsType(template); + Assert.Same(linkGenerator, defaultTemplate.LinkGenerator); + Assert.Equal(new[] { endpoint1 }, defaultTemplate.Endpoints); + Assert.Equal(values, defaultTemplate.EarlierExplicitValues); + Assert.Null(defaultTemplate.HttpContext); + Assert.Empty(defaultTemplate.AmbientValues); + } + + [Fact] + public void GetTemplate_ByRouteName_ReturnsTemplate() + { + // Arrange + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "Product/Edit/{id}", + defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, + metadata: new RouteValuesAddressMetadata( + "EditProduct", + new RouteValueDictionary(new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }))); + var linkGenerator = CreateLinkGenerator(endpoint1); + + // Act + var template = linkGenerator.GetTemplate("EditProduct", values: new { }); + + // Assert + var defaultTemplate = Assert.IsType(template); + Assert.Same(linkGenerator, defaultTemplate.LinkGenerator); + Assert.Equal(new[] { endpoint1 }, defaultTemplate.Endpoints); + Assert.Empty(defaultTemplate.EarlierExplicitValues); + Assert.Null(defaultTemplate.HttpContext); + Assert.Empty(defaultTemplate.AmbientValues); + } + + [Fact] + public void GetTemplate_ByRouteName_ReturnsTemplate_WithMultipleEndpoints() + { + // Arrange + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "Product/Edit/{id}", + defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, + metadata: new RouteValuesAddressMetadata( + "default", + new RouteValueDictionary(new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }))); + var endpoint2 = EndpointFactory.CreateMatcherEndpoint( + "Product/Details/{id}", + defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, + metadata: new RouteValuesAddressMetadata( + "default", + new RouteValueDictionary(new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }))); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var template = linkGenerator.GetTemplate("default", values: new { }); + + // Assert + var defaultTemplate = Assert.IsType(template); + Assert.Same(linkGenerator, defaultTemplate.LinkGenerator); + Assert.Equal(new[] { endpoint1, endpoint2 }, defaultTemplate.Endpoints); + Assert.Empty(defaultTemplate.EarlierExplicitValues); + Assert.Null(defaultTemplate.HttpContext); + Assert.Empty(defaultTemplate.AmbientValues); + } + + [Fact] + public void GetTemplateByAddress_ByCustomAddress_ReturnsTemplate() + { + // Arrange + var services = GetBasicServices(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton, EndpointFinderByName>()); + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "Product/Edit/{id}", + requiredValues: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, + defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }); + var endpoint2 = EndpointFactory.CreateMatcherEndpoint( + "Customers/Details/{id}", + requiredValues: new { controller = "Customers", action = "Details" }, + defaults: new { controller = "Customers", action = "Details" }, + metadata: new NameMetadata("CustomerDetails")); + var linkGenerator = CreateLinkGenerator(new[] { endpoint1, endpoint2 }, new RouteOptions(), services); + + // Act + var template = linkGenerator.GetTemplateByAddress(new NameMetadata("CustomerDetails")); + + // Assert + var defaultTemplate = Assert.IsType(template); + Assert.Same(linkGenerator, defaultTemplate.LinkGenerator); + Assert.Equal(new[] { endpoint2 }, defaultTemplate.Endpoints); + Assert.Empty(defaultTemplate.EarlierExplicitValues); + Assert.Null(defaultTemplate.HttpContext); + Assert.Empty(defaultTemplate.AmbientValues); + } + + [Fact] + public void MakeUrl_Honors_LinkOptions() + { + // Arrange + var services = GetBasicServices(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton, EndpointFinderByName>()); + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "Product/Edit/{id}", + requiredValues: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, + defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }); + var endpoint2 = EndpointFactory.CreateMatcherEndpoint( + "Customers/Details/{id}", + requiredValues: new { controller = "Customers", action = "Details" }, + defaults: new { controller = "Customers", action = "Details" }, + metadata: new NameMetadata("CustomerDetails")); + var linkGenerator = CreateLinkGenerator(new[] { endpoint1, endpoint2 }, new RouteOptions(), services); + + // Act1 + var template = linkGenerator.GetTemplateByAddress(new NameMetadata("CustomerDetails")); + + // Assert1 + Assert.NotNull(template); + + // Act2 + var link = template.MakeUrl(new { id = 10 }, new LinkOptions { LowercaseUrls = true }); + + // Assert2 + Assert.Equal("/customers/details/10", link); + + // Act3 + link = template.MakeUrl(new { id = 25 }); + + // Assert3 + Assert.Equal("/Customers/Details/25", link); + } + + [Fact] + public void MakeUrl_GeneratesLink_WithExtraRouteValues() + { + // Arrange + var endpoint1 = EndpointFactory.CreateMatcherEndpoint( + "Product/Edit/{id}", + requiredValues: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }, + defaults: new { controller = "Product", action = "Edit", area = (string)null, page = (string)null }); + var linkGenerator = CreateLinkGenerator(endpoint1); + + // Act1 + var template = linkGenerator.GetTemplate( + values: new { controller = "Product", action = "Edit", foo = "bar" }); + + // Assert1 + Assert.NotNull(template); + + // Act2 + var link = template.MakeUrl(new { id = 10 }); + + // Assert2 + Assert.Equal("/Product/Edit/10?foo=bar", link); + + // Act3 + link = template.MakeUrl(new { id = 25, foo = "boo" }); + + // Assert3 + Assert.Equal("/Product/Edit/25?foo=boo", link); + } + + private LinkGenerator CreateLinkGenerator(params Endpoint[] endpoints) + { + return CreateLinkGenerator(endpoints, routeOptions: null); + } + + private LinkGenerator CreateLinkGenerator( + Endpoint[] endpoints, + RouteOptions routeOptions, + ServiceCollection services = null) + { + if (services == null) + { + services = GetBasicServices(); + } + + if (endpoints != null || endpoints.Length > 0) + { + services.Configure(o => + { + o.DataSources.Add(new DefaultEndpointDataSource(endpoints)); + }); + } + routeOptions = routeOptions ?? new RouteOptions(); var options = Options.Create(routeOptions); + var serviceProvider = services.BuildServiceProvider(); + return new DefaultLinkGenerator( - new DefaultMatchProcessorFactory( - options, - Mock.Of()), + new DefaultMatchProcessorFactory(options, serviceProvider), new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), options, - NullLogger.Instance); + NullLogger.Instance, + serviceProvider); + } + + private HttpContext CreateHttpContext(object ambientValues) + { + var httpContext = new DefaultHttpContext(); + httpContext.Features.Set(new EndpointFeature + { + Values = new RouteValueDictionary(ambientValues) + }); + return httpContext; + } + + private ServiceCollection GetBasicServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddOptions(); + services.AddRouting(); + services.AddLogging(); + return services; + } + + private class EndpointFinderByName : IEndpointFinder + { + private readonly CompositeEndpointDataSource _dataSource; + + public EndpointFinderByName(CompositeEndpointDataSource dataSource) + { + _dataSource = dataSource; + } + + public IEnumerable FindEndpoints(INameMetadata address) + { + var endpoint = _dataSource.Endpoints.SingleOrDefault(e => + { + var nameMetadata = e.Metadata.GetMetadata(); + return nameMetadata != null && string.Equals(address.Name, nameMetadata.Name); + }); + return new[] { endpoint }; + } + } + + private interface INameMetadata + { + string Name { get; } + } + + private class NameMetadata : INameMetadata + { + public NameMetadata(string name) + { + Name = name; + } + public string Name { get; } } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs index 6711ff64d6..6ff5f8b909 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; +using System; +using System.Collections.Generic; namespace Microsoft.AspNetCore.Routing { @@ -17,18 +19,17 @@ namespace Microsoft.AspNetCore.Routing string displayName = null, params object[] metadata) { - var metadataCollection = EndpointMetadataCollection.Empty; - if (metadata != null) + var d = new List(metadata ?? Array.Empty()); + if (requiredValues != null) { - metadataCollection = new EndpointMetadataCollection(metadata); + d.Add(new RouteValuesAddressMetadata(null, new RouteValueDictionary(requiredValues))); } return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template, defaults, constraints), - new RouteValueDictionary(requiredValues), order, - metadataCollection, + new EndpointMetadataCollection(d), displayName); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/CandidateSetTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/CandidateSetTest.cs index 54db9ca02d..ac78dcaf7b 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/CandidateSetTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/CandidateSetTest.cs @@ -85,7 +85,6 @@ namespace Microsoft.AspNetCore.Routing.Matching return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template), - new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, "test"); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DataSourceDependentMatcherTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DataSourceDependentMatcherTest.cs index e88c707217..07c1db9537 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DataSourceDependentMatcherTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DataSourceDependentMatcherTest.cs @@ -37,7 +37,6 @@ namespace Microsoft.AspNetCore.Routing.Matching var endpoint = new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse("a/b/c"), - new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, "test"); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultEndpointSelectorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultEndpointSelectorTest.cs index d8586f23d6..5ec095bc4e 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultEndpointSelectorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultEndpointSelectorTest.cs @@ -216,7 +216,6 @@ test: /test3", ex.Message); return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template), - new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, $"test: {template}"); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultMatchProcessorFactoryTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultMatchProcessorFactoryTest.cs index 1be24950b1..acfd78d29f 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultMatchProcessorFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DefaultMatchProcessorFactoryTest.cs @@ -61,8 +61,8 @@ namespace Microsoft.AspNetCore.Routing.Matching var factory = GetMatchProcessorFactory(); var parameter = RoutePatternFactory.ParameterPart( - "id", - @default: null, + "id", + @default: null, parameterKind: RoutePatternParameterKind.Standard, constraints: new[] { RoutePatternFactory.Constraint("int"), }); @@ -282,12 +282,38 @@ namespace Microsoft.AspNetCore.Routing.Matching } } - private class EndsWithStringMatchProcessor : MatchProcessorBase + private class EndsWithStringMatchProcessor : MatchProcessor { - public override bool Process(object value) + public string ParameterName { get; private set; } + + public string ConstraintArgument { get; private set; } + + public override void Initialize(string parameterName, string constraintArgument) { + ParameterName = parameterName; + ConstraintArgument = constraintArgument; + } + + public override bool ProcessInbound(HttpContext httpContext, RouteValueDictionary values) + { + return Process(values); + } + + public override bool ProcessOutbound(HttpContext httpContext, RouteValueDictionary values) + { + return Process(values); + } + + private bool Process(RouteValueDictionary values) + { + if (!values.TryGetValue(ParameterName, out var value) || value == null) + { + return false; + } + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return valueString.EndsWith(ConstraintArgument); + var endsWith = valueString.EndsWith(ConstraintArgument, StringComparison.OrdinalIgnoreCase); + return endsWith; } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherBuilderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherBuilderTest.cs index e712210e44..a8735c02fa 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherBuilderTest.cs @@ -860,7 +860,6 @@ namespace Microsoft.AspNetCore.Routing.Matching return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)), - new RouteValueDictionary(), 0, new EndpointMetadataCollection(metadata), "test"); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs index 7e80607008..b77650deda 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/DfaMatcherTest.cs @@ -20,7 +20,6 @@ namespace Microsoft.AspNetCore.Routing.Matching return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template, defaults, constraints: null), - new RouteValueDictionary(), order, metadata ?? EndpointMetadataCollection.Empty, template); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/HttpMethodMatcherPolicyIntegrationTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/HttpMethodMatcherPolicyIntegrationTest.cs index d3ae78485b..5d5c0eccd8 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/HttpMethodMatcherPolicyIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/HttpMethodMatcherPolicyIntegrationTest.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -343,7 +342,6 @@ namespace Microsoft.AspNetCore.Routing.Matching return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template, defaults, constraints), - new RouteValueDictionary(), order, new EndpointMetadataCollection(metadata), displayName); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/HttpMethodMatcherPolicyTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/HttpMethodMatcherPolicyTest.cs index f302bebb10..e8c7121597 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/HttpMethodMatcherPolicyTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/HttpMethodMatcherPolicyTest.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.AspNetCore.Routing.Metadata; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; using static Microsoft.AspNetCore.Routing.Matching.HttpMethodMatcherPolicy; @@ -288,7 +287,6 @@ namespace Microsoft.AspNetCore.Routing.Matching return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template), - new RouteValueDictionary(), 0, new EndpointMetadataCollection(metadata), $"test: {template}"); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs index 9e4986c543..9dbc881548 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherConformanceTest.cs @@ -43,7 +43,6 @@ namespace Microsoft.AspNetCore.Routing.Matching return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template, defaults, constraints), - new RouteValueDictionary(), order ?? 0, EndpointMetadataCollection.Empty, "endpoint: " + template); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherEndpointComparerTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherEndpointComparerTest.cs index 87bbc0c877..96539d8304 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherEndpointComparerTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/MatcherEndpointComparerTest.cs @@ -222,7 +222,6 @@ namespace Microsoft.AspNetCore.Routing.Matching return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template), - new RouteValueDictionary(), order, new EndpointMetadataCollection(metadata), "test: " + template); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs index c18b0f5f2f..25ce04bf82 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/RouteValueBasedEndpointFinderTest.cs @@ -1,6 +1,7 @@ // 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; @@ -79,29 +80,6 @@ namespace Microsoft.AspNetCore.Routing Assert.Same(endpoint3, Assert.IsType(namedMatches[1].Match.Entry.Data)); } - [Fact] - public void GetOutboundMatches_DoesNotGetNamedMatchesFor_EndpointsHaving_INameMetadata() - { - // Arrange - var endpoint1 = CreateEndpoint("/a"); - var endpoint2 = CreateEndpoint("/a", routeName: "named"); - var endpoint3 = CreateEndpoint( - "/b", - metadataCollection: new EndpointMetadataCollection(new[] { new NameMetadata("named") })); - - // Act - var finder = CreateEndpointFinder(endpoint1, endpoint2); - - // Assert - Assert.NotNull(finder.AllMatches); - Assert.Equal(2, finder.AllMatches.Count()); - Assert.NotNull(finder.NamedMatches); - Assert.True(finder.NamedMatches.TryGetValue("named", out var namedMatches)); - var namedMatch = Assert.Single(namedMatches); - var actual = Assert.IsType(namedMatch.Match.Entry.Data); - Assert.Same(endpoint2, actual); - } - [Fact] public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches() { @@ -263,40 +241,22 @@ namespace Microsoft.AspNetCore.Routing { if (metadataCollection == null) { - metadataCollection = EndpointMetadataCollection.Empty; - if (!string.IsNullOrEmpty(routeName)) + var metadata = new List(); + if (!string.IsNullOrEmpty(routeName) || requiredValues != null) { - metadataCollection = new EndpointMetadataCollection(new[] { new RouteNameMetadata(routeName) }); + metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues))); } + metadataCollection = new EndpointMetadataCollection(metadata); } return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, RoutePatternFactory.Parse(template, defaults, constraints: null), - new RouteValueDictionary(requiredValues), order, metadataCollection, null); } - private class RouteNameMetadata : IRouteNameMetadata - { - public RouteNameMetadata(string name) - { - Name = name; - } - public string Name { get; } - } - - private class NameMetadata : INameMetadata - { - public NameMetadata(string name) - { - Name = name; - } - public string Name { get; } - } - private class CustomRouteValuesBasedEndpointFinder : RouteValuesBasedEndpointFinder { public CustomRouteValuesBasedEndpointFinder( @@ -318,7 +278,5 @@ namespace Microsoft.AspNetCore.Routing return matches; } } - - private class SuppressLinkGenerationMetadata : ISuppressLinkGenerationMetadata { } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs new file mode 100644 index 0000000000..f05e763484 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.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.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteValuesAddressMetadataTests + { + [Fact] + public void DebuggerToString_NoNameAndRequiredValues_ReturnsString() + { + var metadata = new RouteValuesAddressMetadata(null, null); + + Assert.Equal("Name: - Required values: ", metadata.DebuggerToString()); + } + + [Fact] + public void DebuggerToString_HasNameAndRequiredValues_ReturnsString() + { + var metadata = new RouteValuesAddressMetadata("Name!", new Dictionary + { + ["requiredValue1"] = "One", + ["requiredValue2"] = 2, + }); + + Assert.Equal("Name: Name! - Required values: requiredValue1 = \"One\", requiredValue2 = \"2\"", metadata.DebuggerToString()); + } + } +}