-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Add support for optional parameters in Blazor routes #19733
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
I've left some code comments to explain the changes that require context to understand. While implementing this, I was prioritizing reducing the number of fundamental changes that needed to be made to the routing logic to support this. The consequence of this is that the I also added test cases to validate this based on my understanding of our test suite and made sure that existing route tests still pass. Happy to add more test coverage to this as needed. @pranavkm had shared a pointer to other routing tests in this repo that can be used as inspiration so I will take a look at that. |
Thanks @captainsafia! This looks superb. It might be a couple of days until I get to review this properly since I need to complete something else pretty quickly. Hope that's OK. In the meantime if either @pranavkm or @javiercn finds an opportunity to take a look they should feel free :) |
src/Components/Components/test/Routing/RouteTableFactoryTests.cs
Outdated
Show resolved
Hide resolved
I might be missing the implementation here, but is it also possible add support for optional parameters that don't define a type constraint? For example, |
This is looking really good to me. Thanks @captainsafia! People have been wanting this feature for ages. Hopefully @pranavkm will also get a chance to dig into it and comment on any of the subtleties. |
🤦♀ I can add support for this. |
src/Components/Components/src/Routing/OptionalTypeRouteConstraint.cs
Outdated
Show resolved
Hide resolved
// Set the IsOptional flag to true if any type constraints | ||
// for this parameter are designated as optional. | ||
IsOptional = tokens.Skip(1).Any(token => token.Contains('?')); | ||
|
||
Value = tokens[0]; | ||
Constraints = tokens.Skip(1) | ||
.Select(token => RouteConstraint.Parse(template, segment, token)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this already works just fine, but I suspect you might have to trim the trailing ?
before passing it through. This is incredibly rare, but you could do {id:int:long?}
which basically treats the entire segment as optional, not just the long?
Would the :int
constraint get treated as non-optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constraint itself wouldn't be treated as optional, but the segment would be. The result of this is as follows:
- If the user has a route like
/a/{id:int:long?}/literal
, then it will through an error because they have provided a non-optional segment after an optional one. It makes sense to me to throw an error in this case but let me know if you think otherwise. - When matching it is not required that a match be found for this segment since it contains an optional component.
You'd still get two separate constraints to match with, one optional and one not.
src/Components/Components/test/Routing/RouteTableFactoryTests.cs
Outdated
Show resolved
Hide resolved
routeTable.Route(context); | ||
|
||
// Assert | ||
Assert.NotNull(context.Handler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert.NotNull(context.Handler); | |
Assert.NotNull(context.Handler); | |
Assert.Equal(typeof(TestHandler1), context.Handler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be useful to verify these scenarios work as expected:
var routeTable = new TestRouteTableBuilder()
.AddRoute("/users/{id}", typeof(TestHandler1))
.AddRoute("/users/{id?}", typeof(TestHandler2))
.Build();
// Scenario 1:
var context = new RouteContext("/users/");
// Scenario 2: (this should throw)
var context = new RouteContext("/users/2");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also the reverse of the scenario that @pranavkm mentioned 👍 (reverse the optional and non-optional ones)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pranavkm and I discussed this on teams but I'll post notes on my understanding of how this should function (which I reflected in the tests).
For the situation above, Scenario 1 should actually match with TestHandler2 and Scenario 2 should match with TestHandler1.
IMO, it makes sense that if the routes are otherwise ambiguous but one has an optional parameter and the other does not, then it should favor the non-optional over the optional unless no parameter is provided.
src/Components/Components/test/Routing/RouteTableFactoryTests.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pranavkm This is ready for another look. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great
Thanks all for the reviews! Merging this now. |
Summary of the changes
OptionalTypeRouteConstraint
subclass forRouteConstraint
/a//b
and/users/
)Addresses #10759