Skip to content
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

[Blazor] Enable regex constraint on the Blazor router #53533

Merged
merged 1 commit into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11405,6 +11405,7 @@ Global
{9788C76F-658B-4441-88F8-22C6B86FAD27} = {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-6BEB1B070308} = {6126DCE4-9692-4EE2-B240-C65743572995}
{C1E7F837-6988-43E2-9E1C-7302DB484F99} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}
{2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E} = {C1E7F837-6988-43E2-9E1C-7302DB484F99}
{7CB09412-C9B0-47E8-A8C3-311AA4CFDE04} = {C1E7F837-6988-43E2-9E1C-7302DB484F99}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
<Compile Include="$(RoutingSourceRoot)Constraints\**\*.cs" LinkBase="Routing\Constraints" />

<Compile Remove="$(RoutingSourceRoot)Constraints\HttpMethodRouteConstraint.cs" />
<Compile Remove="$(RoutingSourceRoot)Constraints\RegexErrorStubRouteConstraint.cs" />
<Compile Remove="$(RoutingSourceRoot)Constraints\RequiredRouteConstraint.cs" />
<Compile Remove="$(RoutingSourceRoot)Constraints\StringRouteConstraint.cs" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
<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" />
</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>
</linker>
15 changes: 15 additions & 0 deletions src/Components/Components/src/Routing/RegexConstraintSupport.cs
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion src/Components/Components/src/Routing/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
<value>Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.</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>
<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 name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve">
<value>Value must be greater than or equal to {0}.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using static Microsoft.AspNetCore.Internal.LinkerFlags;
using Microsoft.AspNetCore.Routing.Constraints;

namespace Microsoft.AspNetCore.Components;

Expand Down Expand Up @@ -112,9 +113,14 @@ private static string[] GetTemplates(Type componentType)
[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)
{
var routeOptions = Options.Create(new RouteOptions());
if (!OperatingSystem.IsBrowser() || RegexConstraintSupport.IsEnabled)
{
routeOptions.Value.SetParameterPolicy("regex", typeof(RegexInlineRouteConstraint));
}
var builder = new TreeRouteBuilder(
serviceProvider.GetRequiredService<ILoggerFactory>(),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), serviceProvider));
new DefaultInlineConstraintResolver(routeOptions, serviceProvider));

foreach (var (type, templates) in templatesByHandler)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Infrastructure;
Expand Down Expand Up @@ -76,6 +77,12 @@ internal WebAssemblyHostBuilder(
Services = new ServiceCollection();
Logging = new LoggingBuilder(Services);

var assembly = Assembly.GetEntryAssembly();
if (assembly != null)
{
InitializeRoutingAppContextSwitch(assembly);
}

InitializeWebAssemblyRenderer();

// Retrieve required attributes from JSRuntimeInvoker
Expand All @@ -93,6 +100,22 @@ internal 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.")]
private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMethods)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
<Project>
<PropertyGroup>
<BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath>
<BlazorRoutingEnableRegexConstraint Condition="'$(BlazorRoutingEnableRegexConstraint)' == ''">false</BlazorRoutingEnableRegexConstraint>
</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>
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ public void Routing_CanRenderPagesWithParameters_And_TransitionToInteractive(str
ExecuteRoutingTestCore(url, expectedValue);
}

[Fact]
public void Routing_CanRenderPagesWithConstrainedParameters_And_TransitionToInteractive()
[Theory]
[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]
Expand Down Expand Up @@ -73,10 +75,12 @@ public void Routing_CanRenderPagesWithCatchAllParameters_And_TransitionToInterac
ExecuteRoutingTestCore(url, expectedValue);
}

[Fact]
public void Routing_CanRenderPagesWithConstrainedCatchAllParameters_And_TransitionToInteractive()
[Theory]
[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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<Import Project="..\..\..\WebAssembly\WebAssembly\src\targets\Microsoft.AspNetCore.Components.WebAssembly.props" />

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>

<!-- Project supports more than one language -->
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<Nullable>annotations</Nullable>
<RazorLangVersion>latest</RazorLangVersion>
<BlazorRoutingEnableRegexConstraint>true</BlazorRoutingEnableRegexConstraint>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if !COMPONENTS
using Microsoft.AspNetCore.Http;
#else
using Microsoft.AspNetCore.Components.Routing;
#endif

namespace Microsoft.AspNetCore.Routing.Constraints;

#if !COMPONENTS
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint
#else
internal sealed class RegexErrorStubRouteConstraint : IRouteConstraint, IParameterPolicy
#endif
{
public RegexErrorStubRouteConstraint(string _)
{
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
}

#if !COMPONENTS
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.
throw new InvalidOperationException(Resources.RegexRouteContraint_NotConfigured);
Expand Down
9 changes: 8 additions & 1 deletion src/Http/Routing/src/RouteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#if !COMPONENTS
using System.Diagnostics;
#else
using Microsoft.AspNetCore.Components.Routing;
#endif
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Routing.Constraints;
Expand Down Expand Up @@ -126,8 +128,13 @@ private static IDictionary<string, Type> GetDefaultConstraintMap()

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

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

// Files
Expand Down