Skip to content

Commit 23afddf

Browse files
Per-page opt out from interactive routing (#55157)
1 parent 7ad9556 commit 23afddf

File tree

24 files changed

+277
-76
lines changed

24 files changed

+277
-76
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute
3+
Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute.ExcludeFromInteractiveRoutingAttribute() -> void
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
using Microsoft.AspNetCore.Components.Routing;
5+
6+
namespace Microsoft.AspNetCore.Components;
7+
8+
/// <summary>
9+
/// When applied to a page component, indicates that the interactive <see cref="Router"/> component should
10+
/// ignore that page. This means that navigations to the page will not be resolved by interactive routing,
11+
/// but instead will cause a full page reload.
12+
/// </summary>
13+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
14+
public sealed class ExcludeFromInteractiveRoutingAttribute : Attribute
15+
{
16+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ static void GetRouteableComponents(List<Type> routeableComponents, Assembly asse
7373
{
7474
foreach (var type in assembly.ExportedTypes)
7575
{
76-
if (typeof(IComponent).IsAssignableFrom(type) && type.IsDefined(typeof(RouteAttribute)))
76+
if (typeof(IComponent).IsAssignableFrom(type)
77+
&& type.IsDefined(typeof(RouteAttribute))
78+
&& !type.IsDefined(typeof(ExcludeFromInteractiveRoutingAttribute)))
7779
{
7880
routeableComponents.Add(type);
7981
}

src/Components/Components/test/Routing/RouteTableFactoryTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Globalization;
5+
using System.Reflection;
56
using Microsoft.AspNetCore.Routing.Patterns;
67
using Microsoft.AspNetCore.Routing.Tree;
78
using Microsoft.Extensions.DependencyInjection;
@@ -74,6 +75,20 @@ public void IgnoresIdenticalTypes()
7475
Assert.Equal(routes.GroupBy(x => x.Handler).Count(), routes.Count);
7576
}
7677

78+
[Fact]
79+
public void RespectsExcludeFromInteractiveRoutingAttribute()
80+
{
81+
// Arrange & Act
82+
var routeTableFactory = new RouteTableFactory();
83+
var routeTable = routeTableFactory.Create(new RouteKey(GetType().Assembly, Array.Empty<Assembly>()), _serviceProvider);
84+
85+
var routes = GetRoutes(routeTable);
86+
87+
// Assert
88+
Assert.Contains(routes, r => r.Handler == typeof(ComponentWithoutExcludeFromInteractiveRoutingAttribute));
89+
Assert.DoesNotContain(routes, r => r.Handler == typeof(ComponentWithExcludeFromInteractiveRoutingAttribute));
90+
}
91+
7792
[Fact]
7893
public void CanDiscoverRoute()
7994
{
@@ -1120,4 +1135,11 @@ public RouteTable Build()
11201135

11211136
class TestHandler1 { }
11221137
class TestHandler2 { }
1138+
1139+
[Route("/ComponentWithoutExcludeFromInteractiveRoutingAttribute")]
1140+
public class ComponentWithoutExcludeFromInteractiveRoutingAttribute : ComponentBase { }
1141+
1142+
[Route("/ComponentWithExcludeFromInteractiveRoutingAttribute")]
1143+
[ExcludeFromInteractiveRouting]
1144+
public class ComponentWithExcludeFromInteractiveRoutingAttribute : ComponentBase { }
11231145
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
using System.Collections.Concurrent;
5+
using System.Reflection;
6+
using System.Reflection.Metadata;
7+
using Microsoft.AspNetCore.Components.Endpoints;
8+
using Microsoft.AspNetCore.Components.Routing;
9+
using Microsoft.AspNetCore.Http;
10+
11+
[assembly: MetadataUpdateHandler(typeof(RazorComponentsEndpointHttpContextExtensions.MetadataUpdateHandler))]
12+
13+
namespace Microsoft.AspNetCore.Components.Routing;
14+
15+
/// <summary>
16+
/// Extensions to <see cref="HttpContext"/> for Razor component applications.
17+
/// </summary>
18+
public static class RazorComponentsEndpointHttpContextExtensions
19+
{
20+
private static readonly ConcurrentDictionary<Type, bool> AcceptsInteractiveRoutingCache = new();
21+
22+
/// <summary>
23+
/// Determines whether the current endpoint is a Razor component that can be reached through
24+
/// interactive routing. This is true for all page components except if they declare the
25+
/// attribute <see cref="ExcludeFromInteractiveRoutingAttribute"/>.
26+
/// </summary>
27+
/// <param name="context">The <see cref="HttpContext"/>.</param>
28+
/// <returns>True if the current endpoint is a Razor component that does not declare <see cref="ExcludeFromInteractiveRoutingAttribute"/>.</returns>
29+
public static bool AcceptsInteractiveRouting(this HttpContext context)
30+
{
31+
ArgumentNullException.ThrowIfNull(context);
32+
33+
var pageType = context.GetEndpoint()?.Metadata.GetMetadata<ComponentTypeMetadata>()?.Type;
34+
35+
return pageType is not null
36+
&& AcceptsInteractiveRoutingCache.GetOrAdd(
37+
pageType,
38+
static pageType => !pageType.IsDefined(typeof(ExcludeFromInteractiveRoutingAttribute)));
39+
}
40+
41+
internal static class MetadataUpdateHandler
42+
{
43+
/// <summary>
44+
/// Invoked as part of <see cref="MetadataUpdateHandlerAttribute" /> contract for hot reload.
45+
/// </summary>
46+
public static void ClearCache(Type[]? _)
47+
=> AcceptsInteractiveRoutingCache.Clear();
48+
}
49+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Components.Routing.RazorComponentsEndpointHttpContextExtensions
3+
static Microsoft.AspNetCore.Components.Routing.RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting(this Microsoft.AspNetCore.Http.HttpContext! context) -> bool

src/Components/Samples/BlazorUnitedApp/App.razor

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,10 @@
99
<link rel="stylesheet" href="css/site.css" />
1010
<link href="BlazorUnitedApp.styles.css" rel="stylesheet" />
1111
<link rel="icon" type="image/png" href="favicon.png" />
12-
<HeadOutlet></HeadOutlet>
12+
<HeadOutlet />
1313
</head>
1414
<body>
15-
<Router AppAssembly="@typeof(App).Assembly">
16-
<Found Context="routeData">
17-
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
18-
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
19-
</Found>
20-
<NotFound>
21-
<PageTitle>Not found</PageTitle>
22-
<LayoutView Layout="@typeof(MainLayout)">
23-
<p role="alert">Sorry, there's nothing at this address.</p>
24-
</LayoutView>
25-
</NotFound>
26-
</Router>
27-
28-
<div id="blazor-error-ui">
29-
<environment include="Staging,Production">
30-
An error has occurred. This application may no longer respond until reloaded.
31-
</environment>
32-
<environment include="Development">
33-
An unhandled exception has occurred. See browser dev tools for details.
34-
</environment>
35-
<a href="" class="reload">Reload</a>
36-
<a class="dismiss">🗙</a>
37-
</div>
38-
15+
<Routes />
3916
<script src="_framework/blazor.web.js" suppress-error="BL9992"></script>
4017
</body>
4118
</html>

src/Components/Samples/BlazorUnitedApp/Pages/Counter.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@page "/counter"
2+
@rendermode InteractiveServer
23
<PageTitle>Counter</PageTitle>
34

45
<h1>Counter</h1>

src/Components/Samples/BlazorUnitedApp/Program.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
var builder = WebApplication.CreateBuilder(args);
88

99
// Add services to the container.
10-
builder.Services.AddRazorComponents();
10+
builder.Services.AddRazorComponents()
11+
.AddInteractiveServerComponents();
1112

1213
builder.Services.AddSingleton<WeatherForecastService>();
1314

@@ -26,6 +27,7 @@
2627
app.UseStaticFiles();
2728
app.UseAntiforgery();
2829

29-
app.MapRazorComponents<App>();
30+
app.MapRazorComponents<App>()
31+
.AddInteractiveServerRenderMode();
3032

3133
app.Run();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Router AppAssembly="@typeof(App).Assembly">
2+
<Found Context="routeData">
3+
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
4+
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
5+
</Found>
6+
<NotFound>
7+
<PageTitle>Not found</PageTitle>
8+
<LayoutView Layout="@typeof(MainLayout)">
9+
<p role="alert">Sorry, there's nothing at this address.</p>
10+
</LayoutView>
11+
</NotFound>
12+
</Router>

0 commit comments

Comments
 (0)