Skip to content

Commit 6f37e8a

Browse files
rynowakjaviercnJamesNK
authored
[Mvc] Improved route value transformer (#21481)
* Apply suggestions from code review Cleaned up error messages. Thanks @JamesNK, I totally overlooked the content. Co-authored-by: James Newton-King <[email protected]> Co-authored-by: Javier Calvarro Nelson <[email protected]> Co-authored-by: James Newton-King <[email protected]>
1 parent 4734f47 commit 6f37e8a

15 files changed

+657
-55
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/Resources.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,4 +519,7 @@
519519
<data name="ValidationVisitor_ContainerCannotBeSpecified" xml:space="preserve">
520520
<value>A container cannot be specified when the ModelMetada is of kind '{0}'.</value>
521521
</data>
522-
</root>
522+
<data name="StateShouldBeNullForRouteValueTransformers" xml:space="preserve">
523+
<value>Transformer '{0}' was retrieved from dependency injection with a state value. State can only be specified when the dynamic route is mapped using MapDynamicControllerRoute's state argument together with transient lifetime transformer. Ensure that '{0}' doesn't set its own state and that the transformer is registered with a transient lifetime in dependency injection.</value>
524+
</data>
525+
</root>

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// 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

44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Threading.Tasks;
88
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Mvc.Core;
910
using Microsoft.AspNetCore.Routing;
1011
using Microsoft.AspNetCore.Routing.Matching;
1112
using Microsoft.Extensions.DependencyInjection;
@@ -97,13 +98,21 @@ public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
9798
// no realistic way this could happen.
9899
var dynamicControllerMetadata = endpoint.Metadata.GetMetadata<DynamicControllerMetadata>();
99100
var transformerMetadata = endpoint.Metadata.GetMetadata<DynamicControllerRouteValueTransformerMetadata>();
101+
102+
DynamicRouteValueTransformer transformer = null;
100103
if (dynamicControllerMetadata != null)
101104
{
102105
dynamicValues = dynamicControllerMetadata.Values;
103106
}
104107
else if (transformerMetadata != null)
105108
{
106-
var transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
109+
transformer = (DynamicRouteValueTransformer)httpContext.RequestServices.GetRequiredService(transformerMetadata.SelectorType);
110+
if (transformer.State != null)
111+
{
112+
throw new InvalidOperationException(Resources.FormatStateShouldBeNullForRouteValueTransformers(transformerMetadata.SelectorType.Name));
113+
}
114+
transformer.State = transformerMetadata.State;
115+
107116
dynamicValues = await transformer.TransformAsync(httpContext, originalValues);
108117
}
109118
else
@@ -146,6 +155,16 @@ public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
146155
}
147156
}
148157

158+
if (transformer != null)
159+
{
160+
endpoints = await transformer.FilterAsync(httpContext, values, endpoints);
161+
if (endpoints.Count == 0)
162+
{
163+
candidates.ReplaceEndpoint(i, null, null);
164+
continue;
165+
}
166+
}
167+
149168
// Update the route values
150169
candidates.ReplaceEndpoint(i, endpoint, values);
151170

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 it 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)