[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.
This commit is contained in:
Javier Calvarro Nelson 2024-02-01 18:51:35 +01:00 коммит произвёл GitHub
Родитель ca988856b7
Коммит f51ae60426
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
14 изменённых файлов: 147 добавлений и 10 удалений

Просмотреть файл

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

Просмотреть файл

@ -33,7 +33,6 @@
<Compile Include="$(RoutingSourceRoot)Constraints\**\*.cs" LinkBase="Routing\Constraints" /> <Compile Include="$(RoutingSourceRoot)Constraints\**\*.cs" LinkBase="Routing\Constraints" />
<Compile Remove="$(RoutingSourceRoot)Constraints\HttpMethodRouteConstraint.cs" /> <Compile Remove="$(RoutingSourceRoot)Constraints\HttpMethodRouteConstraint.cs" />
<Compile Remove="$(RoutingSourceRoot)Constraints\RegexErrorStubRouteConstraint.cs" />
<Compile Remove="$(RoutingSourceRoot)Constraints\RequiredRouteConstraint.cs" /> <Compile Remove="$(RoutingSourceRoot)Constraints\RequiredRouteConstraint.cs" />
<Compile Remove="$(RoutingSourceRoot)Constraints\StringRouteConstraint.cs" /> <Compile Remove="$(RoutingSourceRoot)Constraints\StringRouteConstraint.cs" />
</ItemGroup> </ItemGroup>

Просмотреть файл

@ -3,5 +3,8 @@
<type fullname="Microsoft.AspNetCore.Components.HotReload.HotReloadManager" feature="System.Reflection.Metadata.MetadataUpdater.IsSupported" featurevalue="false"> <type fullname="Microsoft.AspNetCore.Components.HotReload.HotReloadManager" feature="System.Reflection.Metadata.MetadataUpdater.IsSupported" featurevalue="false">
<method signature="System.Boolean get_MetadataUpdateSupported()" body="stub" value="false" /> <method signature="System.Boolean get_MetadataUpdateSupported()" body="stub" value="false" />
</type> </type>
<type fullname="Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport" feature="Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport" featurevalue="false">
<method signature="System.Boolean get_IsEnabled()" body="stub" value="false" />
</type>
</assembly> </assembly>
</linker> </linker>

Просмотреть файл

@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.AspNetCore.Components.Routing;
internal static class RegexConstraintSupport
{
// We should check the AppContext switch in the implementation, but it doesn't flow to the wasm runtime
// during development, so we can't offer a better experience (build time message to enable the switch)
// until the context switch flows to the runtime.
// This value gets updated by the linker when the app is trimmed, so the code will always be removed from
// webassembly unless the switch is enabled.
public static bool IsEnabled =>
AppContext.TryGetSwitch("Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", out var enabled) && enabled;
}

Просмотреть файл

@ -154,7 +154,7 @@
<value>Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.</value> <value>Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.</value>
</data> </data>
<data name="RegexRouteContraint_NotConfigured" xml:space="preserve"> <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> <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>
</data> </data>
<data name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve"> <data name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve">
<value>Value must be greater than or equal to {0}.</value> <value>Value must be greater than or equal to {0}.</value>

Просмотреть файл

@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using static Microsoft.AspNetCore.Internal.LinkerFlags; using static Microsoft.AspNetCore.Internal.LinkerFlags;
using Microsoft.AspNetCore.Routing.Constraints;
namespace Microsoft.AspNetCore.Components; namespace Microsoft.AspNetCore.Components;
@ -112,9 +113,14 @@ internal class RouteTableFactory
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Application code does not get trimmed, and the framework does not define routable components.")] [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Application code does not get trimmed, and the framework does not define routable components.")]
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler, IServiceProvider serviceProvider) internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler, IServiceProvider serviceProvider)
{ {
var routeOptions = Options.Create(new RouteOptions());
if (!OperatingSystem.IsBrowser() || RegexConstraintSupport.IsEnabled)
{
routeOptions.Value.SetParameterPolicy("regex", typeof(RegexInlineRouteConstraint));
}
var builder = new TreeRouteBuilder( var builder = new TreeRouteBuilder(
serviceProvider.GetRequiredService<ILoggerFactory>(), serviceProvider.GetRequiredService<ILoggerFactory>(),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), serviceProvider)); new DefaultInlineConstraintResolver(routeOptions, serviceProvider));
foreach (var (type, templates) in templatesByHandler) foreach (var (type, templates) in templatesByHandler)
{ {

Просмотреть файл

@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Reflection;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Infrastructure;
@ -76,6 +77,12 @@ public sealed class WebAssemblyHostBuilder
Services = new ServiceCollection(); Services = new ServiceCollection();
Logging = new LoggingBuilder(Services); Logging = new LoggingBuilder(Services);
var assembly = Assembly.GetEntryAssembly();
if (assembly != null)
{
InitializeRoutingAppContextSwitch(assembly);
}
InitializeWebAssemblyRenderer(); InitializeWebAssemblyRenderer();
// Retrieve required attributes from JSRuntimeInvoker // Retrieve required attributes from JSRuntimeInvoker
@ -93,6 +100,22 @@ public sealed class WebAssemblyHostBuilder
}; };
} }
private static void InitializeRoutingAppContextSwitch(Assembly assembly)
{
var assemblyMetadataAttributes = assembly.GetCustomAttributes<AssemblyMetadataAttribute>();
foreach (var ama in assemblyMetadataAttributes)
{
if (string.Equals(ama.Key, "Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", StringComparison.Ordinal))
{
if (ama.Value != null && string.Equals((string?)ama.Value, "true", StringComparison.OrdinalIgnoreCase))
{
AppContext.SetSwitch("Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport", true);
}
return;
}
}
}
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Root components are expected to be defined in assemblies that do not get trimmed.")] [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Root components are expected to be defined in assemblies that do not get trimmed.")]
private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMethods) private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMethods)
{ {

Просмотреть файл

@ -1,5 +1,21 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath> <BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath>
<BlazorRoutingEnableRegexConstraint Condition="'$(BlazorRoutingEnableRegexConstraint)' == ''">false</BlazorRoutingEnableRegexConstraint>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport"
Condition="'$(BlazorRoutingEnableRegexConstraint)' != ''"
Value="$(BlazorRoutingEnableRegexConstraint)"
Trim="true" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(BlazorRoutingEnableRegexConstraint)' == 'true'">
<_Parameter1>Microsoft.AspNetCore.Components.Routing.RegexConstraintSupport</_Parameter1>
<_Parameter2>true</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>

Просмотреть файл

@ -39,10 +39,12 @@ public class UnifiedRoutingTests : ServerTestBase<BasicTestAppServerSiteFixture<
ExecuteRoutingTestCore(url, expectedValue); ExecuteRoutingTestCore(url, expectedValue);
} }
[Fact] [Theory]
public void Routing_CanRenderPagesWithConstrainedParameters_And_TransitionToInteractive() [InlineData("routing/constraints/5", "5")]
[InlineData("%F0%9F%99%82/routing/constraints/http%3A%2F%2Fwww.example.com%2Flogin%2Fcallback", "http://www.example.com/login/callback")]
public void Routing_CanRenderPagesWithConstrainedParameters_And_TransitionToInteractive(string url, string expectedValue)
{ {
ExecuteRoutingTestCore("routing/constraints/5", "5"); ExecuteRoutingTestCore(url, expectedValue);
} }
[Theory] [Theory]
@ -73,10 +75,12 @@ public class UnifiedRoutingTests : ServerTestBase<BasicTestAppServerSiteFixture<
ExecuteRoutingTestCore(url, expectedValue); ExecuteRoutingTestCore(url, expectedValue);
} }
[Fact] [Theory]
public void Routing_CanRenderPagesWithConstrainedCatchAllParameters_And_TransitionToInteractive() [InlineData("routing/constrained-catch-all/a/b", "a/b")]
[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")]
public void Routing_CanRenderPagesWithConstrainedCatchAllParameters_And_TransitionToInteractive(string url, string expectedValue)
{ {
ExecuteRoutingTestCore("routing/constrained-catch-all/a/b", "a/b"); ExecuteRoutingTestCore(url, expectedValue);
} }
private void ExecuteRoutingTestCore(string url, string expectedValue) private void ExecuteRoutingTestCore(string url, string expectedValue)

Просмотреть файл

@ -1,5 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\..\WebAssembly\WebAssembly\src\targets\Microsoft.AspNetCore.Components.WebAssembly.props" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@ -7,6 +9,7 @@
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData> <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<Nullable>annotations</Nullable> <Nullable>annotations</Nullable>
<RazorLangVersion>latest</RazorLangVersion> <RazorLangVersion>latest</RazorLangVersion>
<BlazorRoutingEnableRegexConstraint>true</BlazorRoutingEnableRegexConstraint>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

Просмотреть файл

@ -0,0 +1,24 @@
@page "/🙂/routing/constrained-catch-all/{*parameter:regex(http:%2F%2Fwww\\.example\\.com%2Flogin%2Fcallback\\/another)}"
<h3>Parameters</h3>
<p id="parameter-value">@Parameter</p>
@if (_interactive)
{
<p id="interactive">Rendered interactive.</p>
}
@code {
private bool _interactive;
[Parameter] public string Parameter { get; set; }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
_interactive = true;
StateHasChanged();
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
@page "/🙂/routing/constraints/{parameter:regex(http:%2F%2Fwww\\.example\\.com%2Flogin%2Fcallback)}"
<h3>Constrained Parameters</h3>
<p id="parameter-value">@Parameter</p>
@if (_interactive)
{
<p id="interactive">Rendered interactive.</p>
}
@code {
private bool _interactive;
[Parameter] public string Parameter { get; set; }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
_interactive = true;
StateHasChanged();
}
}
}

Просмотреть файл

@ -1,18 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
#if !COMPONENTS
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
#else
using Microsoft.AspNetCore.Components.Routing;
#endif
namespace Microsoft.AspNetCore.Routing.Constraints; namespace Microsoft.AspNetCore.Routing.Constraints;
#if !COMPONENTS
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint
#else
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint, IParameterPolicy
#endif
{ {
public RegexErrorStubRouteConstraint(string _) public RegexErrorStubRouteConstraint(string _)
{ {
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured); throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
} }
#if !COMPONENTS
bool IRouteConstraint.Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) bool IRouteConstraint.Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
#else
bool IRouteConstraint.Match(string routeKey, RouteValueDictionary values)
#endif
{ {
// Should never get called, but is same as throw in constructor in case constructor is changed. // Should never get called, but is same as throw in constructor in case constructor is changed.
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured); throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);

Просмотреть файл

@ -3,6 +3,8 @@
#if !COMPONENTS #if !COMPONENTS
using System.Diagnostics; using System.Diagnostics;
#else
using Microsoft.AspNetCore.Components.Routing;
#endif #endif
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Constraints;
@ -126,8 +128,13 @@ internal class RouteOptions
#if !COMPONENTS #if !COMPONENTS
AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message. AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message.
AddConstraint<RequiredRouteConstraint>(defaults, "required"); AddConstraint<RequiredRouteConstraint>(defaults, "required");
#else
// Check if the feature is not enabled in the browser context
if (OperatingSystem.IsBrowser() && !RegexConstraintSupport.IsEnabled)
{
AddConstraint<RegexErrorStubRouteConstraint>(defaults, "regex"); // Used to generate error message at runtime with helpful message.
}
#endif #endif
// Files // Files