diff --git a/src/Http/Routing/src/Constraints/OptionalOutboundParameterTransformerRouteConstraint.cs b/src/Http/Routing/src/Constraints/OptionalOutboundParameterTransformerRouteConstraint.cs
new file mode 100644
index 000000000000..797975bc77f6
--- /dev/null
+++ b/src/Http/Routing/src/Constraints/OptionalOutboundParameterTransformerRouteConstraint.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Routing.Constraints;
+
+///
+/// Defines a constraint on an optional parameter whose inner constraint also implements
+/// . This preserves the transformer capability
+/// that would otherwise be lost when wrapping in .
+///
+internal sealed class OptionalOutboundParameterTransformerRouteConstraint : OptionalRouteConstraint, IOutboundParameterTransformer
+{
+ ///
+ /// Creates a new instance.
+ ///
+ /// The inner constraint that also implements .
+ public OptionalOutboundParameterTransformerRouteConstraint(IRouteConstraint innerConstraint)
+ : base(innerConstraint)
+ {
+ }
+
+ ///
+ public string? TransformOutbound(object? value)
+ {
+ return ((IOutboundParameterTransformer)InnerConstraint).TransformOutbound(value);
+ }
+}
diff --git a/src/Http/Routing/src/DefaultParameterPolicyFactory.cs b/src/Http/Routing/src/DefaultParameterPolicyFactory.cs
index 8f7cf63f24a9..99a67409d089 100644
--- a/src/Http/Routing/src/DefaultParameterPolicyFactory.cs
+++ b/src/Http/Routing/src/DefaultParameterPolicyFactory.cs
@@ -64,7 +64,9 @@ private static IParameterPolicy InitializeRouteConstraint(
{
if (optional)
{
- routeConstraint = new OptionalRouteConstraint(routeConstraint);
+ routeConstraint = routeConstraint is IOutboundParameterTransformer
+ ? new OptionalOutboundParameterTransformerRouteConstraint(routeConstraint)
+ : new OptionalRouteConstraint(routeConstraint);
}
return routeConstraint;
diff --git a/src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs b/src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs
index 4ea9408505bb..5266e60e4592 100644
--- a/src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs
+++ b/src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs
@@ -127,6 +127,26 @@ public void Create_CreatesParameterPolicy_FromRoutePattern_Constraint_Optional()
Assert.IsType(optionalConstraint.InnerConstraint);
}
+ [Fact]
+ public void Create_CreatesParameterPolicy_FromRoutePattern_Constraint_Optional_PreservesOutboundTransformer()
+ {
+ // Regression test for https://github.com/dotnet/aspnetcore/issues/23063
+ var factory = GetParameterPolicyFactory();
+
+ var parameter = RoutePatternFactory.ParameterPart(
+ "id",
+ @default: null,
+ parameterKind: RoutePatternParameterKind.Optional,
+ parameterPolicies: new[] { RoutePatternFactory.ParameterPolicy(new TransformingRouteConstraint()), });
+
+ var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]);
+
+ var optionalConstraint = Assert.IsType(parameterPolicy);
+ Assert.IsType(optionalConstraint.InnerConstraint);
+ var transformer = Assert.IsAssignableFrom(parameterPolicy);
+ Assert.Equal("hello", transformer.TransformOutbound("HELLO"));
+ }
+
[Fact]
public void Create_CreatesParameterPolicy_FromRoutePattern_ParameterPolicy()
{
@@ -445,6 +465,24 @@ public bool Match(
return false;
}
}
+
+ private class TransformingRouteConstraint : IRouteConstraint, IOutboundParameterTransformer
+ {
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ return true;
+ }
+
+ public string TransformOutbound(object value)
+ {
+ return value?.ToString()?.ToLowerInvariant();
+ }
+ }
}
public class CustomParameterPolicy : IParameterPolicy