Skip to content

Commit f51ae60

Browse files
authoredFeb 1, 2024
[Blazor] Enable regex constraint in Blazor routing (#53533)
This was discovered when working on the issue for #53138. The regex constraint is not enabled by default on the blazor router. As a result if a route uses a regex constraint, it will work on SSR but will fail the moment the Blazor router tries to construct the route. The fix enables the regex constraint on the blazor router, unconditionally for server, and behind a feature flag for webassembly. This is because enabling the regex constraint adds +80kb to the payload due to the inclusion of the System.Text.RegularExpressions assembly.
1 parent ca98885 commit f51ae60

File tree

14 files changed

+147
-10
lines changed

14 files changed

+147
-10
lines changed
 

‎AspNetCore.sln

+1
Original file line numberDiff line numberDiff line change
@@ -11405,6 +11405,7 @@ Global
1140511405
{9788C76F-658B-4441-88F8-22C6B86FAD27} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
1140611406
{1970D5CD-D9A4-4673-A297-179BB04199F4} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
1140711407
{A40350FE-4334-4007-B1C3-6BEB1B070309} = {7D2B0799-A634-42AC-AE77-5D167BA51389}
11408+
{A40350FE-4334-4007-B1C3-6BEB1B070308} = {6126DCE4-9692-4EE2-B240-C65743572995}
1140811409
{C1E7F837-6988-43E2-9E1C-7302DB484F99} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}
1140911410
{2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E} = {C1E7F837-6988-43E2-9E1C-7302DB484F99}
1141011411
{7CB09412-C9B0-47E8-A8C3-311AA4CFDE04} = {C1E7F837-6988-43E2-9E1C-7302DB484F99}

‎src/Components/Components/src/Microsoft.AspNetCore.Components.Routing.targets

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
<Compile Include="$(RoutingSourceRoot)Constraints\**\*.cs" LinkBase="Routing\Constraints" />
3434

3535
<Compile Remove="$(RoutingSourceRoot)Constraints\HttpMethodRouteConstraint.cs" />
36-
<Compile Remove="$(RoutingSourceRoot)Constraints\RegexErrorStubRouteConstraint.cs" />
3736
<Compile Remove="$(RoutingSourceRoot)Constraints\RequiredRouteConstraint.cs" />
3837
<Compile Remove="$(RoutingSourceRoot)Constraints\StringRouteConstraint.cs" />
3938
</ItemGroup>

‎src/Components/Components/src/Properties/ILLink.Substitutions.xml

+3
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@
33
<type fullname="Microsoft.AspNetCore.Components.HotReload.HotReloadManager" feature="System.Reflection.Metadata.MetadataUpdater.IsSupported" featurevalue="false">
44
<method signature="System.Boolean get_MetadataUpdateSupported()" body="stub" value="false" />
55
</type>
6+
<type fullname="Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport" feature="Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport" featurevalue="false">
7+
<method signature="System.Boolean get_IsEnabled()" body="stub" value="false" />
8+
</type>
69
</assembly>
710
</linker>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
namespace Microsoft.AspNetCore.Components.Routing;
5+
6+
internal static class RegexConstraintSupport
7+
{
8+
// We should check the AppContext switch in the implementation, but it doesn't flow to the wasm runtime
9+
// during development, so we can't offer a better experience (build time message to enable the switch)
10+
// until the context switch flows to the runtime.
11+
// This value gets updated by the linker when the app is trimmed, so the code will always be removed from
12+
// webassembly unless the switch is enabled.
13+
public static bool IsEnabled =>
14+
AppContext.TryGetSwitch("Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", out var enabled) && enabled;
15+
}

‎src/Components/Components/src/Routing/Resources.resx

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@
154154
<value>Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.</value>
155155
</data>
156156
<data name="RegexRouteContraint_NotConfigured" xml:space="preserve">
157-
<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>
157+
<value>A route parameter uses the regex constraint, which isn't registered. To enable it add the property 'BlazorRoutingEnableRegexConstraint' to your project file inside a `PropertyGroup`.</value>
158158
</data>
159159
<data name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve">
160160
<value>Value must be greater than or equal to {0}.</value>

‎src/Components/Components/src/Routing/RouteTableFactory.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.Extensions.Logging;
1414
using Microsoft.Extensions.Options;
1515
using static Microsoft.AspNetCore.Internal.LinkerFlags;
16+
using Microsoft.AspNetCore.Routing.Constraints;
1617

1718
namespace Microsoft.AspNetCore.Components;
1819

@@ -112,9 +113,14 @@ private static string[] GetTemplates(Type componentType)
112113
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Application code does not get trimmed, and the framework does not define routable components.")]
113114
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler, IServiceProvider serviceProvider)
114115
{
116+
var routeOptions = Options.Create(new RouteOptions());
117+
if (!OperatingSystem.IsBrowser() || RegexConstraintSupport.IsEnabled)
118+
{
119+
routeOptions.Value.SetParameterPolicy("regex", typeof(RegexInlineRouteConstraint));
120+
}
115121
var builder = new TreeRouteBuilder(
116122
serviceProvider.GetRequiredService<ILoggerFactory>(),
117-
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), serviceProvider));
123+
new DefaultInlineConstraintResolver(routeOptions, serviceProvider));
118124

119125
foreach (var (type, templates) in templatesByHandler)
120126
{

‎src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

+23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using System.Globalization;
6+
using System.Reflection;
67
using System.Text.Json;
78
using Microsoft.AspNetCore.Components.Forms;
89
using Microsoft.AspNetCore.Components.Infrastructure;
@@ -76,6 +77,12 @@ internal WebAssemblyHostBuilder(
7677
Services = new ServiceCollection();
7778
Logging = new LoggingBuilder(Services);
7879

80+
var assembly = Assembly.GetEntryAssembly();
81+
if (assembly != null)
82+
{
83+
InitializeRoutingAppContextSwitch(assembly);
84+
}
85+
7986
InitializeWebAssemblyRenderer();
8087

8188
// Retrieve required attributes from JSRuntimeInvoker
@@ -93,6 +100,22 @@ internal WebAssemblyHostBuilder(
93100
};
94101
}
95102

103+
private static void InitializeRoutingAppContextSwitch(Assembly assembly)
104+
{
105+
var assemblyMetadataAttributes = assembly.GetCustomAttributes<AssemblyMetadataAttribute>();
106+
foreach (var ama in assemblyMetadataAttributes)
107+
{
108+
if (string.Equals(ama.Key, "Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", StringComparison.Ordinal))
109+
{
110+
if (ama.Value != null && string.Equals((string?)ama.Value, "true", StringComparison.OrdinalIgnoreCase))
111+
{
112+
AppContext.SetSwitch("Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", true);
113+
}
114+
return;
115+
}
116+
}
117+
}
118+
96119
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Root components are expected to be defined in assemblies that do not get trimmed.")]
97120
private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMethods)
98121
{
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
<Project>
22
<PropertyGroup>
33
<BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath>
4+
<BlazorRoutingEnableRegexConstraint Condition="'$(BlazorRoutingEnableRegexConstraint)' == ''">false</BlazorRoutingEnableRegexConstraint>
45
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<RuntimeHostConfigurationOption Include="Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport"
9+
Condition="'$(BlazorRoutingEnableRegexConstraint)' != ''"
10+
Value="$(BlazorRoutingEnableRegexConstraint)"
11+
Trim="true" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(BlazorRoutingEnableRegexConstraint)' == 'true'">
16+
<_Parameter1>Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport</_Parameter1>
17+
<_Parameter2>true</_Parameter2>
18+
</AssemblyAttribute>
19+
</ItemGroup>
20+
521
</Project>

‎src/Components/test/E2ETest/ServerRenderingTests/UnifiedRoutingTests.cs

+10-6
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ public void Routing_CanRenderPagesWithParameters_And_TransitionToInteractive(str
3939
ExecuteRoutingTestCore(url, expectedValue);
4040
}
4141

42-
[Fact]
43-
public void Routing_CanRenderPagesWithConstrainedParameters_And_TransitionToInteractive()
42+
[Theory]
43+
[InlineData("routing/constraints/5", "5")]
44+
[InlineData("%F0%9F%99%82/routing/constraints/http%3A%2F%2Fwww.example.com%2Flogin%2Fcallback", "http://www.example.com/login/callback")]
45+
public void Routing_CanRenderPagesWithConstrainedParameters_And_TransitionToInteractive(string url, string expectedValue)
4446
{
45-
ExecuteRoutingTestCore("routing/constraints/5", "5");
47+
ExecuteRoutingTestCore(url, expectedValue);
4648
}
4749

4850
[Theory]
@@ -73,10 +75,12 @@ public void Routing_CanRenderPagesWithCatchAllParameters_And_TransitionToInterac
7375
ExecuteRoutingTestCore(url, expectedValue);
7476
}
7577

76-
[Fact]
77-
public void Routing_CanRenderPagesWithConstrainedCatchAllParameters_And_TransitionToInteractive()
78+
[Theory]
79+
[InlineData("routing/constrained-catch-all/a/b", "a/b")]
80+
[InlineData("%F0%9F%99%82/routing/constrained-catch-all/http%3A%2F%2Fwww.example.com%2Flogin%2Fcallback/another", "http://www.example.com/login/callback/another")]
81+
public void Routing_CanRenderPagesWithConstrainedCatchAllParameters_And_TransitionToInteractive(string url, string expectedValue)
7882
{
79-
ExecuteRoutingTestCore("routing/constrained-catch-all/a/b", "a/b");
83+
ExecuteRoutingTestCore(url, expectedValue);
8084
}
8185

8286
private void ExecuteRoutingTestCore(string url, string expectedValue)

‎src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
22

3+
<Import Project="..\..\..\WebAssembly\WebAssembly\src\targets\Microsoft.AspNetCore.Components.WebAssembly.props" />
4+
35
<PropertyGroup>
46
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
57

68
<!-- Project supports more than one language -->
79
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
810
<Nullable>annotations</Nullable>
911
<RazorLangVersion>latest</RazorLangVersion>
12+
<BlazorRoutingEnableRegexConstraint>true</BlazorRoutingEnableRegexConstraint>
1013
</PropertyGroup>
1114

1215
<ItemGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@page "/🙂/routing/constrained-catch-all/{*parameter:regex(http:%2F%2Fwww\\.example\\.com%2Flogin%2Fcallback\\/another)}"
2+
<h3>Parameters</h3>
3+
4+
<p id="parameter-value">@Parameter</p>
5+
6+
@if (_interactive)
7+
{
8+
<p id="interactive">Rendered interactive.</p>
9+
}
10+
11+
@code {
12+
private bool _interactive;
13+
14+
[Parameter] public string Parameter { get; set; }
15+
16+
protected override void OnAfterRender(bool firstRender)
17+
{
18+
if (firstRender)
19+
{
20+
_interactive = true;
21+
StateHasChanged();
22+
}
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@page "/🙂/routing/constraints/{parameter:regex(http:%2F%2Fwww\\.example\\.com%2Flogin%2Fcallback)}"
2+
<h3>Constrained Parameters</h3>
3+
4+
<p id="parameter-value">@Parameter</p>
5+
6+
@if (_interactive)
7+
{
8+
<p id="interactive">Rendered interactive.</p>
9+
}
10+
11+
@code {
12+
private bool _interactive;
13+
14+
[Parameter] public string Parameter { get; set; }
15+
16+
protected override void OnAfterRender(bool firstRender)
17+
{
18+
if (firstRender)
19+
{
20+
_interactive = true;
21+
StateHasChanged();
22+
}
23+
}
24+
}

‎src/Http/Routing/src/Constraints/RegexErrorStubRouteConstraint.cs

+12
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#if !COMPONENTS
45
using Microsoft.AspNetCore.Http;
6+
#else
7+
using Microsoft.AspNetCore.Components.Routing;
8+
#endif
59

610
namespace Microsoft.AspNetCore.Routing.Constraints;
711

12+
#if !COMPONENTS
813
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint
14+
#else
15+
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint, IParameterPolicy
16+
#endif
917
{
1018
public RegexErrorStubRouteConstraint(string _)
1119
{
1220
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
1321
}
1422

23+
#if !COMPONENTS
1524
bool IRouteConstraint.Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
25+
#else
26+
bool IRouteConstraint.Match(string routeKey, RouteValueDictionary values)
27+
#endif
1628
{
1729
// Should never get called, but is same as throw in constructor in case constructor is changed.
1830
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);

‎src/Http/Routing/src/RouteOptions.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#if !COMPONENTS
55
using System.Diagnostics;
6+
#else
7+
using Microsoft.AspNetCore.Components.Routing;
68
#endif
79
using System.Diagnostics.CodeAnalysis;
810
using Microsoft.AspNetCore.Routing.Constraints;
@@ -126,8 +128,13 @@ private static IDictionary<string, Type> GetDefaultConstraintMap()
126128

127129
#if !COMPONENTS
128130
AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message.
129-
130131
AddConstraint<RequiredRouteConstraint>(defaults, "required");
132+
#else
133+
// Check if the feature is not enabled in the browser context
134+
if (OperatingSystem.IsBrowser() && !RegexConstraintSupport.IsEnabled)
135+
{
136+
AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message.
137+
}
131138
#endif
132139

133140
// Files

0 commit comments

Comments
 (0)
Please sign in to comment.