Skip to content

Call TransformOutbound for optional route parameters - #68453

Open
GOVINSAGA wants to merge 1 commit into
dotnet:mainfrom
GOVINSAGA:fix-23063-optional-outbound-transformer
Open

Call TransformOutbound for optional route parameters#68453
GOVINSAGA wants to merge 1 commit into
dotnet:mainfrom
GOVINSAGA:fix-23063-optional-outbound-transformer

Conversation

@GOVINSAGA

Copy link
Copy Markdown
  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Call TransformOutbound for optional route parameters

Description

DefaultParameterPolicyFactory.InitializeRouteConstraint wraps an optional parameter's constraint in OptionalRouteConstraint, which implements only IRouteConstraint. TemplateBinder.Initialize discovers transformers with a separate is IOutboundParameterTransformer test, and the wrapper fails that test — so when the inner constraint also implements IOutboundParameterTransformer, the transformer is silently dropped and TransformOutbound is never called during URL generation.

The visible effect is that a transforming constraint applies to a required segment but not to an optional one:

Route template Outbound value today Expected
{id:myconstraint} transformed transformed
{id:myconstraint?} not transformed transformed

Approach

Optional constraints that also implement IOutboundParameterTransformer are now wrapped in a new internal sealed subclass of OptionalRouteConstraint that additionally implements IOutboundParameterTransformer and forwards to the inner constraint. Matching behaviour is inherited unchanged.

Constraints that are not transformers keep using OptionalRouteConstraint exactly as before, so the wrapper type callers already observe is unchanged for every existing case.

The public OptionalRouteConstraint type is untouched, so there is no public API change and no PublicAPI.Unshipped.txt entry.

Note on the Components build

src/Http/Routing/src/Constraints/**/*.cs is compiled into Microsoft.AspNetCore.Components via Microsoft.AspNetCore.Components.Routing.targets, and IOutboundParameterTransformer is not part of that source set. The new file is excluded with a Compile Remove entry, alongside the three constraints already excluded there. Verified that Microsoft.AspNetCore.Components still builds clean.

Behaviour change

URLs generated for an optional segment with a transforming constraint now contain the transformed value where they previously contained the raw value. That is the fix, but it is an observable output change for anyone relying on the current behaviour — happy to gate it if you would prefer that.

Tests

  • DefaultParameterPolicyFactoryTest.Create_CreatesParameterPolicy_FromRoutePattern_Constraint_Optional_PreservesOutboundParameterTransformer — the policy returned for an optional transforming constraint still surfaces IOutboundParameterTransformer and forwards TransformOutbound.
  • TemplateBinderTests.BindValues_ParameterTransformer_OptionalParameter — end-to-end URL generation applies the transform, using a policy built through DefaultParameterPolicyFactory so the parameter is wrapped exactly as it is at runtime.

Both fail without the change. Full Microsoft.AspNetCore.Routing.Tests suite passes (3480 tests).

Fixes #23063

DefaultParameterPolicyFactory wrapped every optional parameter's
constraint in OptionalRouteConstraint, which implements only
IRouteConstraint. When the inner constraint also implemented
IOutboundParameterTransformer, the wrap hid the transformer from
TemplateBinder, so TransformOutbound was never called and URL
generation emitted the untransformed value for optional segments.

Wrap transforming constraints in a new internal subclass that forwards
TransformOutbound to the inner constraint, leaving the public
OptionalRouteConstraint type unchanged.
@GOVINSAGA
GOVINSAGA requested review from a team and halter73 as code owners August 12, 2026 12:54
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Aug 12, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Thanks for your PR, @GOVINSAGA. Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

routeConstraint = new OptionalRouteConstraint(routeConstraint);
// Wrapping the constraint must not hide an IOutboundParameterTransformer it implements,
// otherwise TransformOutbound is never called when generating a URL for this parameter.
routeConstraint = routeConstraint is IOutboundParameterTransformer

@halter73 halter73 Aug 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javiercn Do you have any thoughts on this change?

I see that IParameterLiteralNodeMatchingPolicy also implements IParameterPolicy. ICachableParameterPolicy is also commonly implemented by IParmaterPolicy, so would we eventually need to have a special wrapper for those as well? I assume not, since the number of combinations would become unwieldy.

I wonder if option B from the issue (#23063) would be the better way to address this:

Option B — leave the factory alone and unwrap in TemplateBinder.Initialize, i.e. also accept OptionalRouteConstraint { InnerConstraint: IOutboundParameterTransformer }. Narrower, but puts routing-specific unwrapping knowledge into the binder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TransformOutbound of route constraint is not called when the route segment is optional

2 participants