Skip to content

Commit 953567e

Browse files
rynowakjaviercn
authored andcommitted
Prototype of improved route value transformer
See: #21471 This change just include the product-side changes and just for controllers. This addresses most the major gaps our partners have found with dynamic route value transformers. Doesn't include anything order-related.
1 parent 406fe1b commit 953567e

4 files changed

+104
-6
lines changed

src/Mvc/Mvc.Core/src/Builder/ControllerEndpointRouteBuilderExtensions.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,36 @@ public static void MapDynamicControllerRoute<TTransformer>(this IEndpointRouteBu
473473
throw new ArgumentNullException(nameof(endpoints));
474474
}
475475

476+
MapDynamicControllerRoute<TTransformer>(endpoints, pattern, state: null);
477+
}
478+
479+
/// <summary>
480+
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will
481+
/// attempt to select a controller action using the route values produced by <typeparamref name="TTransformer"/>.
482+
/// </summary>
483+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
484+
/// <param name="pattern">The URL pattern of the route.</param>
485+
/// <param name="state">A state object to provide to the <typeparamref name="TTransformer" /> instance.</param>
486+
/// <typeparam name="TTransformer">The type of a <see cref="DynamicRouteValueTransformer"/>.</typeparam>
487+
/// <remarks>
488+
/// <para>
489+
/// This method allows the registration of a <see cref="RouteEndpoint"/> and <see cref="DynamicRouteValueTransformer"/>
490+
/// that combine to dynamically select a controller action using custom logic.
491+
/// </para>
492+
/// <para>
493+
/// The instance of <typeparamref name="TTransformer"/> will be retrieved from the dependency injection container.
494+
/// Register <typeparamref name="TTransformer"/> as transient in <c>ConfigureServices</c>. Using the transient lifetime
495+
/// is required when using <paramref name="state" />.
496+
/// </para>
497+
/// </remarks>
498+
public static void MapDynamicControllerRoute<TTransformer>(this IEndpointRouteBuilder endpoints, string pattern, object state)
499+
where TTransformer : DynamicRouteValueTransformer
500+
{
501+
if (endpoints == null)
502+
{
503+
throw new ArgumentNullException(nameof(endpoints));
504+
}
505+
476506
EnsureControllerServices(endpoints);
477507

478508
// Called for side-effect to make sure that the data source is registered.
@@ -486,7 +516,7 @@ public static void MapDynamicControllerRoute<TTransformer>(this IEndpointRouteBu
486516
})
487517
.Add(b =>
488518
{
489-
b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(typeof(TTransformer)));
519+
b.Metadata.Add(new DynamicControllerRouteValueTransformerMetadata(typeof(TTransformer), state));
490520
});
491521
}
492522

src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,17 @@ public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
9797
// no realistic way this could happen.
9898
var dynamicControllerMetadata = endpoint.Metadata.GetMetadata<DynamicControllerMetadata>();
9999
var transformerMetadata = endpoint.Metadata.GetMetadata<DynamicControllerRouteValueTransformerMetadata>();
100+
101+
DynamicRouteValueTransformer transformer = null;
100102
if (dynamicControllerMetadata != null)
101103
{
102104
dynamicValues = dynamicControllerMetadata.Values;
103105
}
104106
else if (transformerMetadata != null)
105107
{
106-
var transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
108+
transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
109+
transformer.State = transformerMetadata.State;
110+
107111
dynamicValues = await transformer.TransformAsync(httpContext, originalValues);
108112
}
109113
else
@@ -146,6 +150,16 @@ public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
146150
}
147151
}
148152

153+
if (transformer != null)
154+
{
155+
endpoints = await transformer.FilterAsync(httpContext, values, endpoints);
156+
if (endpoints.Count == 0)
157+
{
158+
candidates.ReplaceEndpoint(i, null, null);
159+
continue;
160+
}
161+
}
162+
149163
// Update the route values
150164
candidates.ReplaceEndpoint(i, endpoint, values);
151165

src/Mvc/Mvc.Core/src/Routing/DynamicControllerRouteValueTransformerMetadata.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
88
{
99
internal class DynamicControllerRouteValueTransformerMetadata : IDynamicEndpointMetadata
1010
{
11-
public DynamicControllerRouteValueTransformerMetadata(Type selectorType)
11+
public DynamicControllerRouteValueTransformerMetadata(Type selectorType, object state)
1212
{
1313
if (selectorType == null)
1414
{
@@ -23,10 +23,13 @@ public DynamicControllerRouteValueTransformerMetadata(Type selectorType)
2323
}
2424

2525
SelectorType = selectorType;
26+
State = state;
2627
}
2728

2829
public bool IsDynamic => true;
2930

3031
public Type SelectorType { get; }
32+
33+
public object State { get; }
3134
}
3235
}

src/Mvc/Mvc.Core/src/Routing/DynamicRouteValueTransformer.cs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.Collections.Generic;
45
using System.Threading.Tasks;
56
using Microsoft.AspNetCore.Http;
67
using Microsoft.AspNetCore.Routing;
@@ -20,23 +21,73 @@ namespace Microsoft.AspNetCore.Mvc.Routing
2021
/// <para>
2122
/// The route values returned from a <see cref="TransformAsync(HttpContext, RouteValueDictionary)"/> implementation
2223
/// will be used to select an action based on matching of the route values. All actions that match the route values
23-
/// will be considered as candidates, and may be further disambiguated by <see cref="IEndpointSelectorPolicy" />
24-
/// implementations such as <see cref="HttpMethodMatcherPolicy" />.
24+
/// will be considered as candidates, and may be further disambiguated by
25+
/// <see cref="FilterAsync(HttpContext, RouteValueDictionary, IReadOnlyList{Endpoint})" /> as well as
26+
/// <see cref="IEndpointSelectorPolicy" /> implementations such as <see cref="HttpMethodMatcherPolicy" />.
27+
/// </para>
28+
/// <para>
29+
/// Operations on a <see cref="DynamicRouteValueTransformer" /> instance will be called for each dynamic endpoint
30+
/// in the following sequence:
31+
///
32+
/// <list type="bullet">
33+
/// <item><description><see cref="State" /> is set</description></item>
34+
/// <item><description><see cref="TransformAsync(HttpContext, RouteValueDictionary)"/></description></item>
35+
/// <item><description><see cref="FilterAsync(HttpContext, RouteValueDictionary, IReadOnlyList{Endpoint})" /></description></item>
36+
/// </list>
37+
///
38+
/// Implementations that are registered with the service collection as transient may safely use class
39+
/// members to persist state across these operations.
2540
/// </para>
2641
/// <para>
2742
/// Implementations <see cref="DynamicRouteValueTransformer" /> should be registered with the service
2843
/// collection as type <see cref="DynamicRouteValueTransformer" />. Implementations can use any service
29-
/// lifetime.
44+
/// lifetime. Implementations that make use of <see cref="State" /> must be registered as transient.
3045
/// </para>
3146
/// </remarks>
3247
public abstract class DynamicRouteValueTransformer
3348
{
49+
/// <summary>
50+
/// Gets or sets a state value. An arbitrary value passed to the transformer from where was registered.
51+
/// </summary>
52+
/// <remarks>
53+
/// Implementations that make use of <see cref="State" /> must be registered as transient with the service
54+
/// collection.
55+
/// </remarks>
56+
public object State { get; set; }
57+
3458
/// <summary>
3559
/// Creates a set of transformed route values that will be used to select an action.
3660
/// </summary>
3761
/// <param name="httpContext">The <see cref="HttpContext" /> associated with the current request.</param>
3862
/// <param name="values">The route values associated with the current match. Implementations should not modify <paramref name="values"/>.</param>
3963
/// <returns>A task which asynchronously returns a set of route values.</returns>
4064
public abstract ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values);
65+
66+
/// <summary>
67+
/// Filters the set of endpoints that were chosen as a result of lookup based on the route values returned by
68+
/// <see cref="TransformAsync(HttpContext, RouteValueDictionary)" />.
69+
/// </summary>
70+
/// <param name="httpContext">The <see cref="HttpContext" /> associated with the current request.</param>
71+
/// <param name="values">The route values returned from <see cref="TransformAsync(HttpContext, RouteValueDictionary)" />.</param>
72+
/// <param name="endpoints">
73+
/// The endpoints that were chosen as a result of lookup based on the route values returned by
74+
/// <see cref="TransformAsync(HttpContext, RouteValueDictionary)" />.
75+
/// </param>
76+
/// <returns>Asynchronously returns a list of endpoints to apply to the matches collection.</returns>
77+
/// <remarks>
78+
/// <para>
79+
/// Implementations of <see cref="FilterAsync(HttpContext, RouteValueDictionary, IReadOnlyList{Endpoint})" /> may further
80+
/// refine the list of endpoints chosen based on route value matching by returning a new list of endpoints based on
81+
/// <paramref name="endpoints" />.
82+
/// </para>
83+
/// <para>
84+
/// <see cref="FilterAsync(HttpContext, RouteValueDictionary, IReadOnlyList{Endpoint})" /> will not be called in the case
85+
/// where zero endpoints were matched based on route values.
86+
/// </para>
87+
/// </remarks>
88+
public virtual ValueTask<IReadOnlyList<Endpoint>> FilterAsync(HttpContext httpContext, RouteValueDictionary values, IReadOnlyList<Endpoint> endpoints)
89+
{
90+
return new ValueTask<IReadOnlyList<Endpoint>>(endpoints);
91+
}
4192
}
4293
}

0 commit comments

Comments
 (0)