Skip to content

Commit bb3f0d6

Browse files
authored
Exclude regex and alpha constraints when SlimBuilder is used. (#46227)
1 parent e78564f commit bb3f0d6

20 files changed

+340
-10
lines changed

src/DefaultBuilder/src/WebApplicationBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ internal WebApplicationBuilder(WebApplicationOptions options, bool slim, Action<
135135
bootstrapHostBuilder.ConfigureSlimWebHost(
136136
webHostBuilder =>
137137
{
138-
AspNetCore.WebHost.UseKestrel(webHostBuilder);
138+
AspNetCore.WebHost.ConfigureWebDefaultsCore(webHostBuilder);
139139

140140
webHostBuilder.Configure(ConfigureEmptyApplication);
141141

src/DefaultBuilder/src/WebHost.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,14 +223,17 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder)
223223
}
224224
});
225225

226-
UseKestrel(builder);
226+
ConfigureWebDefaultsCore(builder, services =>
227+
{
228+
services.AddRouting();
229+
});
227230

228231
builder
229232
.UseIIS()
230233
.UseIISIntegration();
231234
}
232235

233-
internal static void UseKestrel(IWebHostBuilder builder)
236+
internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder, Action<IServiceCollection>? configureRouting = null)
234237
{
235238
builder.UseKestrel((builderContext, options) =>
236239
{
@@ -257,7 +260,17 @@ internal static void UseKestrel(IWebHostBuilder builder)
257260
services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
258261
services.AddTransient<IConfigureOptions<ForwardedHeadersOptions>, ForwardedHeadersOptionsSetup>();
259262

260-
services.AddRouting();
263+
// Provide a way for the default host builder to configure routing. This probably means calling AddRouting.
264+
// A lambda is used here because we don't want to reference AddRouting directly because of trimming.
265+
// This avoids the overhead of calling AddRoutingCore multiple times on app startup.
266+
if (configureRouting == null)
267+
{
268+
services.AddRoutingCore();
269+
}
270+
else
271+
{
272+
configureRouting(services);
273+
}
261274
});
262275
}
263276

src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Microsoft.AspNetCore.Http;
1919
using Microsoft.AspNetCore.Http.Features;
2020
using Microsoft.AspNetCore.Routing;
21+
using Microsoft.AspNetCore.Routing.Constraints;
2122
using Microsoft.AspNetCore.TestHost;
2223
using Microsoft.AspNetCore.Testing;
2324
using Microsoft.AspNetCore.Tests;
@@ -2351,6 +2352,141 @@ public async Task SupportsDisablingMiddlewareAutoRegistration()
23512352
Assert.True(app.Properties.ContainsKey("__AuthorizationMiddlewareSet"));
23522353
}
23532354

2355+
[Fact]
2356+
public async Task UsingCreateBuilderResultsInRegexConstraintBeingPresent()
2357+
{
2358+
var builder = WebApplication.CreateBuilder();
2359+
builder.WebHost.UseTestServer();
2360+
2361+
var app = builder.Build();
2362+
2363+
var chosenRoute = string.Empty;
2364+
2365+
app.Use((context, next) =>
2366+
{
2367+
chosenRoute = context.GetEndpoint()?.DisplayName;
2368+
return next(context);
2369+
});
2370+
2371+
app.MapGet("/products/{productId:regex(^[a-z]{{4}}\\d{{4}}$)}", (string productId) => productId).WithDisplayName("RegexRoute");
2372+
2373+
await app.StartAsync();
2374+
2375+
var client = app.GetTestClient();
2376+
2377+
_ = await client.GetAsync("https://localhost/products/abcd1234");
2378+
Assert.Equal("RegexRoute", chosenRoute);
2379+
}
2380+
2381+
[Fact]
2382+
public async Task UsingCreateSlimBuilderResultsInAlphaConstraintStillWorking()
2383+
{
2384+
var builder = WebApplication.CreateSlimBuilder();
2385+
builder.WebHost.UseTestServer();
2386+
2387+
var app = builder.Build();
2388+
2389+
var chosenRoute = string.Empty;
2390+
2391+
app.Use((context, next) =>
2392+
{
2393+
chosenRoute = context.GetEndpoint()?.DisplayName;
2394+
return next(context);
2395+
});
2396+
2397+
app.MapGet("/products/{productId:alpha:minlength(4):maxlength(4)}", (string productId) => productId).WithDisplayName("AlphaRoute");
2398+
2399+
await app.StartAsync();
2400+
2401+
var client = app.GetTestClient();
2402+
2403+
_ = await client.GetAsync("https://localhost/products/abcd");
2404+
Assert.Equal("AlphaRoute", chosenRoute);
2405+
}
2406+
2407+
[Fact]
2408+
public async Task UsingCreateSlimBuilderResultsInErrorWhenTryingToUseRegexConstraint()
2409+
{
2410+
var builder = WebApplication.CreateSlimBuilder();
2411+
builder.WebHost.UseTestServer();
2412+
2413+
var app = builder.Build();
2414+
2415+
app.MapGet("/products/{productId:regex(^[a-z]{{4}}\\d{{4}}$)}", (string productId) => productId).WithDisplayName("AlphaRoute");
2416+
2417+
await app.StartAsync();
2418+
2419+
var client = app.GetTestClient();
2420+
2421+
var ex = await Record.ExceptionAsync(async () =>
2422+
{
2423+
_ = await client.GetAsync("https://localhost/products/abcd1234");
2424+
});
2425+
2426+
Assert.IsType<RouteCreationException>(ex);
2427+
Assert.IsType<InvalidOperationException>(ex.InnerException.InnerException);
2428+
Assert.Equal(
2429+
"A route parameter uses the regex constraint, which isn't registered. If this application was configured using CreateSlimBuilder(...) or AddRoutingCore(...) then this constraint is not registered by default. To use the regex constraint, configure route options at app startup: services.Configure<RouteOptions>(options => options.SetParameterPolicy<RegexInlineRouteConstraint>(\"regex\"));",
2430+
ex.InnerException.InnerException.Message);
2431+
}
2432+
2433+
[Fact]
2434+
public async Task UsingCreateSlimBuilderWorksIfRegexConstraintAddedViaAddRouting()
2435+
{
2436+
var builder = WebApplication.CreateSlimBuilder();
2437+
builder.Services.AddRouting();
2438+
builder.WebHost.UseTestServer();
2439+
2440+
var app = builder.Build();
2441+
2442+
var chosenRoute = string.Empty;
2443+
2444+
app.Use((context, next) =>
2445+
{
2446+
chosenRoute = context.GetEndpoint()?.DisplayName;
2447+
return next(context);
2448+
});
2449+
2450+
app.MapGet("/products/{productId:regex(^[a-z]{{4}}\\d{{4}}$)}", (string productId) => productId).WithDisplayName("RegexRoute");
2451+
2452+
await app.StartAsync();
2453+
2454+
var client = app.GetTestClient();
2455+
2456+
_ = await client.GetAsync("https://localhost/products/abcd1234");
2457+
Assert.Equal("RegexRoute", chosenRoute);
2458+
}
2459+
2460+
[Fact]
2461+
public async Task UsingCreateSlimBuilderWorksIfRegexConstraintAddedViaAddRoutingCoreWithActionDelegate()
2462+
{
2463+
var builder = WebApplication.CreateSlimBuilder();
2464+
builder.Services.AddRoutingCore().Configure<RouteOptions>(options =>
2465+
{
2466+
options.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
2467+
});
2468+
builder.WebHost.UseTestServer();
2469+
2470+
var app = builder.Build();
2471+
2472+
var chosenRoute = string.Empty;
2473+
2474+
app.Use((context, next) =>
2475+
{
2476+
chosenRoute = context.GetEndpoint()?.DisplayName;
2477+
return next(context);
2478+
});
2479+
2480+
app.MapGet("/products/{productId:regex(^[a-z]{{4}}\\d{{4}}$)}", (string productId) => productId).WithDisplayName("RegexRoute");
2481+
2482+
await app.StartAsync();
2483+
2484+
var client = app.GetTestClient();
2485+
2486+
_ = await client.GetAsync("https://localhost/products/abcd1234");
2487+
Assert.Equal("RegexRoute", chosenRoute);
2488+
}
2489+
23542490
private class UberHandler : AuthenticationHandler<AuthenticationSchemeOptions>
23552491
{
23562492
public UberHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { }

src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.AspNetCore.HostFiltering;
88
using Microsoft.AspNetCore.Hosting;
99
using Microsoft.AspNetCore.Routing;
10+
using Microsoft.AspNetCore.Routing.Constraints;
1011
using Microsoft.AspNetCore.TestHost;
1112
using Microsoft.AspNetCore.Testing;
1213
using Microsoft.Extensions.Configuration;
@@ -110,6 +111,19 @@ public void CreateDefaultBuilder_RegistersEventSourceLogger()
110111
args.Payload.OfType<string>().Any(p => p.Contains("Request starting")));
111112
}
112113

114+
[Fact]
115+
public void WebHost_CreateDefaultBuilder_ConfiguresRegexInlineRouteConstraint_ByDefault()
116+
{
117+
var host = WebHost.CreateDefaultBuilder()
118+
.Configure(_ => { })
119+
.Build();
120+
121+
var routeOptions = host.Services.GetService<IOptions<RouteOptions>>();
122+
123+
Assert.True(routeOptions.Value.ConstraintMap.ContainsKey("regex"));
124+
Assert.Equal(typeof(RegexInlineRouteConstraint), routeOptions.Value.ConstraintMap["regex"]);
125+
}
126+
113127
private class TestEventListener : EventListener
114128
{
115129
private volatile bool _disposed;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Routing.Constraints;
7+
8+
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint
9+
{
10+
public RegexErrorStubRouteConstraint(string _)
11+
{
12+
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
13+
}
14+
15+
bool IRouteConstraint.Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
16+
{
17+
// Should never get called, but is same as throw in constructor in case constructor is changed.
18+
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
19+
}
20+
}

src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ public static class RoutingServiceCollectionExtensions
2626
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
2727
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
2828
public static IServiceCollection AddRouting(this IServiceCollection services)
29+
{
30+
services.AddRoutingCore();
31+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<RouteOptions>, RegexInlineRouteConstraintSetup>());
32+
return services;
33+
}
34+
35+
/// <summary>
36+
/// Adds services required for routing requests. This is similar to
37+
/// <see cref="AddRouting(IServiceCollection)" /> except that it
38+
/// excludes certain options that can be opted in separately, if needed.
39+
/// </summary>
40+
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
41+
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
42+
public static IServiceCollection AddRoutingCore(this IServiceCollection services)
2943
{
3044
ArgumentNullException.ThrowIfNull(services);
3145

src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@
5151
<InternalsVisibleTo Include="Microsoft.AspNetCore.Routing.Tests" />
5252
<InternalsVisibleTo Include="Microsoft.AspNetCore.Mvc.ApiExplorer.Test" />
5353
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" Key="$(MoqPublicKey)" />
54+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Mvc.Test" />
5455
</ItemGroup>
5556
</Project>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
#nullable enable
22
Microsoft.AspNetCore.Routing.RouteHandlerServices
33
static Microsoft.AspNetCore.Routing.RouteHandlerServices.Map(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! handler, System.Collections.Generic.IEnumerable<string!>! httpMethods, System.Func<System.Reflection.MethodInfo!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions?, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult!>! populateMetadata, System.Func<System.Delegate!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions!, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult?, Microsoft.AspNetCore.Http.RequestDelegateResult!>! createRequestDelegate) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
4+
static Microsoft.Extensions.DependencyInjection.RoutingServiceCollectionExtensions.AddRoutingCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Routing.Constraints;
5+
using Microsoft.AspNetCore.Routing;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace Microsoft.Extensions.DependencyInjection;
9+
10+
internal sealed class RegexInlineRouteConstraintSetup : IConfigureOptions<RouteOptions>
11+
{
12+
public void Configure(RouteOptions options)
13+
{
14+
var existingRegexConstraintType = options.TrimmerSafeConstraintMap["regex"];
15+
16+
// Don't override regex constraint if it has already been overridden
17+
// this behavior here is just to add it back in if someone calls AddRouting(...)
18+
// after setting up routing with AddRoutingCore(...).
19+
if (existingRegexConstraintType == typeof(RegexErrorStubRouteConstraint))
20+
{
21+
options.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
22+
}
23+
}
24+
}

src/Http/Routing/src/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,7 @@
249249
<data name="RouteEndpointDataSource_RequestDelegateCannotBeCalledBeforeBuild" xml:space="preserve">
250250
<value>This RequestDelegate cannot be called before the final endpoint is built.</value>
251251
</data>
252+
<data name="RegexRouteContraint_NotConfigured" xml:space="preserve">
253+
<value>A route parameter uses the regex constraint, which isn't registered. If this application was configured using CreateSlimBuilder(...) or AddRoutingCore(...) then this constraint is not registered by default. To use the regex constraint, configure route options at app startup: services.Configure&lt;RouteOptions&gt;(options =&gt; options.SetParameterPolicy&lt;RegexInlineRouteConstraint&gt;("regex"));</value>
254+
</data>
252255
</root>

src/Http/Routing/src/RouteOptions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,10 @@ private static IDictionary<string, Type> GetDefaultConstraintMap()
113113
AddConstraint<MaxRouteConstraint>(defaults, "max");
114114
AddConstraint<RangeRouteConstraint>(defaults, "range");
115115

116-
// Regex-based constraints
116+
// The alpha constraint uses a compiled regex which has a minimal size cost.
117117
AddConstraint<AlphaRouteConstraint>(defaults, "alpha");
118-
AddConstraint<RegexInlineRouteConstraint>(defaults, "regex");
118+
119+
AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message.
119120

120121
AddConstraint<RequiredRouteConstraint>(defaults, "required");
121122

src/Http/Routing/test/UnitTests/DefaultInlineConstraintResolverTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public class DefaultInlineConstraintResolverTest
1616
public DefaultInlineConstraintResolverTest()
1717
{
1818
var routeOptions = new RouteOptions();
19+
routeOptions.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
20+
1921
_constraintResolver = GetInlineConstraintResolver(routeOptions);
2022
}
2123

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3546,7 +3546,8 @@ private static DefaultParameterPolicyFactory CreateParameterPolicyFactory()
35463546
ConstraintMap =
35473547
{
35483548
["slugify"] = typeof(SlugifyParameterTransformer),
3549-
["upper-case"] = typeof(UpperCaseParameterTransform)
3549+
["upper-case"] = typeof(UpperCaseParameterTransform),
3550+
["regex"] = typeof(RegexInlineRouteConstraint) // Regex not included by default since introduction of CreateSlimBuilder
35503551
}
35513552
}),
35523553
serviceCollection.BuildServiceProvider());

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Routing.Constraints;
56
using Microsoft.AspNetCore.Routing.Patterns;
67
using Microsoft.AspNetCore.Routing.TestObjects;
78
using Microsoft.Extensions.Options;
@@ -15,7 +16,9 @@ internal class RouteMatcherBuilder : MatcherBuilder
1516

1617
public RouteMatcherBuilder()
1718
{
18-
_constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), new TestServiceProvider());
19+
var routeOptions = new RouteOptions();
20+
routeOptions.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
21+
_constraintResolver = new DefaultInlineConstraintResolver(Options.Create(routeOptions), new TestServiceProvider());
1922
_endpoints = new List<RouteEndpoint>();
2023
}
2124

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Routing.Constraints;
56
using Microsoft.AspNetCore.Routing.Template;
67
using Microsoft.AspNetCore.Routing.TestObjects;
78
using Microsoft.AspNetCore.Routing.Tree;
@@ -27,10 +28,13 @@ public override void AddEndpoint(RouteEndpoint endpoint)
2728

2829
public override Matcher Build()
2930
{
31+
var routeOptions = new RouteOptions();
32+
routeOptions.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
33+
3034
var builder = new TreeRouteBuilder(
3135
NullLoggerFactory.Instance,
3236
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
33-
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), new TestServiceProvider()));
37+
new DefaultInlineConstraintResolver(Options.Create(routeOptions), new TestServiceProvider()));
3438

3539
var selector = new DefaultEndpointSelector();
3640

src/Http/Routing/test/UnitTests/RouteTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1861,6 +1861,7 @@ private static IInlineConstraintResolver GetInlineConstraintResolver()
18611861
private static void ConfigureRouteOptions(RouteOptions options)
18621862
{
18631863
options.ConstraintMap["test-policy"] = typeof(TestPolicy);
1864+
options.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
18641865
}
18651866

18661867
private class TestPolicy : IParameterPolicy

0 commit comments

Comments
 (0)