Skip to content

Commit 323ba3d

Browse files
[release/6.0] Check for duplicate endpoint names on startup (#36488)
* Check for duplicate endpoint names on startup * Add display name to exception message * Always validate duplicate endpoints and add more info to error Co-authored-by: Safia Abdalla <[email protected]> Co-authored-by: Safia Abdalla <[email protected]>
1 parent 8511fef commit 323ba3d

File tree

2 files changed

+117
-2
lines changed

2 files changed

+117
-2
lines changed

src/Http/Routing/src/Matching/DataSourceDependentMatcher.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,33 @@ public override Task MatchAsync(HttpContext httpContext)
4141
private Matcher CreateMatcher(IReadOnlyList<Endpoint> endpoints)
4242
{
4343
var builder = _matcherBuilderFactory();
44+
var seenEndpointNames = new Dictionary<string, string?>();
4445
for (var i = 0; i < endpoints.Count; i++)
4546
{
4647
// By design we only look at RouteEndpoint here. It's possible to
4748
// register other endpoint types, which are non-routable, and it's
4849
// ok that we won't route to them.
49-
if (endpoints[i] is RouteEndpoint endpoint && endpoint.Metadata.GetMetadata<ISuppressMatchingMetadata>()?.SuppressMatching != true)
50+
if (endpoints[i] is RouteEndpoint endpoint)
5051
{
51-
builder.AddEndpoint(endpoint);
52+
// Validate that endpoint names are unique.
53+
var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName;
54+
if (endpointName is not null)
55+
{
56+
if (seenEndpointNames.TryGetValue(endpointName, out var existingEndpoint))
57+
{
58+
throw new InvalidOperationException($"Duplicate endpoint name '{endpointName}' found on '{endpoint.DisplayName}' and '{existingEndpoint}'. Endpoint names must be globally unique.");
59+
}
60+
61+
seenEndpointNames.Add(endpointName, endpoint.DisplayName ?? endpoint.RoutePattern.RawText);
62+
}
63+
64+
// We check for duplicate endpoint names on all endpoints regardless
65+
// of whether they suppress matching because endpoint names can be
66+
// used in OpenAPI specifications as well.
67+
if (endpoint.Metadata.GetMetadata<ISuppressMatchingMetadata>()?.SuppressMatching != true)
68+
{
69+
builder.AddEndpoint(endpoint);
70+
}
5271
}
5372
}
5473

src/Http/Routing/test/UnitTests/Matching/DataSourceDependentMatcherTest.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,102 @@ public void Matcher_UnsuppressedEndpoint_IsUsed()
141141
Assert.Same(endpoint, Assert.Single(inner.Endpoints));
142142
}
143143

144+
[Fact]
145+
public void Matcher_ThrowsOnDuplicateEndpoints()
146+
{
147+
// Arrange
148+
var expectedError = "Duplicate endpoint name 'Foo' found on '/bar' and '/foo'. Endpoint names must be globally unique.";
149+
var dataSource = new DynamicEndpointDataSource();
150+
var lifetime = new DataSourceDependentMatcher.Lifetime();
151+
dataSource.AddEndpoint(new RouteEndpoint(
152+
TestConstants.EmptyRequestDelegate,
153+
RoutePatternFactory.Parse("/foo"),
154+
0,
155+
new EndpointMetadataCollection(new EndpointNameMetadata("Foo")),
156+
"/foo"
157+
));
158+
dataSource.AddEndpoint(new RouteEndpoint(
159+
TestConstants.EmptyRequestDelegate,
160+
RoutePatternFactory.Parse("/bar"),
161+
0,
162+
new EndpointMetadataCollection(new EndpointNameMetadata("Foo")),
163+
"/bar"
164+
));
165+
166+
// Assert
167+
var exception = Assert.Throws<InvalidOperationException>(
168+
() => new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create));
169+
Assert.Equal(expectedError, exception.Message);
170+
}
171+
172+
[Fact]
173+
public void Matcher_ThrowsOnDuplicateEndpointsFromMultipleSources()
174+
{
175+
// Arrange
176+
var expectedError = "Duplicate endpoint name 'Foo' found on '/foo2' and '/foo'. Endpoint names must be globally unique.";
177+
var dataSource = new DynamicEndpointDataSource();
178+
var lifetime = new DataSourceDependentMatcher.Lifetime();
179+
dataSource.AddEndpoint(new RouteEndpoint(
180+
TestConstants.EmptyRequestDelegate,
181+
RoutePatternFactory.Parse("/foo"),
182+
0,
183+
new EndpointMetadataCollection(new EndpointNameMetadata("Foo")),
184+
"/foo"
185+
));
186+
dataSource.AddEndpoint(new RouteEndpoint(
187+
TestConstants.EmptyRequestDelegate,
188+
RoutePatternFactory.Parse("/bar"),
189+
0,
190+
new EndpointMetadataCollection(new EndpointNameMetadata("Bar")),
191+
"/bar"
192+
));
193+
var anotherDataSource = new DynamicEndpointDataSource();
194+
anotherDataSource.AddEndpoint(new RouteEndpoint(
195+
TestConstants.EmptyRequestDelegate,
196+
RoutePatternFactory.Parse("/foo2"),
197+
0,
198+
new EndpointMetadataCollection(new EndpointNameMetadata("Foo")),
199+
"/foo2"
200+
));
201+
202+
var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource, anotherDataSource });
203+
204+
// Assert
205+
var exception = Assert.Throws<InvalidOperationException>(
206+
() => new DataSourceDependentMatcher(compositeDataSource, lifetime, TestMatcherBuilder.Create));
207+
Assert.Equal(expectedError, exception.Message);
208+
}
209+
210+
[Fact]
211+
public void Matcher_ThrowsOnDuplicateEndpointAddedLater()
212+
{
213+
// Arrange
214+
var expectedError = "Duplicate endpoint name 'Foo' found on '/bar' and '/foo'. Endpoint names must be globally unique.";
215+
var dataSource = new DynamicEndpointDataSource();
216+
var lifetime = new DataSourceDependentMatcher.Lifetime();
217+
dataSource.AddEndpoint(new RouteEndpoint(
218+
TestConstants.EmptyRequestDelegate,
219+
RoutePatternFactory.Parse("/foo"),
220+
0,
221+
new EndpointMetadataCollection(new EndpointNameMetadata("Foo")),
222+
"/foo"
223+
));
224+
225+
// Act (should be all good since no duplicate has been added yet)
226+
var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create);
227+
228+
// Assert that rerunning initializer throws AggregateException
229+
var exception = Assert.Throws<AggregateException>(
230+
() => dataSource.AddEndpoint(new RouteEndpoint(
231+
TestConstants.EmptyRequestDelegate,
232+
RoutePatternFactory.Parse("/bar"),
233+
0,
234+
new EndpointMetadataCollection(new EndpointNameMetadata("Foo")),
235+
"/bar"
236+
)));
237+
Assert.Equal(expectedError, exception.InnerException.Message);
238+
}
239+
144240
private class TestMatcherBuilder : MatcherBuilder
145241
{
146242
public static Func<MatcherBuilder> Create = () => new TestMatcherBuilder();

0 commit comments

Comments
 (0)