Skip to content

Commit 180f735

Browse files
authored
Fix non-parameter route constraints not called with endpoint routing for 2.2 (#6587)
1 parent 874a67a commit 180f735

File tree

8 files changed

+199
-1
lines changed

8 files changed

+199
-1
lines changed

eng/PatchConfig.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Later on, this will be checked using this condition:
3131
Microsoft.AspNetCore.AspNetCoreModuleV2;
3232
Microsoft.AspNetCore.Authentication.Google;
3333
Microsoft.AspNetCore.Http;
34+
Microsoft.AspNetCore.Mvc.Core;
3435
Microsoft.AspNetCore.Server.IIS;
3536
java:signalr;
3637
</PackagesInPatch>

src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,15 @@ private int CreateEndpoints(
224224
var newPathSegments = routePattern.PathSegments.ToList();
225225
var hasLinkGenerationEndpoint = false;
226226

227+
// This is required because we create modified copies of the route pattern using its segments
228+
// A segment with a parameter will automatically include its policies
229+
// Non-parameter policies need to be manually included
230+
var nonParameterPolicyValues = routePattern.ParameterPolicies
231+
.Where(p => routePattern.GetParameter(p.Key ?? string.Empty) == null && p.Value.Count > 0 && p.Value.First().ParameterPolicy != null) // Only GetParameter is required. Extra is for safety
232+
.Select(p => new KeyValuePair<string, object>(p.Key, p.Value.First().ParameterPolicy)) // Can only pass a single non-parameter to RouteParameter
233+
.ToArray();
234+
var nonParameterPolicies = RouteValueDictionary.FromArray(nonParameterPolicyValues);
235+
227236
// Create a mutable copy
228237
var nonInlineDefaultsCopy = nonInlineDefaults != null
229238
? new RouteValueDictionary(nonInlineDefaults)
@@ -259,6 +268,7 @@ private int CreateEndpoints(
259268
resolvedRouteValues,
260269
name,
261270
GetPattern(ref patternStringBuilder, newPathSegments),
271+
nonParameterPolicies,
262272
newPathSegments,
263273
nonInlineDefaultsCopy,
264274
routeOrder++,
@@ -277,6 +287,7 @@ private int CreateEndpoints(
277287
resolvedRouteValues,
278288
name,
279289
GetPattern(ref patternStringBuilder, subPathSegments),
290+
nonParameterPolicies,
280291
subPathSegments,
281292
nonInlineDefaultsCopy,
282293
routeOrder++,
@@ -294,6 +305,7 @@ private int CreateEndpoints(
294305
resolvedRouteValues,
295306
name,
296307
GetPattern(ref patternStringBuilder, newPathSegments),
308+
nonParameterPolicies,
297309
newPathSegments,
298310
nonInlineDefaultsCopy,
299311
routeOrder++,
@@ -531,6 +543,7 @@ private RouteEndpoint CreateEndpoint(
531543
IDictionary<string, string> actionRouteValues,
532544
string routeName,
533545
string patternRawText,
546+
object nonParameterPolicies,
534547
IEnumerable<RoutePatternPathSegment> segments,
535548
object nonInlineDefaults,
536549
int order,
@@ -561,7 +574,7 @@ private RouteEndpoint CreateEndpoint(
561574

562575
var endpoint = new RouteEndpoint(
563576
requestDelegate,
564-
RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments),
577+
RoutePatternFactory.Pattern(patternRawText, defaults, nonParameterPolicies, segments),
565578
order,
566579
metadataCollection,
567580
action.DisplayName);

src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.AspNetCore.Mvc.Infrastructure;
1515
using Microsoft.AspNetCore.Mvc.Routing;
1616
using Microsoft.AspNetCore.Routing;
17+
using Microsoft.AspNetCore.Routing.Constraints;
1718
using Microsoft.AspNetCore.Routing.Matching;
1819
using Microsoft.Extensions.DependencyInjection;
1920
using Microsoft.Extensions.Options;
@@ -311,6 +312,109 @@ public void Endpoints_SingleAction_ConventionalRoute_ContainsParameterWithNullRe
311312
Assert.Empty(endpoints);
312313
}
313314

315+
[Fact]
316+
public void Endpoints_SingleAction_ConventionalRoute_ContainsNonParameterConstraint()
317+
{
318+
// Arrange
319+
var actionDescriptorCollection = GetActionDescriptorCollection(
320+
new { controller = "TestController", action = "TestAction", page = (string)null });
321+
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
322+
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
323+
string.Empty,
324+
"{controller}/{action}/{id:range(0, 100)}",
325+
new RouteValueDictionary(new { action = "TestAction" }),
326+
new RouteValueDictionary(new { controller = "TestController", nonParameter = new CustomConstraint(), id = new IntRouteConstraint() })));
327+
328+
// Act
329+
var endpoints = dataSource.Endpoints;
330+
331+
// Assert
332+
var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpoints));
333+
334+
var routePattern = endpoint.RoutePattern;
335+
336+
Assert.Equal("TestController/TestAction/{id::range(0, 100)}", routePattern.RawText);
337+
Assert.Collection(routePattern.ParameterPolicies.OrderBy(p => p.Key),
338+
p =>
339+
{
340+
Assert.Equal("id", p.Key);
341+
Assert.Collection(p.Value,
342+
c => Assert.IsType<IntRouteConstraint>(c.ParameterPolicy),
343+
c => Assert.Equal("range(0, 100)", c.Content));
344+
},
345+
p =>
346+
{
347+
Assert.Equal("nonParameter", p.Key);
348+
Assert.IsType<CustomConstraint>(p.Value.Single().ParameterPolicy);
349+
});
350+
}
351+
352+
[Fact]
353+
public void Endpoints_SingleAction_ConventionalRouteWithOptional_ContainsNonParameterConstraint()
354+
{
355+
// Arrange
356+
var actionDescriptorCollection = GetActionDescriptorCollection(
357+
new { controller = "TestController", action = "TestAction", page = (string)null });
358+
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
359+
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
360+
string.Empty,
361+
"{controller}/{action}/{id?}",
362+
new RouteValueDictionary(new { action = "TestAction" }),
363+
new RouteValueDictionary(new { controller = "TestController", nonParameter = new CustomConstraint(), id = new IntRouteConstraint() })));
364+
365+
// Act
366+
var endpoints = dataSource.Endpoints;
367+
368+
// Assert
369+
var endpoint1 = Assert.IsType<RouteEndpoint>(endpoints[0]);
370+
var routePattern1 = endpoint1.RoutePattern;
371+
Assert.Equal("TestController/{action=TestAction}/{id:?}", routePattern1.RawText);
372+
Assert.Collection(routePattern1.ParameterPolicies.OrderBy(p => p.Key),
373+
p =>
374+
{
375+
Assert.Equal("id", p.Key);
376+
Assert.IsType<IntRouteConstraint>(p.Value.Single().ParameterPolicy);
377+
},
378+
p =>
379+
{
380+
Assert.Equal("nonParameter", p.Key);
381+
Assert.IsType<CustomConstraint>(p.Value.Single().ParameterPolicy);
382+
});
383+
384+
var endpoint2 = Assert.IsType<RouteEndpoint>(endpoints[1]);
385+
var routePattern2 = endpoint2.RoutePattern;
386+
Assert.Equal("TestController", routePattern2.RawText);
387+
Assert.Collection(routePattern2.ParameterPolicies.OrderBy(p => p.Key),
388+
p =>
389+
{
390+
Assert.Equal("nonParameter", p.Key);
391+
Assert.IsType<CustomConstraint>(p.Value.Single().ParameterPolicy);
392+
});
393+
394+
var endpoint3 = Assert.IsType<RouteEndpoint>(endpoints[2]);
395+
var routePattern3 = endpoint3.RoutePattern;
396+
Assert.Equal("TestController/TestAction/{id:?}", routePattern3.RawText);
397+
Assert.Collection(routePattern3.ParameterPolicies.OrderBy(p => p.Key),
398+
p =>
399+
{
400+
Assert.Equal("id", p.Key);
401+
Assert.IsType<IntRouteConstraint>(p.Value.Single().ParameterPolicy);
402+
},
403+
p =>
404+
{
405+
Assert.Equal("nonParameter", p.Key);
406+
Assert.IsType<CustomConstraint>(p.Value.Single().ParameterPolicy);
407+
});
408+
}
409+
410+
private class CustomConstraint : IRouteConstraint
411+
{
412+
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
413+
{
414+
throw new NotImplementedException();
415+
}
416+
}
417+
314418
[Fact]
315419
public void Endpoints_SingleAction_AttributeRoute_ContainsParameterWithNullRequiredRouteValue()
316420
{

src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,32 @@ private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
2626

2727
public HttpClient Client { get; }
2828

29+
[Fact]
30+
public async Task ConventionalRoutedAction_RouteHasNonParameterConstraint_RouteConstraintRun_Allowed()
31+
{
32+
// Arrange & Act
33+
var response = await Client.GetAsync("http://localhost/NonParameterConstraintRoute/NonParameterConstraint/Index?allowed=true");
34+
35+
// Assert
36+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
37+
38+
var body = await response.Content.ReadAsStringAsync();
39+
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
40+
41+
Assert.Equal("NonParameterConstraint", result.Controller);
42+
Assert.Equal("Index", result.Action);
43+
}
44+
45+
[Fact]
46+
public async Task ConventionalRoutedAction_RouteHasNonParameterConstraint_RouteConstraintRun_Denied()
47+
{
48+
// Arrange & Act
49+
var response = await Client.GetAsync("http://localhost/NonParameterConstraintRoute/NonParameterConstraint/Index?allowed=false");
50+
51+
// Assert
52+
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
53+
}
54+
2955
[Fact]
3056
public async Task ConventionalRoutedAction_RouteContainsPage_RouteNotMatched()
3157
{
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Mvc;
9+
10+
namespace RoutingWebSite
11+
{
12+
public class NonParameterConstraintController : Controller
13+
{
14+
private readonly TestResponseGenerator _generator;
15+
16+
public NonParameterConstraintController(TestResponseGenerator generator)
17+
{
18+
_generator = generator;
19+
}
20+
21+
public IActionResult Index()
22+
{
23+
return _generator.Generate("/NonParameterConstraintRoute/NonParameterConstraint/Index");
24+
}
25+
}
26+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Routing;
6+
7+
namespace RoutingWebSite
8+
{
9+
public class QueryStringConstraint : IRouteConstraint
10+
{
11+
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
12+
{
13+
return httpContext.Request.Query["allowed"].ToString() == "true";
14+
}
15+
}
16+
}

src/Mvc/test/WebSites/RoutingWebSite/Startup.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ public void Configure(IApplicationBuilder app)
4949
{
5050
app.UseMvc(routes =>
5151
{
52+
routes.MapRoute(
53+
"NonParameterConstraintRoute",
54+
"NonParameterConstraintRoute/{controller}/{action}",
55+
defaults: null,
56+
constraints: new { controller = "NonParameterConstraint", nonParameter = new QueryStringConstraint() });
57+
5258
routes.MapRoute(
5359
"DataTokensRoute",
5460
"DataTokensRoute/{controller}/{action}",

src/Mvc/test/WebSites/RoutingWebSite/StartupWith21Compat.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ public void Configure(IApplicationBuilder app)
5454
{
5555
app.UseMvc(routes =>
5656
{
57+
routes.MapRoute(
58+
"NonParameterConstraintRoute",
59+
"NonParameterConstraintRoute/{controller}/{action}",
60+
defaults: null,
61+
constraints: new { controller = "NonParameterConstraint", nonParameter = new QueryStringConstraint() });
62+
5763
routes.MapRoute(
5864
"DataTokensRoute",
5965
"DataTokensRoute/{controller}/{action}",

0 commit comments

Comments
 (0)