Skip to content

Commit fd94970

Browse files
authored
Make sure route binding isn't case sensitive (#35090)
1 parent d941575 commit fd94970

File tree

2 files changed

+104
-2
lines changed

2 files changed

+104
-2
lines changed

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
208208

209209
if (parameterCustomAttributes.OfType<IFromRouteMetadata>().FirstOrDefault() is { } routeAttribute)
210210
{
211-
if (factoryContext.RouteParameters is { } routeParams && !routeParams.Contains(parameter.Name))
211+
if (factoryContext.RouteParameters is { } routeParams && !routeParams.Contains(parameter.Name, StringComparer.OrdinalIgnoreCase))
212212
{
213213
throw new InvalidOperationException($"{parameter.Name} is not a route paramter.");
214214
}
@@ -255,7 +255,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
255255
{
256256
// We're in the fallback case and we have a parameter and route parameter match so don't fallback
257257
// to query string in this case
258-
if (factoryContext.RouteParameters is { } routeParams && routeParams.Contains(parameter.Name))
258+
if (factoryContext.RouteParameters is { } routeParams && routeParams.Contains(parameter.Name, StringComparer.OrdinalIgnoreCase))
259259
{
260260
return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext);
261261
}

src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs

+102
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,32 @@ private RouteEndpointBuilder GetRouteEndpointBuilder(IEndpointRouteBuilder endpo
2929
return Assert.IsType<RouteEndpointBuilder>(Assert.Single(GetBuilderEndpointDataSource(endpointRouteBuilder).EndpointBuilders));
3030
}
3131

32+
public static object[][] MapMethods
33+
{
34+
get
35+
{
36+
void MapGet(IEndpointRouteBuilder routes, string template, Delegate action) =>
37+
routes.MapGet(template, action);
38+
39+
void MapPost(IEndpointRouteBuilder routes, string template, Delegate action) =>
40+
routes.MapPost(template, action);
41+
42+
void MapPut(IEndpointRouteBuilder routes, string template, Delegate action) =>
43+
routes.MapPut(template, action);
44+
45+
void MapDelete(IEndpointRouteBuilder routes, string template, Delegate action) =>
46+
routes.MapDelete(template, action);
47+
48+
return new object[][]
49+
{
50+
new object[] { (Action<IEndpointRouteBuilder, string, Delegate>)MapGet, "GET" },
51+
new object[] { (Action<IEndpointRouteBuilder, string, Delegate>)MapPost, "POST" },
52+
new object[] { (Action<IEndpointRouteBuilder, string, Delegate>)MapPut, "PUT" },
53+
new object[] { (Action<IEndpointRouteBuilder, string, Delegate>)MapDelete, "DELETE" },
54+
};
55+
}
56+
}
57+
3258
[Fact]
3359
public void MapEndpoint_PrecedenceOfMetadata_BuilderMetadataReturned()
3460
{
@@ -115,6 +141,82 @@ public async Task MapGetWithRouteParameter_BuildsEndpointWithRouteSpecificBindin
115141
Assert.Null(httpContext.Items["input"]);
116142
}
117143

144+
[Theory]
145+
[MemberData(nameof(MapMethods))]
146+
public async Task MapVerbWithExplicitRouteParameterIsCaseInsensitive(Action<IEndpointRouteBuilder, string, Delegate> map, string expectedMethod)
147+
{
148+
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
149+
150+
map(builder, "/{ID}", ([FromRoute] int? id, HttpContext httpContext) =>
151+
{
152+
if (id is not null)
153+
{
154+
httpContext.Items["input"] = id;
155+
}
156+
});
157+
158+
var dataSource = GetBuilderEndpointDataSource(builder);
159+
// Trigger Endpoint build by calling getter.
160+
var endpoint = Assert.Single(dataSource.Endpoints);
161+
162+
var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
163+
Assert.NotNull(methodMetadata);
164+
var method = Assert.Single(methodMetadata!.HttpMethods);
165+
Assert.Equal(expectedMethod, method);
166+
167+
var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
168+
Assert.Equal($"/{{ID}} HTTP: {expectedMethod}", routeEndpointBuilder.DisplayName);
169+
Assert.Equal($"/{{ID}}", routeEndpointBuilder.RoutePattern.RawText);
170+
171+
var httpContext = new DefaultHttpContext();
172+
173+
httpContext.Request.RouteValues["id"] = "13";
174+
175+
await endpoint.RequestDelegate!(httpContext);
176+
177+
Assert.Equal(13, httpContext.Items["input"]);
178+
}
179+
180+
[Theory]
181+
[MemberData(nameof(MapMethods))]
182+
public async Task MapVerbWithRouteParameterDoesNotFallbackToQuery(Action<IEndpointRouteBuilder, string, Delegate> map, string expectedMethod)
183+
{
184+
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
185+
186+
map(builder, "/{ID}", (int? id, HttpContext httpContext) =>
187+
{
188+
if (id is not null)
189+
{
190+
httpContext.Items["input"] = id;
191+
}
192+
});
193+
194+
var dataSource = GetBuilderEndpointDataSource(builder);
195+
// Trigger Endpoint build by calling getter.
196+
var endpoint = Assert.Single(dataSource.Endpoints);
197+
198+
var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
199+
Assert.NotNull(methodMetadata);
200+
var method = Assert.Single(methodMetadata!.HttpMethods);
201+
Assert.Equal(expectedMethod, method);
202+
203+
var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
204+
Assert.Equal($"/{{ID}} HTTP: {expectedMethod}", routeEndpointBuilder.DisplayName);
205+
Assert.Equal($"/{{ID}}", routeEndpointBuilder.RoutePattern.RawText);
206+
207+
// Assert that we don't fallback to the query string
208+
var httpContext = new DefaultHttpContext();
209+
210+
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
211+
{
212+
["id"] = "42"
213+
});
214+
215+
await endpoint.RequestDelegate!(httpContext);
216+
217+
Assert.Null(httpContext.Items["input"]);
218+
}
219+
118220
[Fact]
119221
public void MapGetWithRouteParameter_ThrowsIfRouteParameterDoesNotExist()
120222
{

0 commit comments

Comments
 (0)