Skip to content

Exclude regex and alpha constraints when SlimBuilder is used. #46227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a7d32fd
Implement AddRoutingCore to support excluding Regex.
mitchdenny Jan 30, 2023
473587b
Rename UseKestrel.
mitchdenny Jan 30, 2023
63661ce
Add XML docs and API.
mitchdenny Jan 31, 2023
ea59a01
Merge branch 'main' into exclude-regex-alpha-on-slim-builder
mitchdenny Jan 31, 2023
e9ac4e7
Added test cases to cover removing regex.
mitchdenny Feb 1, 2023
1d723ee
Update src/Http/Routing/src/RouteOptions.cs
mitchdenny Feb 1, 2023
008f937
Cleaned up some tests and added message assertion.
mitchdenny Feb 1, 2023
50395a0
Merge branch 'exclude-regex-alpha-on-slim-builder' of https://github.…
mitchdenny Feb 1, 2023
fc44ea3
Remove AddRegexConstraint and use SetParameterPolicy instead.
mitchdenny Feb 3, 2023
78056ab
Introduce dummy route constraint to give helpful error message.
mitchdenny Feb 3, 2023
ddf8fd9
Update src/Http/Routing/src/RouteOptions.cs
mitchdenny Feb 3, 2023
6c9d3d2
Update src/Http/Routing/src/RouteOptions.cs
mitchdenny Feb 3, 2023
3070320
Update src/Http/Routing/src/ParameterPolicyActivator.cs
mitchdenny Feb 5, 2023
e6d632f
Update src/Http/Routing/src/RouteOptions.cs
mitchdenny Feb 5, 2023
03bfcd9
Update src/Http/Routing/src/Resources.resx
mitchdenny Feb 5, 2023
1c94ae4
Throw on first request.
mitchdenny Feb 5, 2023
d836911
Update src/Http/Routing/src/DependencyInjection/RoutingServiceCollect…
mitchdenny Feb 6, 2023
233903a
Update src/Http/Routing/src/DependencyInjection/RoutingServiceCollect…
mitchdenny Feb 6, 2023
91b00ab
Update src/Http/Routing/src/RouteOptions.cs
mitchdenny Feb 6, 2023
a18ac7c
Rename AddRoutingCore to AddRoutingSlim.
mitchdenny Feb 9, 2023
a060ef7
Revert back to AddRoutingCore(...).
mitchdenny Feb 9, 2023
44f29ef
Update src/Http/Routing/src/RouteOptions.cs
mitchdenny Feb 9, 2023
63b6f59
Update src/Http/Routing/src/DependencyInjection/RoutingServiceCollect…
mitchdenny Feb 10, 2023
bfcc4ff
Moved error stub to separate file.
mitchdenny Feb 10, 2023
6c3f447
Applied Stephen's changes.
mitchdenny Feb 10, 2023
5b36ab8
Responding to PR feedback around handling edge cases.
mitchdenny Feb 10, 2023
346876a
Swap out call to AddRouting(...) with call to Configure<RouteOptions>…
mitchdenny Feb 10, 2023
029b2dc
Make sure WebHost.CreateDefaultBuilder(...) configures RegexInlineRou…
mitchdenny Feb 10, 2023
4b7991d
Fix using constraint build error.
mitchdenny Feb 10, 2023
dc6c143
Update src/Http/Routing/src/Constraints/RegexErrorStubRouteConstraint.cs
mitchdenny Feb 11, 2023
ae15aec
Update src/Http/Routing/test/UnitTests/RoutingServiceCollectionExtens…
mitchdenny Feb 12, 2023
d24a958
Update src/Http/Routing/src/RouteOptions.cs
mitchdenny Feb 12, 2023
e0ca212
Update src/DefaultBuilder/src/WebApplicationBuilder.cs
mitchdenny Feb 12, 2023
aa74554
Updates from PR feedback.
mitchdenny Feb 13, 2023
94dcdef
Revert back to calling AddRouting(...) from WebHost for now.
mitchdenny Feb 14, 2023
f2cf03e
Change back to IConfigureOptions.
mitchdenny Feb 16, 2023
8819aae
We did it!
mitchdenny Feb 17, 2023
b9e4f5a
Forogt to actually call configureRouting(...) :)
mitchdenny Feb 17, 2023
5254b6f
Update src/DefaultBuilder/src/WebHost.cs
mitchdenny Feb 17, 2023
9adca9c
Update src/DefaultBuilder/src/WebHost.cs
mitchdenny Feb 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/DefaultBuilder/src/WebApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ internal WebApplicationBuilder(WebApplicationOptions options, bool slim, Action<
bootstrapHostBuilder.ConfigureSlimWebHost(
webHostBuilder =>
{
AspNetCore.WebHost.UseKestrel(webHostBuilder);
AspNetCore.WebHost.ConfigureWebDefaultsCore(webHostBuilder);

webHostBuilder.Configure(ConfigureEmptyApplication);

Expand Down
19 changes: 16 additions & 3 deletions src/DefaultBuilder/src/WebHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,17 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder)
}
});

UseKestrel(builder);
ConfigureWebDefaultsCore(builder, services =>
{
services.AddRouting();
});

builder
.UseIIS()
.UseIISIntegration();
}

internal static void UseKestrel(IWebHostBuilder builder)
internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder, Action<IServiceCollection>? configureRouting = null)
{
builder.UseKestrel((builderContext, options) =>
{
Expand All @@ -257,7 +260,17 @@ internal static void UseKestrel(IWebHostBuilder builder)
services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
services.AddTransient<IConfigureOptions<ForwardedHeadersOptions>, ForwardedHeadersOptionsSetup>();

services.AddRouting();
// Provide a way for the default host builder to configure routing. This probably means calling AddRouting.
// A lambda is used here because we don't want to reference AddRouting directly because of trimming.
// This avoids the overhead of calling AddRoutingCore multiple times on app startup.
if (configureRouting == null)
{
services.AddRoutingCore();
}
else
{
configureRouting(services);
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Tests;
Expand Down Expand Up @@ -2267,6 +2268,141 @@ public async Task SupportsDisablingMiddlewareAutoRegistration()
Assert.True(app.Properties.ContainsKey("__AuthorizationMiddlewareSet"));
}

[Fact]
public async Task UsingCreateBuilderResultsInRegexConstraintBeingPresent()
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();

var app = builder.Build();

var chosenRoute = string.Empty;

app.Use((context, next) =>
{
chosenRoute = context.GetEndpoint()?.DisplayName;
return next(context);
});

app.MapGet("/products/{productId:regex(^[a-z]{{4}}\\d{{4}}$)}", (string productId) => productId).WithDisplayName("RegexRoute");

await app.StartAsync();

var client = app.GetTestClient();

_ = await client.GetAsync("https://localhost/products/abcd1234");
Assert.Equal("RegexRoute", chosenRoute);
}

[Fact]
public async Task UsingCreateSlimBuilderResultsInAlphaConstraintStillWorking()
{
var builder = WebApplication.CreateSlimBuilder();
builder.WebHost.UseTestServer();

var app = builder.Build();

var chosenRoute = string.Empty;

app.Use((context, next) =>
{
chosenRoute = context.GetEndpoint()?.DisplayName;
return next(context);
});

app.MapGet("/products/{productId:alpha:minlength(4):maxlength(4)}", (string productId) => productId).WithDisplayName("AlphaRoute");

await app.StartAsync();

var client = app.GetTestClient();

_ = await client.GetAsync("https://localhost/products/abcd");
Assert.Equal("AlphaRoute", chosenRoute);
}

[Fact]
public async Task UsingCreateSlimBuilderResultsInErrorWhenTryingToUseRegexConstraint()
{
var builder = WebApplication.CreateSlimBuilder();
builder.WebHost.UseTestServer();

var app = builder.Build();

app.MapGet("/products/{productId:regex(^[a-z]{{4}}\\d{{4}}$)}", (string productId) => productId).WithDisplayName("AlphaRoute");

await app.StartAsync();

var client = app.GetTestClient();

var ex = await Record.ExceptionAsync(async () =>
{
_ = await client.GetAsync("https://localhost/products/abcd1234");
});

Assert.IsType<RouteCreationException>(ex);
Assert.IsType<InvalidOperationException>(ex.InnerException.InnerException);
Assert.Equal(
"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\"));",
ex.InnerException.InnerException.Message);
}

[Fact]
public async Task UsingCreateSlimBuilderWorksIfRegexConstraintAddedViaAddRouting()
{
var builder = WebApplication.CreateSlimBuilder();
builder.Services.AddRouting();
builder.WebHost.UseTestServer();

var app = builder.Build();

var chosenRoute = string.Empty;

app.Use((context, next) =>
{
chosenRoute = context.GetEndpoint()?.DisplayName;
return next(context);
});

app.MapGet("/products/{productId:regex(^[a-z]{{4}}\\d{{4}}$)}", (string productId) => productId).WithDisplayName("RegexRoute");

await app.StartAsync();

var client = app.GetTestClient();

_ = await client.GetAsync("https://localhost/products/abcd1234");
Assert.Equal("RegexRoute", chosenRoute);
}

[Fact]
public async Task UsingCreateSlimBuilderWorksIfRegexConstraintAddedViaAddRoutingCoreWithActionDelegate()
{
var builder = WebApplication.CreateSlimBuilder();
builder.Services.AddRoutingCore().Configure<RouteOptions>(options =>
{
options.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
});
builder.WebHost.UseTestServer();

var app = builder.Build();

var chosenRoute = string.Empty;

app.Use((context, next) =>
{
chosenRoute = context.GetEndpoint()?.DisplayName;
return next(context);
});

app.MapGet("/products/{productId:regex(^[a-z]{{4}}\\d{{4}}$)}", (string productId) => productId).WithDisplayName("RegexRoute");

await app.StartAsync();

var client = app.GetTestClient();

_ = await client.GetAsync("https://localhost/products/abcd1234");
Assert.Equal("RegexRoute", chosenRoute);
}

private class UberHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public UberHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.AspNetCore.HostFiltering;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -110,6 +111,19 @@ public void CreateDefaultBuilder_RegistersEventSourceLogger()
args.Payload.OfType<string>().Any(p => p.Contains("Request starting")));
}

[Fact]
public void WebHost_CreateDefaultBuilder_ConfiguresRegexInlineRouteConstraint_ByDefault()
{
var host = WebHost.CreateDefaultBuilder()
.Configure(_ => { })
.Build();

var routeOptions = host.Services.GetService<IOptions<RouteOptions>>();

Assert.True(routeOptions.Value.ConstraintMap.ContainsKey("regex"));
Assert.Equal(typeof(RegexInlineRouteConstraint), routeOptions.Value.ConstraintMap["regex"]);
}

private class TestEventListener : EventListener
{
private volatile bool _disposed;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing.Constraints;

internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint
{
public RegexErrorStubRouteConstraint(string _)
{
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
}

bool IRouteConstraint.Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
// Should never get called, but is same as throw in constructor in case constructor is changed.
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ public static class RoutingServiceCollectionExtensions
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddRouting(this IServiceCollection services)
{
services.AddRoutingCore();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<RouteOptions>, RegexInlineRouteConstraintSetup>());
return services;
}

/// <summary>
/// Adds services required for routing requests. This is similar to
/// <see cref="AddRouting(IServiceCollection)" /> except that it
/// excludes certain options that can be opted in separately, if needed.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddRoutingCore(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);

Expand Down
1 change: 1 addition & 0 deletions src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@
<InternalsVisibleTo Include="Microsoft.AspNetCore.Routing.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Mvc.ApiExplorer.Test" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" Key="$(MoqPublicKey)" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Mvc.Test" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions src/Http/Routing/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#nullable enable
Microsoft.AspNetCore.Routing.RouteHandlerServices
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!
static Microsoft.Extensions.DependencyInjection.RoutingServiceCollectionExtensions.AddRoutingCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
24 changes: 24 additions & 0 deletions src/Http/Routing/src/RegexInlineRouteConstraintSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection;

internal sealed class RegexInlineRouteConstraintSetup : IConfigureOptions<RouteOptions>
{
public void Configure(RouteOptions options)
{
var existingRegexConstraintType = options.TrimmerSafeConstraintMap["regex"];

// Don't override regex constraint if it has already been overridden
// this behavior here is just to add it back in if someone calls AddRouting(...)
// after setting up routing with AddRoutingCore(...).
if (existingRegexConstraintType == typeof(RegexErrorStubRouteConstraint))
{
options.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
}
}
}
3 changes: 3 additions & 0 deletions src/Http/Routing/src/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,7 @@
<data name="RouteEndpointDataSource_RequestDelegateCannotBeCalledBeforeBuild" xml:space="preserve">
<value>This RequestDelegate cannot be called before the final endpoint is built.</value>
</data>
<data name="RegexRouteContraint_NotConfigured" xml:space="preserve">
<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>
</data>
</root>
5 changes: 3 additions & 2 deletions src/Http/Routing/src/RouteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,10 @@ private static IDictionary<string, Type> GetDefaultConstraintMap()
AddConstraint<MaxRouteConstraint>(defaults, "max");
AddConstraint<RangeRouteConstraint>(defaults, "range");

// Regex-based constraints
// The alpha constraint uses a compiled regex which has a minimal size cost.
AddConstraint<AlphaRouteConstraint>(defaults, "alpha");
AddConstraint<RegexInlineRouteConstraint>(defaults, "regex");

AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class DefaultInlineConstraintResolverTest
public DefaultInlineConstraintResolverTest()
{
var routeOptions = new RouteOptions();
routeOptions.SetParameterPolicy<RegexInlineRouteConstraint>("regex");

_constraintResolver = GetInlineConstraintResolver(routeOptions);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3546,7 +3546,8 @@ private static DefaultParameterPolicyFactory CreateParameterPolicyFactory()
ConstraintMap =
{
["slugify"] = typeof(SlugifyParameterTransformer),
["upper-case"] = typeof(UpperCaseParameterTransform)
["upper-case"] = typeof(UpperCaseParameterTransform),
["regex"] = typeof(RegexInlineRouteConstraint) // Regex not included by default since introduction of CreateSlimBuilder
}
}),
serviceCollection.BuildServiceProvider());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.Extensions.Options;
Expand All @@ -15,7 +16,9 @@ internal class RouteMatcherBuilder : MatcherBuilder

public RouteMatcherBuilder()
{
_constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), new TestServiceProvider());
var routeOptions = new RouteOptions();
routeOptions.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
_constraintResolver = new DefaultInlineConstraintResolver(Options.Create(routeOptions), new TestServiceProvider());
_endpoints = new List<RouteEndpoint>();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.TestObjects;
using Microsoft.AspNetCore.Routing.Tree;
Expand All @@ -27,10 +28,13 @@ public override void AddEndpoint(RouteEndpoint endpoint)

public override Matcher Build()
{
var routeOptions = new RouteOptions();
routeOptions.SetParameterPolicy<RegexInlineRouteConstraint>("regex");

var builder = new TreeRouteBuilder(
NullLoggerFactory.Instance,
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), new TestServiceProvider()));
new DefaultInlineConstraintResolver(Options.Create(routeOptions), new TestServiceProvider()));

var selector = new DefaultEndpointSelector();

Expand Down
1 change: 1 addition & 0 deletions src/Http/Routing/test/UnitTests/RouteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1861,6 +1861,7 @@ private static IInlineConstraintResolver GetInlineConstraintResolver()
private static void ConfigureRouteOptions(RouteOptions options)
{
options.ConstraintMap["test-policy"] = typeof(TestPolicy);
options.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
}

private class TestPolicy : IParameterPolicy
Expand Down
Loading