Call TransformOutbound for optional route parameters - #68453
Conversation
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.
|
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 |
There was a problem hiding this comment.
@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 acceptOptionalRouteConstraint { InnerConstraint: IOutboundParameterTransformer }. Narrower, but puts routing-specific unwrapping knowledge into the binder.
Call TransformOutbound for optional route parameters
Description
DefaultParameterPolicyFactory.InitializeRouteConstraintwraps an optional parameter's constraint inOptionalRouteConstraint, which implements onlyIRouteConstraint.TemplateBinder.Initializediscovers transformers with a separateis IOutboundParameterTransformertest, and the wrapper fails that test — so when the inner constraint also implementsIOutboundParameterTransformer, the transformer is silently dropped andTransformOutboundis never called during URL generation.The visible effect is that a transforming constraint applies to a required segment but not to an optional one:
{id:myconstraint}{id:myconstraint?}Approach
Optional constraints that also implement
IOutboundParameterTransformerare now wrapped in a newinternal sealedsubclass ofOptionalRouteConstraintthat additionally implementsIOutboundParameterTransformerand forwards to the inner constraint. Matching behaviour is inherited unchanged.Constraints that are not transformers keep using
OptionalRouteConstraintexactly as before, so the wrapper type callers already observe is unchanged for every existing case.The public
OptionalRouteConstrainttype is untouched, so there is no public API change and noPublicAPI.Unshipped.txtentry.Note on the Components build
src/Http/Routing/src/Constraints/**/*.csis compiled intoMicrosoft.AspNetCore.ComponentsviaMicrosoft.AspNetCore.Components.Routing.targets, andIOutboundParameterTransformeris not part of that source set. The new file is excluded with aCompile Removeentry, alongside the three constraints already excluded there. Verified thatMicrosoft.AspNetCore.Componentsstill 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 surfacesIOutboundParameterTransformerand forwardsTransformOutbound.TemplateBinderTests.BindValues_ParameterTransformer_OptionalParameter— end-to-end URL generation applies the transform, using a policy built throughDefaultParameterPolicyFactoryso the parameter is wrapped exactly as it is at runtime.Both fail without the change. Full
Microsoft.AspNetCore.Routing.Testssuite passes (3480 tests).Fixes #23063