Skip to content

Commit bcfbd5c

Browse files
authored
Change route and query fallback semantics (#35317)
* Change route and query fallback semantics - If the list of route parameters is empty then simple parameters are from query only. - If the list of route parameters is null then fallback from route to query. - Added tests.
1 parent 10f8ad2 commit bcfbd5c

File tree

3 files changed

+118
-8
lines changed

3 files changed

+118
-8
lines changed

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,22 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
247247
}
248248
else if (parameter.ParameterType == typeof(string) || TryParseMethodCache.HasTryParseMethod(parameter))
249249
{
250-
// We're in the fallback case and we have a parameter and route parameter match so don't fallback
251-
// to query string in this case
252-
if (factoryContext.RouteParameters is { } routeParams && routeParams.Contains(parameter.Name, StringComparer.OrdinalIgnoreCase))
250+
// 1. We bind from route values only, if route parameters are non-null and the parameter name is in that set.
251+
// 2. We bind from query only, if route parameters are non-null and the parameter name is NOT in that set.
252+
// 3. Otherwise, we fallback to route or query if route parameters is null (it means we don't know what route parameters are defined). This case only happens
253+
// when RDF.Create is manually invoked.
254+
if (factoryContext.RouteParameters is { } routeParams)
253255
{
254-
return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext);
256+
if (routeParams.Contains(parameter.Name, StringComparer.OrdinalIgnoreCase))
257+
{
258+
// We're in the fallback case and we have a parameter and route parameter match so don't fallback
259+
// to query string in this case
260+
return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext);
261+
}
262+
else
263+
{
264+
return BindParameterFromProperty(parameter, QueryExpr, parameter.Name, factoryContext);
265+
}
255266
}
256267

257268
return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext);

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,62 @@ public async Task SpecifiedRouteParametersDoNotFallbackToQueryString()
248248
Assert.Null(httpContext.Items["input"]);
249249
}
250250

251+
[Fact]
252+
public async Task SpecifiedQueryParametersDoNotFallbackToRouteValues()
253+
{
254+
var httpContext = new DefaultHttpContext();
255+
256+
var requestDelegate = RequestDelegateFactory.Create((int? id, HttpContext httpContext) =>
257+
{
258+
if (id is not null)
259+
{
260+
httpContext.Items["input"] = id;
261+
}
262+
},
263+
new() { RouteParameterNames = new string[] { } });
264+
265+
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
266+
{
267+
["id"] = "41"
268+
});
269+
httpContext.Request.RouteValues = new()
270+
{
271+
["id"] = "42"
272+
};
273+
274+
await requestDelegate(httpContext);
275+
276+
Assert.Equal(41, httpContext.Items["input"]);
277+
}
278+
279+
[Fact]
280+
public async Task NullRouteParametersPrefersRouteOverQueryString()
281+
{
282+
var httpContext = new DefaultHttpContext();
283+
284+
var requestDelegate = RequestDelegateFactory.Create((int? id, HttpContext httpContext) =>
285+
{
286+
if (id is not null)
287+
{
288+
httpContext.Items["input"] = id;
289+
}
290+
},
291+
new() { RouteParameterNames = null });
292+
293+
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
294+
{
295+
["id"] = "41"
296+
});
297+
httpContext.Request.RouteValues = new()
298+
{
299+
["id"] = "42"
300+
};
301+
302+
await requestDelegate(httpContext);
303+
304+
Assert.Equal(42, httpContext.Items["input"]);
305+
}
306+
251307
[Fact]
252308
public async Task CreatingDelegateWithInstanceMethodInfoCreatesInstancePerCall()
253309
{
@@ -825,7 +881,7 @@ void TestAction([FromBody] Todo todo)
825881
httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(invalidDataException);
826882
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
827883
httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
828-
884+
829885
httpContext.RequestServices = serviceCollection.BuildServiceProvider();
830886

831887
var requestDelegate = RequestDelegateFactory.Create(TestAction);
@@ -1516,7 +1572,10 @@ public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate
15161572
serviceCollection.AddSingleton(LoggerFactory);
15171573
httpContext.RequestServices = serviceCollection.BuildServiceProvider();
15181574

1519-
var requestDelegate = RequestDelegateFactory.Create(@delegate);
1575+
var requestDelegate = RequestDelegateFactory.Create(@delegate, new()
1576+
{
1577+
RouteParameterNames = routeParam is not null ? new[] { paramName } : Array.Empty<string>()
1578+
});
15201579

15211580
await requestDelegate(httpContext);
15221581

@@ -1567,7 +1626,7 @@ public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate,
15671626
var httpContext = new DefaultHttpContext();
15681627
var responseBodyStream = new MemoryStream();
15691628
httpContext.Response.Body = responseBodyStream;
1570-
1629+
15711630
if (hasBody)
15721631
{
15731632
var todo = new Todo() { Name = "Default Todo" };
@@ -1699,7 +1758,7 @@ public async Task AllowEmptyOverridesOptionality(Delegate @delegate, bool allows
16991758
await requestDelegate(httpContext);
17001759

17011760
var logs = TestSink.Writes.ToArray();
1702-
1761+
17031762
if (!allowsEmptyRequest)
17041763
{
17051764
Assert.Equal(400, httpContext.Response.StatusCode);

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,46 @@ public async Task MapGetWithRouteParameter_BuildsEndpointWithRouteSpecificBindin
141141
Assert.Null(httpContext.Items["input"]);
142142
}
143143

144+
[Fact]
145+
public async Task MapGetWithoutRouteParameter_BuildsEndpointWithQuerySpecificBinding()
146+
{
147+
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
148+
_ = builder.MapGet("/", (int? id, HttpContext httpContext) =>
149+
{
150+
if (id is not null)
151+
{
152+
httpContext.Items["input"] = id;
153+
}
154+
});
155+
156+
var dataSource = GetBuilderEndpointDataSource(builder);
157+
// Trigger Endpoint build by calling getter.
158+
var endpoint = Assert.Single(dataSource.Endpoints);
159+
160+
var methodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
161+
Assert.NotNull(methodMetadata);
162+
var method = Assert.Single(methodMetadata!.HttpMethods);
163+
Assert.Equal("GET", method);
164+
165+
var routeEndpointBuilder = GetRouteEndpointBuilder(builder);
166+
Assert.Equal("/ HTTP: GET", routeEndpointBuilder.DisplayName);
167+
Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText);
168+
169+
// Assert that we don't fallback to the route values
170+
var httpContext = new DefaultHttpContext();
171+
172+
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>()
173+
{
174+
["id"] = "41"
175+
});
176+
httpContext.Request.RouteValues = new();
177+
httpContext.Request.RouteValues["id"] = "42";
178+
179+
await endpoint.RequestDelegate!(httpContext);
180+
181+
Assert.Equal(41, httpContext.Items["input"]);
182+
}
183+
144184
[Theory]
145185
[MemberData(nameof(MapMethods))]
146186
public async Task MapVerbWithExplicitRouteParameterIsCaseInsensitive(Action<IEndpointRouteBuilder, string, Delegate> map, string expectedMethod)

0 commit comments

Comments
 (0)