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 - 🗙 -
- + diff --git a/src/Components/Samples/BlazorUnitedApp/Pages/Counter.razor b/src/Components/Samples/BlazorUnitedApp/Pages/Counter.razor index de653ae1773b..0d2acac011ab 100644 --- a/src/Components/Samples/BlazorUnitedApp/Pages/Counter.razor +++ b/src/Components/Samples/BlazorUnitedApp/Pages/Counter.razor @@ -1,4 +1,5 @@ @page "/counter" +@rendermode InteractiveServer Counter

Counter

diff --git a/src/Components/Samples/BlazorUnitedApp/Program.cs b/src/Components/Samples/BlazorUnitedApp/Program.cs index 3739cf4a31ac..3f3fc0bcbedb 100644 --- a/src/Components/Samples/BlazorUnitedApp/Program.cs +++ b/src/Components/Samples/BlazorUnitedApp/Program.cs @@ -7,7 +7,8 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddRazorComponents(); +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); builder.Services.AddSingleton(); @@ -26,6 +27,7 @@ app.UseStaticFiles(); app.UseAntiforgery(); -app.MapRazorComponents(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); app.Run(); diff --git a/src/Components/Samples/BlazorUnitedApp/Routes.razor b/src/Components/Samples/BlazorUnitedApp/Routes.razor new file mode 100644 index 000000000000..6fd3ed1b5a3b --- /dev/null +++ b/src/Components/Samples/BlazorUnitedApp/Routes.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/MainLayout.razor b/src/Components/Samples/BlazorUnitedApp/Shared/MainLayout.razor index 9b5041643514..40cfc1bb234a 100644 --- a/src/Components/Samples/BlazorUnitedApp/Shared/MainLayout.razor +++ b/src/Components/Samples/BlazorUnitedApp/Shared/MainLayout.razor @@ -13,3 +13,9 @@ + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/src/Components/Samples/BlazorUnitedApp/_Imports.razor b/src/Components/Samples/BlazorUnitedApp/_Imports.razor index f4bc6414aac3..1be7a7e9a5bf 100644 --- a/src/Components/Samples/BlazorUnitedApp/_Imports.razor +++ b/src/Components/Samples/BlazorUnitedApp/_Imports.razor @@ -7,3 +7,4 @@ @using Microsoft.JSInterop @using BlazorUnitedApp @using BlazorUnitedApp.Shared +@using static Microsoft.AspNetCore.Components.Web.RenderMode diff --git a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs index edd2f0bc3656..9ef5c926001a 100644 --- a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs @@ -17,12 +17,77 @@ public class GlobalInteractivityTest( [Fact] public void CanFindStaticallyRenderedPageAfterClickingBrowserBackButtonOnDynamicallyRenderedPage() { - Navigate("/subdir/static"); + // Start on a static page + Navigate("/subdir/globally-interactive/static-via-url"); + Browser.Equal("Global interactivity page: Static via URL", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("static", () => Browser.Exists(By.Id("execution-mode")).Text); - Browser.Click(By.CssSelector("a[href=dynamic]")); + // Navigate to an interactive page and observe it really is interactive + Browser.Click(By.LinkText("Globally-interactive by default")); + Browser.Equal("Global interactivity page: Default", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("interactive webassembly", () => Browser.Exists(By.Id("execution-mode")).Text); + + // Show that, after "back", we revert to the previous page Browser.Navigate().Back(); + Browser.Equal("Global interactivity page: Static via URL", () => Browser.Exists(By.TagName("h1")).Text); + } + + [Fact] + public void CanNavigateFromStaticToInteractiveAndBack() + { + // Start on a static page + Navigate("/subdir/globally-interactive/static-via-attribute"); + Browser.Equal("Global interactivity page: Static via attribute", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("static", () => Browser.Exists(By.Id("execution-mode")).Text); + + // Navigate to an interactive page and observe it really is interactive + Browser.Click(By.LinkText("Globally-interactive by default")); + Browser.Equal("Global interactivity page: Default", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("interactive webassembly", () => Browser.Exists(By.Id("execution-mode")).Text); - var heading = Browser.Exists(By.TagName("h1")); - Browser.Equal("Statically Rendered", () => heading.Text); + // Show that, after "back", we revert to static rendering on the previous page + Browser.Navigate().Back(); + Browser.Equal("Global interactivity page: Static via attribute", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("static", () => Browser.Exists(By.Id("execution-mode")).Text); + } + + [Fact] + public void CanNavigateFromInteractiveToStaticAndBack() + { + // Start on an interactive page + Navigate("/subdir/globally-interactive"); + Browser.Equal("Global interactivity page: Default", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("interactive webassembly", () => Browser.Exists(By.Id("execution-mode")).Text); + + // Navigate to a static page + Browser.Click(By.LinkText("Static via attribute")); + Browser.Equal("Global interactivity page: Static via attribute", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("static", () => Browser.Exists(By.Id("execution-mode")).Text); + + // Show that, after "back", we revert to interactive rendering on the previous page + Browser.Navigate().Back(); + Browser.Equal("Global interactivity page: Default", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("interactive webassembly", () => Browser.Exists(By.Id("execution-mode")).Text); + } + + [Fact] + public void CanNavigateBetweenStaticPagesViaEnhancedNav() + { + // Start on a static page + Navigate("/subdir/globally-interactive/static-via-attribute"); + Browser.Equal("static", () => Browser.Exists(By.Id("execution-mode")).Text); + var h1 = Browser.Exists(By.TagName("h1")); + Assert.Equal("Global interactivity page: Static via attribute", h1.Text); + + // Navigate to another static page + // We check it's the same h1 element, because this is enhanced nav + Browser.Click(By.LinkText("Static via URL")); + Browser.Equal("Global interactivity page: Static via URL", () => h1.Text); + Browser.Equal("static", () => Browser.Exists(By.Id("execution-mode")).Text); + + // Back also works + Browser.Navigate().Back(); + Browser.Equal("Global interactivity page: Static via attribute", () => h1.Text); + Browser.Equal("static", () => Browser.Exists(By.Id("execution-mode")).Text); } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/GlobalInteractivityApp.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/GlobalInteractivityApp.razor index 0682bca04818..05cf45c1a56e 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/GlobalInteractivityApp.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/GlobalInteractivityApp.razor @@ -1,15 +1,12 @@  - - - + - - + - - @code { [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; - // Statically render pages in the "/Account" subdirectory like we do in the Blazor Web template with Individaul auth. - private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/static") - ? null - : RenderMode.InteractiveAuto; + // Show we can use arbitrary logic to determine the rendermode. Here it's global by default, + // but that can be suppressed via URL or attribute. + private IComponentRenderMode? PageRenderMode + => HttpContext.AcceptsInteractiveRouting() && !HttpContext.Request.Path.StartsWithSegments("/globally-interactive/static-via-url") + ? RenderMode.InteractiveWebAssembly + : null; } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Static.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Static.razor deleted file mode 100644 index dd3c7cc235a1..000000000000 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Static.razor +++ /dev/null @@ -1,8 +0,0 @@ -@page "/static" - -@* This should be statically rendered by GlobalInteractivityApp. *@ -

Statically Rendered

- -
    -
  • Dynamic page
  • -
diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/Dynamic.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/Dynamic.razor deleted file mode 100644 index 8678edcb5b34..000000000000 --- a/src/Components/test/testassets/Components.WasmMinimal/Pages/Dynamic.razor +++ /dev/null @@ -1,13 +0,0 @@ -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web - -@rendermode RenderMode.InteractiveWebAssembly - -@page "/dynamic" - -

Dynamically Rendered

- -
    -
  • Static page
  • -
  • Another dynamic page
  • -
diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_Default.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_Default.razor new file mode 100644 index 000000000000..43dafd2cc6a3 --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_Default.razor @@ -0,0 +1,12 @@ +@page "/globally-interactive" + +

Global interactivity page: Default

+ +

+ This page should be rendered interactively by GlobalInteractivityApp because + that's the default rendermode for the application. +

+ + + + diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_Links.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_Links.razor new file mode 100644 index 000000000000..dc101ff75672 --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_Links.razor @@ -0,0 +1,6 @@ +@using Microsoft.AspNetCore.Components.Routing +
    +
  • Globally-interactive by default
  • +
  • Static via URL
  • +
  • Static via attribute
  • +
diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_StaticViaAttribute.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_StaticViaAttribute.razor new file mode 100644 index 000000000000..211126972998 --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_StaticViaAttribute.razor @@ -0,0 +1,15 @@ +@page "/globally-interactive/static-via-attribute" +@using Microsoft.AspNetCore.Components.Routing +@attribute [ExcludeFromInteractiveRouting] + +

Global interactivity page: Static via attribute

+ +

+ This page should be rendered statically by GlobalInteractivityApp because + it has [ExcludeFromInteractiveRoutingAttribute], and the root component is + configured to render pages with that attribute statically. +

+ + + + diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_StaticViaUrl.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_StaticViaUrl.razor new file mode 100644 index 000000000000..c2ddb11aa8b8 --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/GloballyInteractive_StaticViaUrl.razor @@ -0,0 +1,19 @@ +@page "/globally-interactive/static-via-url" + +

Global interactivity page: Static via URL

+ +

+ This page should be rendered statically by GlobalInteractivityApp because the root component + overrides the rendermode for this URL. This is equivalent to how the .NET 8 auth-enabled + template overrides global interactivity for the "/account" subdir. +

+ +

+ Caution: if you navigate here via a link from an interactive page, it will actually render + interactively because the router doesn't know to exit from interactive mode. That's the problem + fixed by [ExcludeFromInteractiveRouting]. +

+ + + + diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/ShowExecutionMode.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/ShowExecutionMode.razor new file mode 100644 index 000000000000..5b44f30020c7 --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/ShowExecutionMode.razor @@ -0,0 +1,18 @@ +

+ Actual execution mode: + @executionMode +

+ +@code { + string executionMode = "static"; + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + executionMode = "interactive "; + executionMode += OperatingSystem.IsBrowser() ? "webassembly" : "server"; + StateHasChanged(); + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/_Imports.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/_Imports.razor index ea096b9c8ff2..0489ff1e395a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/_Imports.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/_Imports.razor @@ -1,2 +1,3 @@ @using BlazorWeb_CSharp.Components.Account.Shared @layout AccountLayout +@attribute [ExcludeFromInteractiveRouting] diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor index 113db756040d..0a2cdaa5391c 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor @@ -16,7 +16,7 @@ @*#if (!InteractiveAtRoot) ##elseif (IndividualLocalAuth) - + ##elseif (UseServer && UseWebAssembly) ##elseif (UseServer) @@ -30,7 +30,7 @@ @*#if (!InteractiveAtRoot) ##elseif (IndividualLocalAuth) - + ##elseif (UseServer && UseWebAssembly) ##elseif (UseServer) @@ -49,9 +49,8 @@ [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; - private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account") - ? null - : InteractiveAuto; + private IComponentRenderMode? PageRenderMode => + HttpContext.AcceptsInteractiveRouting() ? InteractiveAuto : null; } #elseif (UseServer) @@ -59,9 +58,8 @@ [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; - private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account") - ? null - : InteractiveServer; + private IComponentRenderMode? PageRenderMode => + HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null; } #else @@ -69,8 +67,7 @@ [CascadingParameter] private HttpContext HttpContext { get; set; } = default!; - private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account") - ? null - : InteractiveWebAssembly; + private IComponentRenderMode? PageRenderMode => + HttpContext.AcceptsInteractiveRouting() ? InteractiveWebAssembly : null; } #endif*@ diff --git a/src/ProjectTemplates/test/Templates.Mvc.Tests/BlazorTemplateTest.cs b/src/ProjectTemplates/test/Templates.Mvc.Tests/BlazorTemplateTest.cs index bff1e3a7a550..be575cd36c18 100644 --- a/src/ProjectTemplates/test/Templates.Mvc.Tests/BlazorTemplateTest.cs +++ b/src/ProjectTemplates/test/Templates.Mvc.Tests/BlazorTemplateTest.cs @@ -194,7 +194,7 @@ private async Task WorkAroundNonNullableRenderModeAsync(Project project) { var appRazorPath = Path.Combine(project.TemplateOutputDir, "Components", "App.razor"); var appRazorText = await File.ReadAllTextAsync(appRazorPath); - appRazorText = appRazorText.Replace("IComponentRenderMode?", "IComponentRenderMode").Replace("? null", "? null!"); + appRazorText = appRazorText.Replace("IComponentRenderMode?", "IComponentRenderMode").Replace(": null", ": null!"); await File.WriteAllTextAsync(appRazorPath, appRazorText); }