diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..3f7272402f77 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute
+Microsoft.AspNetCore.Components.ExcludeFromInteractiveRoutingAttribute.ExcludeFromInteractiveRoutingAttribute() -> void
diff --git a/src/Components/Components/src/Routing/ExcludeFromInteractiveRoutingAttribute.cs b/src/Components/Components/src/Routing/ExcludeFromInteractiveRoutingAttribute.cs
new file mode 100644
index 000000000000..7291e0bcf23b
--- /dev/null
+++ b/src/Components/Components/src/Routing/ExcludeFromInteractiveRoutingAttribute.cs
@@ -0,0 +1,16 @@
+// 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.Components.Routing;
+
+namespace Microsoft.AspNetCore.Components;
+
+///
+/// When applied to a page component, indicates that the interactive component should
+/// ignore that page. This means that navigations to the page will not be resolved by interactive routing,
+/// but instead will cause a full page reload.
+///
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+public sealed class ExcludeFromInteractiveRoutingAttribute : Attribute
+{
+}
diff --git a/src/Components/Components/src/Routing/RouteTableFactory.cs b/src/Components/Components/src/Routing/RouteTableFactory.cs
index 481b24a68951..17074e267e5e 100644
--- a/src/Components/Components/src/Routing/RouteTableFactory.cs
+++ b/src/Components/Components/src/Routing/RouteTableFactory.cs
@@ -73,7 +73,9 @@ static void GetRouteableComponents(List routeableComponents, Assembly asse
{
foreach (var type in assembly.ExportedTypes)
{
- if (typeof(IComponent).IsAssignableFrom(type) && type.IsDefined(typeof(RouteAttribute)))
+ if (typeof(IComponent).IsAssignableFrom(type)
+ && type.IsDefined(typeof(RouteAttribute))
+ && !type.IsDefined(typeof(ExcludeFromInteractiveRoutingAttribute)))
{
routeableComponents.Add(type);
}
diff --git a/src/Components/Components/test/Routing/RouteTableFactoryTests.cs b/src/Components/Components/test/Routing/RouteTableFactoryTests.cs
index e20aa7a19f4b..6968bf2a59ba 100644
--- a/src/Components/Components/test/Routing/RouteTableFactoryTests.cs
+++ b/src/Components/Components/test/Routing/RouteTableFactoryTests.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
+using System.Reflection;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.DependencyInjection;
@@ -74,6 +75,20 @@ public void IgnoresIdenticalTypes()
Assert.Equal(routes.GroupBy(x => x.Handler).Count(), routes.Count);
}
+ [Fact]
+ public void RespectsExcludeFromInteractiveRoutingAttribute()
+ {
+ // Arrange & Act
+ var routeTableFactory = new RouteTableFactory();
+ var routeTable = routeTableFactory.Create(new RouteKey(GetType().Assembly, Array.Empty()), _serviceProvider);
+
+ var routes = GetRoutes(routeTable);
+
+ // Assert
+ Assert.Contains(routes, r => r.Handler == typeof(ComponentWithoutExcludeFromInteractiveRoutingAttribute));
+ Assert.DoesNotContain(routes, r => r.Handler == typeof(ComponentWithExcludeFromInteractiveRoutingAttribute));
+ }
+
[Fact]
public void CanDiscoverRoute()
{
@@ -1120,4 +1135,11 @@ public RouteTable Build()
class TestHandler1 { }
class TestHandler2 { }
+
+ [Route("/ComponentWithoutExcludeFromInteractiveRoutingAttribute")]
+ public class ComponentWithoutExcludeFromInteractiveRoutingAttribute : ComponentBase { }
+
+ [Route("/ComponentWithExcludeFromInteractiveRoutingAttribute")]
+ [ExcludeFromInteractiveRouting]
+ public class ComponentWithExcludeFromInteractiveRoutingAttribute : ComponentBase { }
}
diff --git a/src/Components/Endpoints/src/Builder/RazorComponentsEndpointHttpContextExtensions.cs b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointHttpContextExtensions.cs
new file mode 100644
index 000000000000..59ca4b8f8477
--- /dev/null
+++ b/src/Components/Endpoints/src/Builder/RazorComponentsEndpointHttpContextExtensions.cs
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Concurrent;
+using System.Reflection;
+using System.Reflection.Metadata;
+using Microsoft.AspNetCore.Components.Endpoints;
+using Microsoft.AspNetCore.Components.Routing;
+using Microsoft.AspNetCore.Http;
+
+[assembly: MetadataUpdateHandler(typeof(RazorComponentsEndpointHttpContextExtensions.MetadataUpdateHandler))]
+
+namespace Microsoft.AspNetCore.Components.Routing;
+
+///
+/// Extensions to for Razor component applications.
+///
+public static class RazorComponentsEndpointHttpContextExtensions
+{
+ private static readonly ConcurrentDictionary AcceptsInteractiveRoutingCache = new();
+
+ ///
+ /// Determines whether the current endpoint is a Razor component that can be reached through
+ /// interactive routing. This is true for all page components except if they declare the
+ /// attribute .
+ ///
+ /// The .
+ /// True if the current endpoint is a Razor component that does not declare .
+ public static bool AcceptsInteractiveRouting(this HttpContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ var pageType = context.GetEndpoint()?.Metadata.GetMetadata()?.Type;
+
+ return pageType is not null
+ && AcceptsInteractiveRoutingCache.GetOrAdd(
+ pageType,
+ static pageType => !pageType.IsDefined(typeof(ExcludeFromInteractiveRoutingAttribute)));
+ }
+
+ internal static class MetadataUpdateHandler
+ {
+ ///
+ /// Invoked as part of contract for hot reload.
+ ///
+ public static void ClearCache(Type[]? _)
+ => AcceptsInteractiveRoutingCache.Clear();
+ }
+}
diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..8bed29bca3ae 100644
--- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.AspNetCore.Components.Routing.RazorComponentsEndpointHttpContextExtensions
+static Microsoft.AspNetCore.Components.Routing.RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting(this Microsoft.AspNetCore.Http.HttpContext! context) -> bool
diff --git a/src/Components/Samples/BlazorUnitedApp/App.razor b/src/Components/Samples/BlazorUnitedApp/App.razor
index 11fbf7c92ab7..e9327fb3cbc2 100644
--- a/src/Components/Samples/BlazorUnitedApp/App.razor
+++ b/src/Components/Samples/BlazorUnitedApp/App.razor
@@ -9,33 +9,10 @@
-
+
-
-
-
-
-
-
- Not found
-
-
Sorry, there's nothing at this address.
-
-
-
-
-
-
- An error has occurred. This application may no longer respond until reloaded.
-
-
- An unhandled exception has occurred. See browser dev tools for details.
-
- Reload
- 🗙
-