Skip to content

Commit d0791a2

Browse files
kubafloCopilot
andcommitted
Preserve IOutboundParameterTransformer on optional route constraints
When a route constraint implements IOutboundParameterTransformer, wrapping it in OptionalRouteConstraint loses the transformer capability because OptionalRouteConstraint does not implement the interface. Add OptionalOutboundParameterTransformerRouteConstraint that extends OptionalRouteConstraint and delegates TransformOutbound to the inner constraint. DefaultParameterPolicyFactory.InitializeRouteConstraint now uses this wrapper when the inner constraint is a transformer. Fixes #23063 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 01129f7 commit d0791a2

File tree

3 files changed

+68
-1
lines changed

3 files changed

+68
-1
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Routing.Constraints;
5+
6+
/// <summary>
7+
/// Defines a constraint on an optional parameter whose inner constraint also implements
8+
/// <see cref="IOutboundParameterTransformer"/>. This preserves the transformer capability
9+
/// that would otherwise be lost when wrapping in <see cref="OptionalRouteConstraint"/>.
10+
/// </summary>
11+
internal sealed class OptionalOutboundParameterTransformerRouteConstraint : OptionalRouteConstraint, IOutboundParameterTransformer
12+
{
13+
/// <summary>
14+
/// Creates a new <see cref="OptionalOutboundParameterTransformerRouteConstraint"/> instance.
15+
/// </summary>
16+
/// <param name="innerConstraint">The inner constraint that also implements <see cref="IOutboundParameterTransformer"/>.</param>
17+
public OptionalOutboundParameterTransformerRouteConstraint(IRouteConstraint innerConstraint)
18+
: base(innerConstraint)
19+
{
20+
}
21+
22+
/// <inheritdoc />
23+
public string? TransformOutbound(object? value)
24+
{
25+
return ((IOutboundParameterTransformer)InnerConstraint).TransformOutbound(value);
26+
}
27+
}

src/Http/Routing/src/DefaultParameterPolicyFactory.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ private static IParameterPolicy InitializeRouteConstraint(
6464
{
6565
if (optional)
6666
{
67-
routeConstraint = new OptionalRouteConstraint(routeConstraint);
67+
routeConstraint = routeConstraint is IOutboundParameterTransformer
68+
? new OptionalOutboundParameterTransformerRouteConstraint(routeConstraint)
69+
: new OptionalRouteConstraint(routeConstraint);
6870
}
6971

7072
return routeConstraint;

src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,26 @@ public void Create_CreatesParameterPolicy_FromRoutePattern_Constraint_Optional()
127127
Assert.IsType<IntRouteConstraint>(optionalConstraint.InnerConstraint);
128128
}
129129

130+
[Fact]
131+
public void Create_CreatesParameterPolicy_FromRoutePattern_Constraint_Optional_PreservesOutboundTransformer()
132+
{
133+
// Regression test for https://github.com/dotnet/aspnetcore/issues/23063
134+
var factory = GetParameterPolicyFactory();
135+
136+
var parameter = RoutePatternFactory.ParameterPart(
137+
"id",
138+
@default: null,
139+
parameterKind: RoutePatternParameterKind.Optional,
140+
parameterPolicies: new[] { RoutePatternFactory.ParameterPolicy(new TransformingRouteConstraint()), });
141+
142+
var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]);
143+
144+
var optionalConstraint = Assert.IsType<OptionalOutboundParameterTransformerRouteConstraint>(parameterPolicy);
145+
Assert.IsType<TransformingRouteConstraint>(optionalConstraint.InnerConstraint);
146+
var transformer = Assert.IsAssignableFrom<IOutboundParameterTransformer>(parameterPolicy);
147+
Assert.Equal("hello", transformer.TransformOutbound("HELLO"));
148+
}
149+
130150
[Fact]
131151
public void Create_CreatesParameterPolicy_FromRoutePattern_ParameterPolicy()
132152
{
@@ -445,6 +465,24 @@ public bool Match(
445465
return false;
446466
}
447467
}
468+
469+
private class TransformingRouteConstraint : IRouteConstraint, IOutboundParameterTransformer
470+
{
471+
public bool Match(
472+
HttpContext httpContext,
473+
IRouter route,
474+
string routeKey,
475+
RouteValueDictionary values,
476+
RouteDirection routeDirection)
477+
{
478+
return true;
479+
}
480+
481+
public string TransformOutbound(object value)
482+
{
483+
return value?.ToString()?.ToLowerInvariant();
484+
}
485+
}
448486
}
449487

450488
public class CustomParameterPolicy : IParameterPolicy

0 commit comments

Comments
 (0)