Skip to content

Commit

Permalink
Exclude regex and alpha constraints when SlimBuilder is used. (#46227)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchdenny authored Feb 17, 2023
1 parent e78564f commit bb3f0d6
Show file tree
Hide file tree
Showing 20 changed files with 340 additions and 10 deletions.
2 changes: 1 addition & 1 deletion src/DefaultBuilder/src/WebApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,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 @@ -18,6 +18,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 @@ -2351,6 +2352,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
14 changes: 14 additions & 0 deletions src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostTests.cs
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
20 changes: 20 additions & 0 deletions src/Http/Routing/src/Constraints/RegexErrorStubRouteConstraint.cs
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 @@ -51,5 +51,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

0 comments on commit bb3f0d6

Please sign in to comment.