-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Add NotRouteConstraint for logical negation in route constraints #62756
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
base: main
Are you sure you want to change the base?
Conversation
I am not sure if those who failed are related with the changes. |
@sami-daniel These failures appear to be flakes in our build and not related to your change. I can kick the build here. |
This is a hot path. Are we adding allocations? Why do we need this logic in the default selector instead of one you can opt into? |
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.
Thanks for taking the time to work on this change, @sami-daniel!
I left some notes on the current implementation but have two big picture thoughts:
- Given the nature of the change, we'll likely have to document this as a breaking behavioral change in routing given it changes the contract the router makes.
- I wonder if it might be better to support this feature by changing the logic that calculates precedence for route templates to account for constraints (ref).
@@ -60,6 +60,8 @@ private static void ProcessFinalCandidates( | |||
Endpoint? endpoint = null; | |||
RouteValueDictionary? values = null; | |||
int? foundScore = null; | |||
var candidatesWithSameScore = new List<CandidateState>(); |
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.
Can we initialize this the first time two candidates with the same score are detected?
// Assert 2 | ||
Assert.Same(endpoint2, httpContext.GetEndpoint()); | ||
} | ||
|
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.
Can we add a test for the case where the match is still ambiguous if there are specificity constraints that match?
{ | ||
// Strong typed constraints that are very restrictive and has | ||
// the highest specificity | ||
"guid" => 100, |
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.
Using .Equals with a StringComparison to ignore casing might be more consistent than calling ToLowerInvariant for every constraint.
I’d follow up here by looking for benchmarks, for scenarios where this new code should not impact simple scenarios. At a glance, it does not look pay for play. |
Few things. I don't believe that the endpoint selector is the right place to make this change. Factoring it on the route precedence would make more sense. That said, I think this change is problematic for several reasons:
I think before we even consider a change in this area, we need to discuss whether this is even a change that we want to make at all, and that should be something we discuss on an issue rather than on a PR. |
Yes, we are adding allocations, and worse. We are copying the candidateState unnecessarily to the list when we could use it by reference. I will change that. |
Placing these changes in the DefaultEndpointSelector was a mistake, as it's not actually the source of the problem. The issue lies with its caller — the DFAMatcher. The DefaultEndpointSelector is just the default implementation used by the engine when no custom endpoint selector is defined. In contrast, the DFAMatcher cannot be replaced or customized with its own selector, so it makes more sense for the changes to be applied there. The real problem is that the DFAMatcher does not consider the weight of constraints, as described on the pr — it only checks whether they are valid. I confess that I did not notice or pay attention to the use of RoutePrecedence in the code. From what I saw, it is used in the DFAMatcher builder, and in some other places that I am not sure are related. I am not sure if it makes sense to make changes to RoutePrecedence, since it is not the direct cause of the problem. I would need to study a bit more to see where it applies... |
Yes, those are great points to consider. It may really not be worth pursuing. The DFA/DFAMatcher is already a solid algorithm that performs its task well. As you mentioned, introducing additional overhead to this hot path might not be a good idea — even if it’s just to eliminate ambiguity and align more closely with how framework users think. From what I can tell, making this change without affecting a critical path in the router is nearly impossible, since its a hot path. It seems like a very costly tradeoff. On top of that, as you pointed out, there are other cases that would also need to be handled. This could potentially require an entirely new pattern matching layer to cover all scenarios, adding even more overhead. Maybe your approach to negating a constraint is much more interesting and much cheaper. The control of this would be in the user's hands, not altering the current behavior of the Router and not adding any overhead. Such a constraint would pave the way for a new range of other possible routes to be created with similar templates. I really would like to do this 😁😁. |
As @javiercn said, perhaps it would be better, instead of adding overhead to |
9990fe5
to
0c947bd
Compare
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.
Pull Request Overview
This PR enhances the routing system's endpoint selection by implementing constraint specificity analysis to resolve ambiguities between routes with different constraint levels. When multiple endpoints have the same route pattern but different constraints (e.g., {name:required}
vs {id:guid:required}
), the system can now select the more specific constraint rather than throwing an ambiguous match exception.
Key Changes
- Constraint specificity analysis in DefaultEndpointSelector to resolve routing ambiguities
- Introduction of NotRouteConstraint for logical negation of route constraints
- Specificity scoring system with weights for different constraint types (strong-typed > ranged > length > string patterns > file/nonfile > unknown > required)
Reviewed Changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
File | Description |
---|---|
DefaultEndpointSelector.cs | Implements constraint specificity analysis logic and scoring system to resolve endpoint ambiguities |
NotRouteConstraint.cs | New constraint class that provides logical negation functionality for route constraints |
NotRouteConstraintTests.cs | Comprehensive test suite for the new NotRouteConstraint functionality |
DfaMatcherTest.cs | Added tests for the enhanced endpoint selection with constraint specificity |
RouteOptions.cs | Minor comment addition |
PublicAPI.Unshipped.txt | API surface changes for the new NotRouteConstraint |
src/Http/Routing/src/RouteOptions.cs
Outdated
@@ -141,6 +141,7 @@ private static IDictionary<string, Type> GetDefaultConstraintMap() | |||
AddConstraint<FileNameRouteConstraint>(defaults, "file"); | |||
AddConstraint<NonFileNameRouteConstraint>(defaults, "nonfile"); | |||
|
|||
// Non |
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 comment '// Non' is incomplete and unclear. It should either be completed with a meaningful description or removed entirely.
// Non |
Copilot uses AI. Check for mistakes.
var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest); | ||
|
||
// Assert | ||
Assert.False(result); // hould fall back to default map and int constraint should match |
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.
There's a typo in the comment: 'hould' should be 'Should'.
Assert.False(result); // hould fall back to default map and int constraint should match | |
Assert.False(result); // Should fall back to default map and int constraint should match |
Copilot uses AI. Check for mistakes.
// FIXME: I'm not sure if this is a good thing to do because | ||
// it requires weak spreading between the ConstraintMap and | ||
// RouteOptions. It doesn't seem appropriate to create two | ||
// identical variables for this... |
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 FIXME comment indicates uncertainty about the design approach. This should be resolved before merging - either the approach should be validated and the comment removed, or an alternative implementation should be considered.
// identical variables for this... |
Copilot uses AI. Check for mistakes.
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.
I agree Mr. copilot 👍
@@ -85,15 +88,27 @@ private static void ProcessFinalCandidates( | |||
} | |||
else if (foundScore == state.Score) |
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.
Missing newline before the opening curly brace. According to the coding guidelines, there should be a newline before the opening brace of code blocks.
Copilot uses AI. Check for mistakes.
|
||
// If we have multiple candidates with the same score, try to resolve using | ||
// constraint specificity rules | ||
if (candidatesWithSameScore.Count > 1) |
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.
Missing newline before the opening curly brace. According to the coding guidelines, there should be a newline before the opening brace of code blocks.
Copilot uses AI. Check for mistakes.
if (candidatesWithSameScore.Count > 1) | ||
{ | ||
var mostSpecific = SelectMostSpecificEndpoint(candidatesWithSameScore); | ||
if (mostSpecific.HasValue) |
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.
Missing newline before the opening curly brace. According to the coding guidelines, there should be a newline before the opening brace of code blocks.
Copilot uses AI. Check for mistakes.
endpoint = mostSpecific.Value.Endpoint; | ||
values = mostSpecific.Value.Values; | ||
} | ||
else |
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.
Missing newline before the opening curly brace. According to the coding guidelines, there should be a newline before the opening brace of code blocks.
Copilot uses AI. Check for mistakes.
0c947bd
to
85a5c5a
Compare
The other point about this is that there is already a mechanism to disambiguate, which is |
85a5c5a
to
90aa229
Compare
It makes sense. I talked a bit about this in referenced issue, and it does. I'm not familiar with |
Implements a new route constraint that provides logical negation capabilities for existing route constraints. This enables developers to create routes that match when specific constraints do NOT match. Key Features: - Single constraint negation: not(int) matches non-integers - Multiple constraint negation: not(int;bool) matches neither integers nor booleans - Nested negation support: not(not(int)) for double negation (equivalent to int) - Parameterized constraints: not(min(18)) for values under 18 or non-numeric - File extension negation: not(file) for values without file extensions - String pattern negation: not(alpha) for non-alphabetic values and other patterns Examples: - /users/{id:not(int)} - matches /users/john but not /users/123 - /files/{name:not(file)} - matches /files/readme but not /files/doc.txt - /ages/{age:not(min(18))} - matches /ages/16 but not /ages/25 - /values/{val:not(int;bool)} - matches /values/text but not /values/123 or /values/true Implementation: - Added NotRouteConstraint class with XML documentation - Supports both IRouteConstraint and IParameterLiteralNodeMatchingPolicy - Added to RouteOptions default constraint map with 'not' token Testing: - Tests for single/multiple constraints, nested negation, edge cases - Integration tests with HttpContext and service provider scenarios - Validation of argument null exceptions and error handling - Performance tests for literal matching optimization This enables more flexible route patterns by allowing inverse constraint logic. It can also be used to escape internal implementation details when building route templates.
90aa229
to
0b80fd8
Compare
Okay, this new implementation has a few issues that I've identified so far, but I'm not sure how to proceed. The first one I've been able to report is whether it's prudent/necessary to recreate a new Another point I'm not sure how to proceed with is how we should proceed with |
Add NotRouteConstraint for logical negation in route constraints
Implements a new route constraint that provides logical negation capabilities
for existing route constraints. This enables developers to create routes that
match when specific constraints do NOT match.
Key Features:
and other patterns
Examples:
Implementation:
Testing:
This enables more flexible route patterns by allowing inverse constraint logic.
It can also be used to escape internal implementation details when
building route templates.
Fixes #62278