From e92baae96e09092204660b6135bb171bde13ebf2 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 4 Mar 2025 11:47:18 +0100 Subject: [PATCH 01/21] Interactive NotFound event + SSR status code. --- .../Components/src/NavigationManager.cs | 202 +++++++++++++++++- .../src/NotFoundRenderingException.cs | 20 ++ .../Components/src/PublicAPI.Unshipped.txt | 20 ++ .../Components/src/Routing/NotFoundContext.cs | 32 +++ .../src/Routing/NotFoundEventArgs.cs | 29 +++ .../Components/src/Routing/Router.cs | 14 ++ .../HttpNavigationManager.cs | 12 ++ .../src/IRazorComponentEndpointInvoker.cs | 2 + .../Endpoints/src/PublicAPI.Unshipped.txt | 1 + .../src/RazorComponentEndpointInvoker.cs | 5 + .../EndpointHtmlRenderer.EventDispatch.cs | 5 + .../src/Circuits/RemoteNavigationManager.cs | 37 ++++ .../Services/WebAssemblyNavigationManager.cs | 39 ++++ 13 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 src/Components/Components/src/NotFoundRenderingException.cs create mode 100644 src/Components/Components/src/Routing/NotFoundContext.cs create mode 100644 src/Components/Components/src/Routing/NotFoundEventArgs.cs diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index fab5c69e9635..45ecc03ae71e 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -35,6 +35,29 @@ public event EventHandler LocationChanged private CancellationTokenSource? _locationChangingCts; + /// + /// An event that fires when the page is not found. + /// + public event EventHandler NotFoundEvent + { + add + { + AssertInitialized(); + _notFound += value; + } + remove + { + AssertInitialized(); + _notFound -= value; + } + } + + private EventHandler? _notFound; + + private readonly List> _notFoundHandlers = new(); + + private CancellationTokenSource? _notFoundCts; + // For the baseUri it's worth storing as a System.Uri so we can do operations // on that type. System.Uri gives us access to the original string anyway. private Uri? _baseUri; @@ -177,6 +200,16 @@ protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)] public virtual void Refresh(bool forceReload = false) => NavigateTo(Uri, forceLoad: true, replace: true); + /// + /// TODO + /// + public virtual void NotFound() => NotFoundCore(); + + /// + /// TODO + /// + protected virtual void NotFoundCore() => throw new NotImplementedException(); + /// /// Called to initialize BaseURI and current URI before these values are used for the first time. /// Override and call this method to dynamically calculate these values. @@ -308,6 +341,26 @@ protected void NotifyLocationChanged(bool isInterceptedLink) } } + /// + /// Triggers the event with the current URI value. + /// + protected void NotifyNotFound(bool isInterceptedLink) + { + try + { + _notFound?.Invoke( + this, + new NotFoundEventArgs(isInterceptedLink) + { + HistoryEntryState = HistoryEntryState + }); + } + catch (Exception ex) + { + throw new NotFoundRenderingException("An exception occurred while dispatching a NotFound event.", ex); + } + } + /// /// Notifies the registered handlers of the current location change. /// @@ -433,12 +486,135 @@ protected async ValueTask NotifyLocationChangingAsync(string uri, string? cts.Dispose(); if (_locationChangingCts == cts) - { + { _locationChangingCts = null; } } } + /// + /// Notifies the registered handlers of the current ot found event. + /// + /// Whether this not found was intercepted from a link. + /// A representing the completion of the operation. If the result is , the navigation should continue. + protected async ValueTask NotifyNotFoundAsync(bool isNavigationIntercepted) + { + _notFoundCts?.Cancel(); + _notFoundCts = null; + + var handlerCount = _notFoundHandlers.Count; + + if (handlerCount == 0) + { + return true; + } + + var cts = new CancellationTokenSource(); + + _notFoundCts = cts; + + var cancellationToken = cts.Token; + var context = new NotFoundContext + { + // HistoryEntryState = state, + IsNavigationIntercepted = isNavigationIntercepted, + CancellationToken = cancellationToken, + }; + + try + { + if (handlerCount == 1) + { + var handlerTask = InvokeNotFoundHandlerAsync(_notFoundHandlers[0], context); + + if (handlerTask.IsFaulted) + { + await handlerTask; + return false; // Unreachable because the previous line will throw. + } + + if (context.DidPreventRendering) + { + return false; + } + + if (!handlerTask.IsCompletedSuccessfully) + { + await handlerTask.AsTask().WaitAsync(cancellationToken); + } + } + else + { + var notFoundHandlersCopy = ArrayPool>.Shared.Rent(handlerCount); + + try + { + _notFoundHandlers.CopyTo(notFoundHandlersCopy); + + var notFoundTasks = new HashSet(); + + for (var i = 0; i < handlerCount; i++) + { + var handlerTask = InvokeNotFoundHandlerAsync(notFoundHandlersCopy[i], context); + + if (handlerTask.IsFaulted) + { + await handlerTask; + return false; // Unreachable because the previous line will throw. + } + + if (context.DidPreventRendering) + { + return false; + } + + notFoundTasks.Add(handlerTask.AsTask()); + } + + while (notFoundTasks.Count != 0) + { + var completedHandlerTask = await Task.WhenAny(notFoundTasks).WaitAsync(cancellationToken); + + if (completedHandlerTask.IsFaulted) + { + await completedHandlerTask; + return false; // Unreachable because the previous line will throw. + } + + notFoundTasks.Remove(completedHandlerTask); + } + } + finally + { + ArrayPool>.Shared.Return(notFoundHandlersCopy); + } + } + + return !context.DidPreventRendering; + } + catch (TaskCanceledException ex) + { + if (ex.CancellationToken == cancellationToken) + { + // This navigation was in progress when a successive navigation occurred. + // We treat this as a canceled navigation. + return false; + } + + throw; + } + finally + { + cts.Cancel(); + cts.Dispose(); + + if (_notFoundCts == cts) + { + _notFoundCts = null; + } + } + } + private async ValueTask InvokeLocationChangingHandlerAsync(Func handler, LocationChangingContext context) { try @@ -455,6 +631,22 @@ private async ValueTask InvokeLocationChangingHandlerAsync(Func handler, NotFoundContext context) + { + try + { + await handler(context); + } + catch (OperationCanceledException) + { + // Ignore exceptions caused by cancellations. + } + catch (Exception ex) + { + HandleNotFoundHandlerException(ex, context); + } + } + /// /// Handles exceptions thrown in location changing handlers. /// @@ -463,6 +655,14 @@ private async ValueTask InvokeLocationChangingHandlerAsync(Func throw new InvalidOperationException($"To support navigation locks, {GetType().Name} must override {nameof(HandleLocationChangingHandlerException)}"); + /// + /// Handles exceptions thrown in NotFound rendering handlers. + /// + /// The exception to handle. + /// The context passed to the handler. + protected virtual void HandleNotFoundHandlerException(Exception ex, NotFoundContext context) + => throw new InvalidOperationException($"To support not found rendering locks, {GetType().Name} must override {nameof(HandleNotFoundHandlerException)}"); + /// /// Sets whether navigation is currently locked. If it is, then implementations should not update and call /// until they have first confirmed the navigation by calling diff --git a/src/Components/Components/src/NotFoundRenderingException.cs b/src/Components/Components/src/NotFoundRenderingException.cs new file mode 100644 index 000000000000..005fbc64c710 --- /dev/null +++ b/src/Components/Components/src/NotFoundRenderingException.cs @@ -0,0 +1,20 @@ +// 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; + +/// +/// Exception thrown when an is not able to render not found page. +/// +public class NotFoundRenderingException : Exception +{ + /// + /// Creates a new instance of . + /// + /// The exception message. + /// The inner exception. + public NotFoundRenderingException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..3094f1d68659 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1 +1,21 @@ #nullable enable + +virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void +virtual Microsoft.AspNetCore.Components.NavigationManager.NotFoundCore() -> void +Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs(bool isNavigationIntercepted) -> void +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.IsNavigationIntercepted.get -> bool +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.HistoryEntryState.get -> string? +Microsoft.AspNetCore.Components.Routing.NotFoundContext +Microsoft.AspNetCore.Components.Routing.NotFoundContext.NotFoundContext() -> void +Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.get -> System.Threading.CancellationToken +Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.init -> void +Microsoft.AspNetCore.Components.Routing.NotFoundContext.IsNavigationIntercepted.get -> bool +Microsoft.AspNetCore.Components.Routing.NotFoundContext.IsNavigationIntercepted.init -> void +Microsoft.AspNetCore.Components.Routing.NotFoundContext.PreventRendering() -> void +Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFound(bool isInterceptedLink) -> void +Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFoundAsync(bool isNavigationIntercepted) -> System.Threading.Tasks.ValueTask +virtual Microsoft.AspNetCore.Components.NavigationManager.HandleNotFoundHandlerException(System.Exception! ex, Microsoft.AspNetCore.Components.Routing.NotFoundContext! context) -> void +Microsoft.AspNetCore.Components.NotFoundRenderingException +Microsoft.AspNetCore.Components.NotFoundRenderingException.NotFoundRenderingException(string! message, System.Exception! innerException) -> void \ No newline at end of file diff --git a/src/Components/Components/src/Routing/NotFoundContext.cs b/src/Components/Components/src/Routing/NotFoundContext.cs new file mode 100644 index 000000000000..909f2d7cf394 --- /dev/null +++ b/src/Components/Components/src/Routing/NotFoundContext.cs @@ -0,0 +1,32 @@ +// 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; + +/// +/// Contains context for a change to the browser's current location. +/// +public sealed class NotFoundContext +{ + internal bool DidPreventRendering { get; private set; } + + /// + /// Gets a that can be used to determine if this navigation was canceled + /// (for example, because the user has triggered a different navigation). + /// + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets whether this navigation was intercepted from a link. + /// + public bool IsNavigationIntercepted { get; init; } + + /// + /// Prevents this navigation from continuing. + /// + public void PreventRendering() + { + DidPreventRendering = true; + } + +} diff --git a/src/Components/Components/src/Routing/NotFoundEventArgs.cs b/src/Components/Components/src/Routing/NotFoundEventArgs.cs new file mode 100644 index 000000000000..2f7514df9a59 --- /dev/null +++ b/src/Components/Components/src/Routing/NotFoundEventArgs.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// for . +/// +public class NotFoundEventArgs : EventArgs +{ + /// + /// Initializes a new instance of . + /// + /// A value that determines if navigation for the link was intercepted. + public NotFoundEventArgs(bool isNavigationIntercepted) + { + IsNavigationIntercepted = isNavigationIntercepted; + } + + /// + /// Gets a value that determines if navigation to NotFound page was intercepted. + /// + public bool IsNavigationIntercepted { get; } + + /// + /// Gets the state associated with the current history entry. + /// + public string? HistoryEntryState { get; internal init; } +} diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 072418b1f688..dfe679b26d15 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -105,6 +105,7 @@ public void Attach(RenderHandle renderHandle) _baseUri = NavigationManager.BaseUri; _locationAbsolute = NavigationManager.Uri; NavigationManager.LocationChanged += OnLocationChanged; + NavigationManager.NotFoundEvent += OnNotFound; RoutingStateProvider = ServiceProvider.GetService(); if (HotReloadManager.Default.MetadataUpdateSupported) @@ -146,6 +147,7 @@ public async Task SetParametersAsync(ParameterView parameters) public void Dispose() { NavigationManager.LocationChanged -= OnLocationChanged; + NavigationManager.NotFoundEvent -= OnNotFound; if (HotReloadManager.Default.MetadataUpdateSupported) { HotReloadManager.Default.OnDeltaApplied -= ClearRouteCaches; @@ -320,6 +322,15 @@ private void OnLocationChanged(object sender, LocationChangedEventArgs args) } } + private void OnNotFound(object sender, NotFoundEventArgs args) + { + if (_renderHandle.IsInitialized) + { + Log.DisplayingNotFound(_logger); + _renderHandle.Render(NotFound ?? DefaultNotFoundContent); + } + } + async Task IHandleAfterRender.OnAfterRenderAsync() { if (!_navigationInterceptionEnabled) @@ -340,6 +351,9 @@ private static partial class Log [LoggerMessage(1, LogLevel.Debug, $"Displaying {nameof(NotFound)} because path '{{Path}}' with base URI '{{BaseUri}}' does not match any component route", EventName = "DisplayingNotFound")] internal static partial void DisplayingNotFound(ILogger logger, string path, string baseUri); + [LoggerMessage(1, LogLevel.Debug, $"Displaying {nameof(NotFound)} on request", EventName = "DisplayingNotFoundOnRequest")] + internal static partial void DisplayingNotFound(ILogger logger); + [LoggerMessage(2, LogLevel.Debug, "Navigating to component {ComponentType} in response to path '{Path}' with base URI '{BaseUri}'", EventName = "NavigatingToComponent")] internal static partial void NavigatingToComponent(ILogger logger, Type componentType, string path, string baseUri); diff --git a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs index e9bb98f2d1c2..41e85a4b72f9 100644 --- a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs +++ b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs @@ -7,6 +7,13 @@ namespace Microsoft.AspNetCore.Components.Endpoints; internal sealed class HttpNavigationManager : NavigationManager, IHostEnvironmentNavigationManager { + private readonly IRazorComponentEndpointInvoker _invoker; + + public HttpNavigationManager(IRazorComponentEndpointInvoker invoker) + { + _invoker = invoker; + } + void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri) => Initialize(baseUri, uri); protected override void NavigateToCore(string uri, NavigationOptions options) @@ -14,4 +21,9 @@ protected override void NavigateToCore(string uri, NavigationOptions options) var absoluteUriString = ToAbsoluteUri(uri).AbsoluteUri; throw new NavigationException(absoluteUriString); } + + protected override void NotFoundCore() + { + _invoker.SetNotFound(); + } } diff --git a/src/Components/Endpoints/src/IRazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/IRazorComponentEndpointInvoker.cs index 22a3b61b44d0..80b569586583 100644 --- a/src/Components/Endpoints/src/IRazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/IRazorComponentEndpointInvoker.cs @@ -25,4 +25,6 @@ public interface IRazorComponentEndpointInvoker /// A that completes when the endpoint has been invoked and the component /// has been rendered into the response. Task Render(HttpContext context); + + void SetNotFound(); } diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..08fd0daf7a1e 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.AspNetCore.Components.Endpoints.IRazorComponentEndpointInvoker.SetNotFound() -> void \ No newline at end of file diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 7a76a43098d4..a8e1081290f9 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -33,6 +33,11 @@ public Task Render(HttpContext context) return _renderer.Dispatcher.InvokeAsync(() => RenderComponentCore(context)); } + public void SetNotFound() + { + _renderer.SetNotFoundResponse(); + } + // We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled. [DebuggerDisableUserUnhandledExceptions] private async Task RenderComponentCore(HttpContext context) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index cd47eb63590f..4d50ace867eb 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -74,6 +74,11 @@ private Task ReturnErrorResponse(string detailedMessage) : Task.CompletedTask; } + internal void SetNotFoundResponse() + { + _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; + } + private void UpdateNamedSubmitEvents(in RenderBatch renderBatch) { if (renderBatch.NamedEventChanges is { } changes) diff --git a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs index a2a148945ee1..ba68c99a32e1 100644 --- a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs +++ b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs @@ -148,6 +148,34 @@ async Task RefreshAsync() } } + /// + protected override void NotFoundCore() + { + Log.RequestingNotFound(_logger); + + _ = NotFoundAsync(); + + async Task NotFoundAsync() + { + try + { + bool shouldContinueRendering = await NotifyNotFoundAsync(false); + if (!shouldContinueRendering) + { + Log.NotFoundRenderCanceled(_logger); + return; + } + + NotifyNotFound(false); + } + catch (Exception ex) + { + Log.NotFoundRenderFailed(_logger, ex); + UnhandledException?.Invoke(this, ex); + } + } + } + protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context) { Log.NavigationFailed(_logger, context.TargetLocation, ex); @@ -194,13 +222,22 @@ public static void RequestingNavigation(ILogger logger, string uri, NavigationOp [LoggerMessage(4, LogLevel.Error, "Navigation failed when changing the location to {Uri}", EventName = "NavigationFailed")] public static partial void NavigationFailed(ILogger logger, string uri, Exception exception); + [LoggerMessage(5, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")] + public static partial void NotFoundRenderFailed(ILogger logger, Exception exception); + [LoggerMessage(5, LogLevel.Error, "Failed to refresh", EventName = "RefreshFailed")] public static partial void RefreshFailed(ILogger logger, Exception exception); + [LoggerMessage(1, LogLevel.Debug, "Requesting not found", EventName = "RequestingNotFound")] + public static partial void RequestingNotFound(ILogger logger); + [LoggerMessage(6, LogLevel.Debug, "Navigation completed when changing the location to {Uri}", EventName = "NavigationCompleted")] public static partial void NavigationCompleted(ILogger logger, string uri); [LoggerMessage(7, LogLevel.Debug, "Navigation stopped because the session ended when navigating to {Uri}", EventName = "NavigationStoppedSessionEnded")] public static partial void NavigationStoppedSessionEnded(ILogger logger, string uri); + + [LoggerMessage(3, LogLevel.Debug, "Rendering of NotFound canceled", EventName = "NotFoundRenderCanceled")] + public static partial void NotFoundRenderCanceled(ILogger logger); } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs index 401938dbe076..1db5fe297854 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs @@ -85,11 +85,44 @@ public override void Refresh(bool forceReload = false) DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(Interop.Refresh, forceReload); } + /// + protected override void NotFoundCore() + { + _ = PerformNotFoundAsync(); + + async Task PerformNotFoundAsync() + { + try + { + var shouldContinueNotFound = await NotifyNotFoundAsync(false); // should we pass NavigationOptions.HistoryEntryState? are we navigating awaay and changing the history? + + if (!shouldContinueNotFound) + { + Log.NotFoundRenderCanceled(_logger); + return; + } + + NotifyNotFound(false); + } + catch (Exception ex) + { + // We shouldn't ever reach this since exceptions thrown from handlers are handled in HandleNotFoundHandlerException. + // But if some other exception gets thrown, we still want to know about it. + Log.NotFoundRenderFailed(_logger, ex); + } + } + } + protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context) { Log.NavigationFailed(_logger, context.TargetLocation, ex); } + protected override void HandleNotFoundHandlerException(Exception ex, NotFoundContext context) + { + Log.NotFoundRenderFailed(_logger, ex); + } + protected override void SetNavigationLockState(bool value) => InternalJSImportMethods.Instance.NavigationManager_SetHasLocationChangingListeners((int)WebRendererId.WebAssembly, value); @@ -100,5 +133,11 @@ private static partial class Log [LoggerMessage(2, LogLevel.Error, "Navigation failed when changing the location to {Uri}", EventName = "NavigationFailed")] public static partial void NavigationFailed(ILogger logger, string uri, Exception exception); + + [LoggerMessage(5, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")] + public static partial void NotFoundRenderFailed(ILogger logger, Exception exception); + + [LoggerMessage(1, LogLevel.Debug, "NotFound render canceled", EventName = "NotFoundRenderCanceled")] + public static partial void NotFoundRenderCanceled(ILogger logger); } } From d48e2bf94b043d9e4eb8b11e07c96c1b28a9a926 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 5 Mar 2025 10:31:15 +0100 Subject: [PATCH 02/21] Remove circular DI, pass renderer on initialization instead. --- .../Components/src/IEndpointHtmlRenderer.cs | 15 +++++++++++++++ .../Components/src/PublicAPI.Shipped.txt | 1 - .../Components/src/PublicAPI.Unshipped.txt | 5 ++++- .../Routing/IHostEnvironmentNavigationManager.cs | 3 ++- .../DependencyInjection/HttpNavigationManager.cs | 11 +++++------ .../src/IRazorComponentEndpointInvoker.cs | 2 -- .../Endpoints/src/PublicAPI.Unshipped.txt | 3 +-- .../src/RazorComponentEndpointInvoker.cs | 7 +------ .../EndpointHtmlRenderer.EventDispatch.cs | 2 +- .../src/Rendering/EndpointHtmlRenderer.cs | 6 +++--- .../Endpoints/test/RazorComponentResultTest.cs | 4 +++- .../src/Circuits/RemoteNavigationManager.cs | 3 ++- 12 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 src/Components/Components/src/IEndpointHtmlRenderer.cs diff --git a/src/Components/Components/src/IEndpointHtmlRenderer.cs b/src/Components/Components/src/IEndpointHtmlRenderer.cs new file mode 100644 index 000000000000..1d137e093a89 --- /dev/null +++ b/src/Components/Components/src/IEndpointHtmlRenderer.cs @@ -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; + +/// +/// An interface for EndpointHtmlRenderer implementations that allows NavigationManagers call renderer's methods +/// +public interface IEndpointHtmlRenderer +{ + /// + /// Sets the html response to 404 Not Found. + /// + void SetNotFoundResponse(); +} \ No newline at end of file diff --git a/src/Components/Components/src/PublicAPI.Shipped.txt b/src/Components/Components/src/PublicAPI.Shipped.txt index c417cab5be3a..9757cbf2a47d 100644 --- a/src/Components/Components/src/PublicAPI.Shipped.txt +++ b/src/Components/Components/src/PublicAPI.Shipped.txt @@ -445,7 +445,6 @@ Microsoft.AspNetCore.Components.RouteView.RouteData.set -> void Microsoft.AspNetCore.Components.RouteView.RouteView() -> void Microsoft.AspNetCore.Components.RouteView.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager -Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri) -> void Microsoft.AspNetCore.Components.Routing.INavigationInterception Microsoft.AspNetCore.Components.Routing.INavigationInterception.EnableNavigationInterceptionAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 3094f1d68659..d774d394985d 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -18,4 +18,7 @@ Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFound(bool isIntercep Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFoundAsync(bool isNavigationIntercepted) -> System.Threading.Tasks.ValueTask virtual Microsoft.AspNetCore.Components.NavigationManager.HandleNotFoundHandlerException(System.Exception! ex, Microsoft.AspNetCore.Components.Routing.NotFoundContext! context) -> void Microsoft.AspNetCore.Components.NotFoundRenderingException -Microsoft.AspNetCore.Components.NotFoundRenderingException.NotFoundRenderingException(string! message, System.Exception! innerException) -> void \ No newline at end of file +Microsoft.AspNetCore.Components.NotFoundRenderingException.NotFoundRenderingException(string! message, System.Exception! innerException) -> void +Microsoft.AspNetCore.Components.IEndpointHtmlRenderer +Microsoft.AspNetCore.Components.IEndpointHtmlRenderer.SetNotFoundResponse() -> void +Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, Microsoft.AspNetCore.Components.IEndpointHtmlRenderer? renderer = null) -> void \ No newline at end of file diff --git a/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs b/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs index cbe9274f9792..f1470f0c39ab 100644 --- a/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs +++ b/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs @@ -14,5 +14,6 @@ public interface IHostEnvironmentNavigationManager /// /// The base URI. /// The absolute URI. - void Initialize(string baseUri, string uri); + /// The optional renderer. + void Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer = null); } diff --git a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs index 41e85a4b72f9..8440c60c597e 100644 --- a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs +++ b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs @@ -7,15 +7,14 @@ namespace Microsoft.AspNetCore.Components.Endpoints; internal sealed class HttpNavigationManager : NavigationManager, IHostEnvironmentNavigationManager { - private readonly IRazorComponentEndpointInvoker _invoker; + private IEndpointHtmlRenderer? _renderer; - public HttpNavigationManager(IRazorComponentEndpointInvoker invoker) + void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer) { - _invoker = invoker; + Initialize(baseUri, uri); + _renderer = renderer; } - void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri) => Initialize(baseUri, uri); - protected override void NavigateToCore(string uri, NavigationOptions options) { var absoluteUriString = ToAbsoluteUri(uri).AbsoluteUri; @@ -24,6 +23,6 @@ protected override void NavigateToCore(string uri, NavigationOptions options) protected override void NotFoundCore() { - _invoker.SetNotFound(); + _renderer?.SetNotFoundResponse(); } } diff --git a/src/Components/Endpoints/src/IRazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/IRazorComponentEndpointInvoker.cs index 80b569586583..22a3b61b44d0 100644 --- a/src/Components/Endpoints/src/IRazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/IRazorComponentEndpointInvoker.cs @@ -25,6 +25,4 @@ public interface IRazorComponentEndpointInvoker /// A that completes when the endpoint has been invoked and the component /// has been rendered into the response. Task Render(HttpContext context); - - void SetNotFound(); } diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 08fd0daf7a1e..815c92006af7 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -1,2 +1 @@ -#nullable enable -Microsoft.AspNetCore.Components.Endpoints.IRazorComponentEndpointInvoker.SetNotFound() -> void \ No newline at end of file +#nullable enable \ No newline at end of file diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index a8e1081290f9..7638eda6163b 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -33,11 +33,6 @@ public Task Render(HttpContext context) return _renderer.Dispatcher.InvokeAsync(() => RenderComponentCore(context)); } - public void SetNotFound() - { - _renderer.SetNotFoundResponse(); - } - // We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled. [DebuggerDisableUserUnhandledExceptions] private async Task RenderComponentCore(HttpContext context) @@ -79,7 +74,7 @@ private async Task RenderComponentCore(HttpContext context) return Task.CompletedTask; }); - await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync( + await _renderer.InitializeStandardComponentServicesAsync( context, componentType: pageComponent, handler: result.HandlerName, diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 4d50ace867eb..cb3a081861a1 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -74,7 +74,7 @@ private Task ReturnErrorResponse(string detailedMessage) : Task.CompletedTask; } - internal void SetNotFoundResponse() + public void SetNotFoundResponse() { _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index e66ca88536a9..7d8600b439d6 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints; /// output with prerendering markers so the content can later switch into interactive mode when used with /// blazor.*.js. It also deals with initializing the standard component DI services once per request. /// -internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrerenderer +internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrerenderer, IEndpointHtmlRenderer { private readonly IServiceProvider _services; private readonly RazorComponentsServiceOptions _options; @@ -70,14 +70,14 @@ private void SetHttpContext(HttpContext httpContext) } } - internal static async Task InitializeStandardComponentServicesAsync( + internal async Task InitializeStandardComponentServicesAsync( HttpContext httpContext, [DynamicallyAccessedMembers(Component)] Type? componentType = null, string? handler = null, IFormCollection? form = null) { var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService(); - navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); + navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request), this); var authenticationStateProvider = httpContext.RequestServices.GetService(); if (authenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthenticationStateProvider) diff --git a/src/Components/Endpoints/test/RazorComponentResultTest.cs b/src/Components/Endpoints/test/RazorComponentResultTest.cs index cbeb6712402a..5c43dc4fe439 100644 --- a/src/Components/Endpoints/test/RazorComponentResultTest.cs +++ b/src/Components/Endpoints/test/RazorComponentResultTest.cs @@ -510,8 +510,10 @@ class FakeDataProtector : IDataProtector class FakeNavigationManager : NavigationManager, IHostEnvironmentNavigationManager { - public new void Initialize(string baseUri, string uri) +#nullable enable + public void Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer = null) => base.Initialize(baseUri, uri); +#nullable disable protected override void NavigateToCore(string uri, NavigationOptions options) { diff --git a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs index ba68c99a32e1..1b24e7962829 100644 --- a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs +++ b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs @@ -39,7 +39,8 @@ public RemoteNavigationManager(ILogger logger) /// /// The base URI. /// The absolute URI. - public new void Initialize(string baseUri, string uri) + /// The optional renderer. + public void Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer = null) { base.Initialize(baseUri, uri); NotifyLocationChanged(isInterceptedLink: false); From 3a7178df90f3b7c458f1c3bbba7c4d64b77f910d Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 5 Mar 2025 11:39:37 +0100 Subject: [PATCH 03/21] Interactive NotFound tests. --- .../E2ETest/ServerRenderingTests/RenderingTest.cs | 11 +++++++++++ .../Pages/Rendering/NotFoundInteractiveServer.razor | 12 ++++++++++++ .../Rendering/NotFoundInteractiveWebassembly.razor | 12 ++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveServer.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveWebassembly.razor diff --git a/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs index 16a1387feb89..2cc8daaa64ba 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs @@ -27,6 +27,17 @@ public RenderingTest( public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + [Theory] + [InlineData("server")] + [InlineData("webassembly")] + public void CanRenderNotFoundInteractive(string renderingMode) + { + Navigate($"{ServerPathBase}/render-not-found-{renderingMode}"); + + var bodyText = Browser.FindElement(By.TagName("body")).Text; + Assert.Contains("There's nothing here", bodyText); + } + [Fact] public void CanRenderLargeComponentsWithServerRenderMode() { diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveServer.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveServer.razor new file mode 100644 index 000000000000..c41776ff528c --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveServer.razor @@ -0,0 +1,12 @@ +@page "/render-not-found-server" +@rendermode RenderMode.InteractiveServer +@inject NavigationManager NavigationManager + +

Any content

+ +@code{ + protected override void OnInitialized() + { + NavigationManager.NotFound(); + } +} \ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveWebassembly.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveWebassembly.razor new file mode 100644 index 000000000000..b05fa74156a6 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveWebassembly.razor @@ -0,0 +1,12 @@ +@page "/render-not-found-webassembly" +@rendermode RenderMode.InteractiveWebAssembly +@inject NavigationManager NavigationManager + +

Any content

+ +@code{ + protected override void OnInitialized() + { + NavigationManager.NotFound(); + } +} \ No newline at end of file From 580b8be00b60b25a807eed7e3086730d6abcf6b3 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 5 Mar 2025 11:51:38 +0100 Subject: [PATCH 04/21] Server test. --- .../ServerRenderingTests/NoInteractivityTest.cs | 16 ++++++++++++++++ .../Pages/Rendering/NotFoundSSR.razor | 11 +++++++++++ .../Pages/Routing/RoutingTestCases.razor | 9 +++++++++ 3 files changed, 36 insertions(+) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundSSR.razor diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index b0f887da31df..7ff6e2001e8f 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Http; using Components.TestServer.RazorComponents; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; @@ -25,6 +26,21 @@ public NoInteractivityTest( public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + [Fact] + public async Task CanRenderNotFound() + { + var url = $"{ServerPathBase}/render-not-found-ssr"; + Navigate(url); + var statusCode = await GetStatusCodeAsync(url); + Assert.Equal(404, statusCode); + } + private async Task GetStatusCodeAsync(string relativeUrl) + { + using var client = new HttpClient(); + string absoluteUrl = $"{_serverFixture.RootUri}/{relativeUrl}"; + var response = await client.GetAsync(absoluteUrl); + return (int)response.StatusCode; + } [Fact] public void NavigationManagerCanRefreshSSRPageWhenInteractivityNotPresent() { diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundSSR.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundSSR.razor new file mode 100644 index 000000000000..a4d3fbe8f7aa --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundSSR.razor @@ -0,0 +1,11 @@ +@page "/render-not-found-ssr" +@inject NavigationManager NavigationManager + +

Any content

+ +@code{ + protected override void OnInitialized() + { + NavigationManager.NotFound(); + } +} \ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/RoutingTestCases.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/RoutingTestCases.razor index f4297a4360c6..e923f23ee88a 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/RoutingTestCases.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/RoutingTestCases.razor @@ -23,4 +23,13 @@
  • Complex segments
  • +
  • + Not found page for Static Server Rendering +
  • +
  • + Not found page for Interactive WebAssembly rendering +
  • +
  • + Not found page for Interactive Server rendering +
  • From 58b6895424f9e368a68322b6fe5b1a5fdc5ea0a6 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 5 Mar 2025 12:19:00 +0100 Subject: [PATCH 05/21] Cleanup. --- .../Components/src/NavigationManager.cs | 24 +++++---------- .../Components/src/PublicAPI.Unshipped.txt | 12 ++------ .../Components/src/Routing/NotFoundContext.cs | 5 ---- .../src/Routing/NotFoundEventArgs.cs | 29 ------------------- .../Components/src/Routing/Router.cs | 2 +- .../Endpoints/src/PublicAPI.Unshipped.txt | 2 +- .../src/Circuits/RemoteNavigationManager.cs | 4 +-- .../Services/WebAssemblyNavigationManager.cs | 4 +-- 8 files changed, 17 insertions(+), 65 deletions(-) delete mode 100644 src/Components/Components/src/Routing/NotFoundEventArgs.cs diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 45ecc03ae71e..72536d8db1f4 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -38,7 +38,7 @@ public event EventHandler LocationChanged /// /// An event that fires when the page is not found. /// - public event EventHandler NotFoundEvent + public event EventHandler NotFoundEvent { add { @@ -52,7 +52,7 @@ public event EventHandler NotFoundEvent } } - private EventHandler? _notFound; + private EventHandler? _notFound; private readonly List> _notFoundHandlers = new(); @@ -201,12 +201,12 @@ public virtual void Refresh(bool forceReload = false) => NavigateTo(Uri, forceLoad: true, replace: true); /// - /// TODO + /// Handles setting the NotFound state. /// public virtual void NotFound() => NotFoundCore(); /// - /// TODO + /// Handles setting the NotFound state. /// protected virtual void NotFoundCore() => throw new NotImplementedException(); @@ -344,16 +344,11 @@ protected void NotifyLocationChanged(bool isInterceptedLink) /// /// Triggers the event with the current URI value. /// - protected void NotifyNotFound(bool isInterceptedLink) + protected void NotifyNotFound() { try { - _notFound?.Invoke( - this, - new NotFoundEventArgs(isInterceptedLink) - { - HistoryEntryState = HistoryEntryState - }); + _notFound?.Invoke(this, new EventArgs()); } catch (Exception ex) { @@ -486,7 +481,7 @@ protected async ValueTask NotifyLocationChangingAsync(string uri, string? cts.Dispose(); if (_locationChangingCts == cts) - { + { _locationChangingCts = null; } } @@ -495,9 +490,8 @@ protected async ValueTask NotifyLocationChangingAsync(string uri, string? /// /// Notifies the registered handlers of the current ot found event. /// - /// Whether this not found was intercepted from a link. /// A representing the completion of the operation. If the result is , the navigation should continue. - protected async ValueTask NotifyNotFoundAsync(bool isNavigationIntercepted) + protected async ValueTask NotifyNotFoundAsync() { _notFoundCts?.Cancel(); _notFoundCts = null; @@ -516,8 +510,6 @@ protected async ValueTask NotifyNotFoundAsync(bool isNavigationIntercepted var cancellationToken = cts.Token; var context = new NotFoundContext { - // HistoryEntryState = state, - IsNavigationIntercepted = isNavigationIntercepted, CancellationToken = cancellationToken, }; diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index d774d394985d..e15a3f98013a 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -2,20 +2,14 @@ virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void virtual Microsoft.AspNetCore.Components.NavigationManager.NotFoundCore() -> void -Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! -Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs -Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs(bool isNavigationIntercepted) -> void -Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.IsNavigationIntercepted.get -> bool -Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.HistoryEntryState.get -> string? +Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! Microsoft.AspNetCore.Components.Routing.NotFoundContext Microsoft.AspNetCore.Components.Routing.NotFoundContext.NotFoundContext() -> void Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.get -> System.Threading.CancellationToken Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.init -> void -Microsoft.AspNetCore.Components.Routing.NotFoundContext.IsNavigationIntercepted.get -> bool -Microsoft.AspNetCore.Components.Routing.NotFoundContext.IsNavigationIntercepted.init -> void Microsoft.AspNetCore.Components.Routing.NotFoundContext.PreventRendering() -> void -Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFound(bool isInterceptedLink) -> void -Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFoundAsync(bool isNavigationIntercepted) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFound() -> void +Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFoundAsync() -> System.Threading.Tasks.ValueTask virtual Microsoft.AspNetCore.Components.NavigationManager.HandleNotFoundHandlerException(System.Exception! ex, Microsoft.AspNetCore.Components.Routing.NotFoundContext! context) -> void Microsoft.AspNetCore.Components.NotFoundRenderingException Microsoft.AspNetCore.Components.NotFoundRenderingException.NotFoundRenderingException(string! message, System.Exception! innerException) -> void diff --git a/src/Components/Components/src/Routing/NotFoundContext.cs b/src/Components/Components/src/Routing/NotFoundContext.cs index 909f2d7cf394..c1ad0bbc401e 100644 --- a/src/Components/Components/src/Routing/NotFoundContext.cs +++ b/src/Components/Components/src/Routing/NotFoundContext.cs @@ -16,11 +16,6 @@ public sealed class NotFoundContext /// public CancellationToken CancellationToken { get; init; } - /// - /// Gets whether this navigation was intercepted from a link. - /// - public bool IsNavigationIntercepted { get; init; } - /// /// Prevents this navigation from continuing. /// diff --git a/src/Components/Components/src/Routing/NotFoundEventArgs.cs b/src/Components/Components/src/Routing/NotFoundEventArgs.cs deleted file mode 100644 index 2f7514df9a59..000000000000 --- a/src/Components/Components/src/Routing/NotFoundEventArgs.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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; - -/// -/// for . -/// -public class NotFoundEventArgs : EventArgs -{ - /// - /// Initializes a new instance of . - /// - /// A value that determines if navigation for the link was intercepted. - public NotFoundEventArgs(bool isNavigationIntercepted) - { - IsNavigationIntercepted = isNavigationIntercepted; - } - - /// - /// Gets a value that determines if navigation to NotFound page was intercepted. - /// - public bool IsNavigationIntercepted { get; } - - /// - /// Gets the state associated with the current history entry. - /// - public string? HistoryEntryState { get; internal init; } -} diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index dfe679b26d15..b1c90d682546 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -322,7 +322,7 @@ private void OnLocationChanged(object sender, LocationChangedEventArgs args) } } - private void OnNotFound(object sender, NotFoundEventArgs args) + private void OnNotFound(object sender, EventArgs args) { if (_renderHandle.IsInitialized) { diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 815c92006af7..7dc5c58110bf 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -1 +1 @@ -#nullable enable \ No newline at end of file +#nullable enable diff --git a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs index 1b24e7962829..b1e5e7fcaa2b 100644 --- a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs +++ b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs @@ -160,14 +160,14 @@ async Task NotFoundAsync() { try { - bool shouldContinueRendering = await NotifyNotFoundAsync(false); + bool shouldContinueRendering = await NotifyNotFoundAsync(); if (!shouldContinueRendering) { Log.NotFoundRenderCanceled(_logger); return; } - NotifyNotFound(false); + NotifyNotFound(); } catch (Exception ex) { diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs index 1db5fe297854..1954c66a0c41 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs @@ -94,7 +94,7 @@ async Task PerformNotFoundAsync() { try { - var shouldContinueNotFound = await NotifyNotFoundAsync(false); // should we pass NavigationOptions.HistoryEntryState? are we navigating awaay and changing the history? + var shouldContinueNotFound = await NotifyNotFoundAsync(); if (!shouldContinueNotFound) { @@ -102,7 +102,7 @@ async Task PerformNotFoundAsync() return; } - NotifyNotFound(false); + NotifyNotFound(); } catch (Exception ex) { From a5ca308e93fe45b55055e6c71be58f4a270e58cc Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 5 Mar 2025 14:31:16 +0100 Subject: [PATCH 06/21] Fix the API Analyzer bug. --- .../Components/src/PublicAPI.Shipped.txt | 1 + .../Components/src/PublicAPI.Unshipped.txt | 21 +++++++++---------- .../IHostEnvironmentNavigationManager.cs | 11 ++++++++-- .../HttpNavigationManager.cs | 2 ++ .../test/RazorComponentResultTest.cs | 3 +++ .../src/Circuits/RemoteNavigationManager.cs | 11 ++++++++++ 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Components/Components/src/PublicAPI.Shipped.txt b/src/Components/Components/src/PublicAPI.Shipped.txt index 9757cbf2a47d..c417cab5be3a 100644 --- a/src/Components/Components/src/PublicAPI.Shipped.txt +++ b/src/Components/Components/src/PublicAPI.Shipped.txt @@ -445,6 +445,7 @@ Microsoft.AspNetCore.Components.RouteView.RouteData.set -> void Microsoft.AspNetCore.Components.RouteView.RouteView() -> void Microsoft.AspNetCore.Components.RouteView.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager +Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri) -> void Microsoft.AspNetCore.Components.Routing.INavigationInterception Microsoft.AspNetCore.Components.Routing.INavigationInterception.EnableNavigationInterceptionAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index e15a3f98013a..f3e3be9a904f 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,18 +1,17 @@ #nullable enable - -virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void -virtual Microsoft.AspNetCore.Components.NavigationManager.NotFoundCore() -> void +Microsoft.AspNetCore.Components.IEndpointHtmlRenderer +Microsoft.AspNetCore.Components.IEndpointHtmlRenderer.SetNotFoundResponse() -> void Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! +Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFound() -> void +Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFoundAsync() -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Components.NotFoundRenderingException +Microsoft.AspNetCore.Components.NotFoundRenderingException.NotFoundRenderingException(string! message, System.Exception! innerException) -> void +Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, Microsoft.AspNetCore.Components.IEndpointHtmlRenderer! renderer) -> void Microsoft.AspNetCore.Components.Routing.NotFoundContext -Microsoft.AspNetCore.Components.Routing.NotFoundContext.NotFoundContext() -> void Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.get -> System.Threading.CancellationToken Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.init -> void +Microsoft.AspNetCore.Components.Routing.NotFoundContext.NotFoundContext() -> void Microsoft.AspNetCore.Components.Routing.NotFoundContext.PreventRendering() -> void -Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFound() -> void -Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFoundAsync() -> System.Threading.Tasks.ValueTask virtual Microsoft.AspNetCore.Components.NavigationManager.HandleNotFoundHandlerException(System.Exception! ex, Microsoft.AspNetCore.Components.Routing.NotFoundContext! context) -> void -Microsoft.AspNetCore.Components.NotFoundRenderingException -Microsoft.AspNetCore.Components.NotFoundRenderingException.NotFoundRenderingException(string! message, System.Exception! innerException) -> void -Microsoft.AspNetCore.Components.IEndpointHtmlRenderer -Microsoft.AspNetCore.Components.IEndpointHtmlRenderer.SetNotFoundResponse() -> void -Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, Microsoft.AspNetCore.Components.IEndpointHtmlRenderer? renderer = null) -> void \ No newline at end of file +virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void +virtual Microsoft.AspNetCore.Components.NavigationManager.NotFoundCore() -> void diff --git a/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs b/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs index f1470f0c39ab..304812cfe96b 100644 --- a/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs +++ b/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs @@ -14,6 +14,13 @@ public interface IHostEnvironmentNavigationManager /// /// The base URI. /// The absolute URI. - /// The optional renderer. - void Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer = null); + void Initialize(string baseUri, string uri); + + /// + /// Initializes the . + /// + /// The base URI. + /// The absolute URI. + /// The renderer. + void Initialize(string baseUri, string uri, IEndpointHtmlRenderer renderer); } diff --git a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs index 8440c60c597e..ef7709829ad7 100644 --- a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs +++ b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs @@ -9,6 +9,8 @@ internal sealed class HttpNavigationManager : NavigationManager, IHostEnvironmen { private IEndpointHtmlRenderer? _renderer; + void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri) => Initialize(baseUri, uri); + void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer) { Initialize(baseUri, uri); diff --git a/src/Components/Endpoints/test/RazorComponentResultTest.cs b/src/Components/Endpoints/test/RazorComponentResultTest.cs index 5c43dc4fe439..bf730dfbce13 100644 --- a/src/Components/Endpoints/test/RazorComponentResultTest.cs +++ b/src/Components/Endpoints/test/RazorComponentResultTest.cs @@ -510,6 +510,9 @@ class FakeDataProtector : IDataProtector class FakeNavigationManager : NavigationManager, IHostEnvironmentNavigationManager { + public new void Initialize(string baseUri, string uri) + => base.Initialize(baseUri, uri); + #nullable enable public void Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer = null) => base.Initialize(baseUri, uri); diff --git a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs index b1e5e7fcaa2b..5bc40a60144c 100644 --- a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs +++ b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs @@ -34,6 +34,17 @@ public RemoteNavigationManager(ILogger logger) /// public bool HasAttachedJSRuntime => _jsRuntime != null; + /// + /// Initializes the . + /// + /// The base URI. + /// The absolute URI. + public new void Initialize(string baseUri, string uri) + { + base.Initialize(baseUri, uri); + NotifyLocationChanged(isInterceptedLink: false); + } + /// /// Initializes the . /// From b3b4e1825d03afade9465051d048a2871f8b9ee3 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 5 Mar 2025 14:53:43 +0100 Subject: [PATCH 07/21] Limit the added APIs by removing event cancellation. --- .../Components/src/NavigationManager.cs | 148 ------------------ .../Components/src/PublicAPI.Unshipped.txt | 7 - .../Components/src/Routing/NotFoundContext.cs | 27 ---- .../src/Circuits/RemoteNavigationManager.cs | 29 +--- .../Services/WebAssemblyNavigationManager.cs | 35 +---- 5 files changed, 13 insertions(+), 233 deletions(-) delete mode 100644 src/Components/Components/src/Routing/NotFoundContext.cs diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 72536d8db1f4..1b7816155b9c 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -54,10 +54,6 @@ public event EventHandler NotFoundEvent private EventHandler? _notFound; - private readonly List> _notFoundHandlers = new(); - - private CancellationTokenSource? _notFoundCts; - // For the baseUri it's worth storing as a System.Uri so we can do operations // on that type. System.Uri gives us access to the original string anyway. private Uri? _baseUri; @@ -487,126 +483,6 @@ protected async ValueTask NotifyLocationChangingAsync(string uri, string? } } - /// - /// Notifies the registered handlers of the current ot found event. - /// - /// A representing the completion of the operation. If the result is , the navigation should continue. - protected async ValueTask NotifyNotFoundAsync() - { - _notFoundCts?.Cancel(); - _notFoundCts = null; - - var handlerCount = _notFoundHandlers.Count; - - if (handlerCount == 0) - { - return true; - } - - var cts = new CancellationTokenSource(); - - _notFoundCts = cts; - - var cancellationToken = cts.Token; - var context = new NotFoundContext - { - CancellationToken = cancellationToken, - }; - - try - { - if (handlerCount == 1) - { - var handlerTask = InvokeNotFoundHandlerAsync(_notFoundHandlers[0], context); - - if (handlerTask.IsFaulted) - { - await handlerTask; - return false; // Unreachable because the previous line will throw. - } - - if (context.DidPreventRendering) - { - return false; - } - - if (!handlerTask.IsCompletedSuccessfully) - { - await handlerTask.AsTask().WaitAsync(cancellationToken); - } - } - else - { - var notFoundHandlersCopy = ArrayPool>.Shared.Rent(handlerCount); - - try - { - _notFoundHandlers.CopyTo(notFoundHandlersCopy); - - var notFoundTasks = new HashSet(); - - for (var i = 0; i < handlerCount; i++) - { - var handlerTask = InvokeNotFoundHandlerAsync(notFoundHandlersCopy[i], context); - - if (handlerTask.IsFaulted) - { - await handlerTask; - return false; // Unreachable because the previous line will throw. - } - - if (context.DidPreventRendering) - { - return false; - } - - notFoundTasks.Add(handlerTask.AsTask()); - } - - while (notFoundTasks.Count != 0) - { - var completedHandlerTask = await Task.WhenAny(notFoundTasks).WaitAsync(cancellationToken); - - if (completedHandlerTask.IsFaulted) - { - await completedHandlerTask; - return false; // Unreachable because the previous line will throw. - } - - notFoundTasks.Remove(completedHandlerTask); - } - } - finally - { - ArrayPool>.Shared.Return(notFoundHandlersCopy); - } - } - - return !context.DidPreventRendering; - } - catch (TaskCanceledException ex) - { - if (ex.CancellationToken == cancellationToken) - { - // This navigation was in progress when a successive navigation occurred. - // We treat this as a canceled navigation. - return false; - } - - throw; - } - finally - { - cts.Cancel(); - cts.Dispose(); - - if (_notFoundCts == cts) - { - _notFoundCts = null; - } - } - } - private async ValueTask InvokeLocationChangingHandlerAsync(Func handler, LocationChangingContext context) { try @@ -623,22 +499,6 @@ private async ValueTask InvokeLocationChangingHandlerAsync(Func handler, NotFoundContext context) - { - try - { - await handler(context); - } - catch (OperationCanceledException) - { - // Ignore exceptions caused by cancellations. - } - catch (Exception ex) - { - HandleNotFoundHandlerException(ex, context); - } - } - /// /// Handles exceptions thrown in location changing handlers. /// @@ -647,14 +507,6 @@ private async ValueTask InvokeNotFoundHandlerAsync(Func throw new InvalidOperationException($"To support navigation locks, {GetType().Name} must override {nameof(HandleLocationChangingHandlerException)}"); - /// - /// Handles exceptions thrown in NotFound rendering handlers. - /// - /// The exception to handle. - /// The context passed to the handler. - protected virtual void HandleNotFoundHandlerException(Exception ex, NotFoundContext context) - => throw new InvalidOperationException($"To support not found rendering locks, {GetType().Name} must override {nameof(HandleNotFoundHandlerException)}"); - /// /// Sets whether navigation is currently locked. If it is, then implementations should not update and call /// until they have first confirmed the navigation by calling diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index f3e3be9a904f..7415ac859795 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -3,15 +3,8 @@ Microsoft.AspNetCore.Components.IEndpointHtmlRenderer Microsoft.AspNetCore.Components.IEndpointHtmlRenderer.SetNotFoundResponse() -> void Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFound() -> void -Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFoundAsync() -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.NotFoundRenderingException Microsoft.AspNetCore.Components.NotFoundRenderingException.NotFoundRenderingException(string! message, System.Exception! innerException) -> void Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, Microsoft.AspNetCore.Components.IEndpointHtmlRenderer! renderer) -> void -Microsoft.AspNetCore.Components.Routing.NotFoundContext -Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.get -> System.Threading.CancellationToken -Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.init -> void -Microsoft.AspNetCore.Components.Routing.NotFoundContext.NotFoundContext() -> void -Microsoft.AspNetCore.Components.Routing.NotFoundContext.PreventRendering() -> void -virtual Microsoft.AspNetCore.Components.NavigationManager.HandleNotFoundHandlerException(System.Exception! ex, Microsoft.AspNetCore.Components.Routing.NotFoundContext! context) -> void virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void virtual Microsoft.AspNetCore.Components.NavigationManager.NotFoundCore() -> void diff --git a/src/Components/Components/src/Routing/NotFoundContext.cs b/src/Components/Components/src/Routing/NotFoundContext.cs deleted file mode 100644 index c1ad0bbc401e..000000000000 --- a/src/Components/Components/src/Routing/NotFoundContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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; - -/// -/// Contains context for a change to the browser's current location. -/// -public sealed class NotFoundContext -{ - internal bool DidPreventRendering { get; private set; } - - /// - /// Gets a that can be used to determine if this navigation was canceled - /// (for example, because the user has triggered a different navigation). - /// - public CancellationToken CancellationToken { get; init; } - - /// - /// Prevents this navigation from continuing. - /// - public void PreventRendering() - { - DidPreventRendering = true; - } - -} diff --git a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs index 5bc40a60144c..dee9e3bc8bff 100644 --- a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs +++ b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs @@ -165,26 +165,14 @@ protected override void NotFoundCore() { Log.RequestingNotFound(_logger); - _ = NotFoundAsync(); - - async Task NotFoundAsync() + try { - try - { - bool shouldContinueRendering = await NotifyNotFoundAsync(); - if (!shouldContinueRendering) - { - Log.NotFoundRenderCanceled(_logger); - return; - } - - NotifyNotFound(); - } - catch (Exception ex) - { - Log.NotFoundRenderFailed(_logger, ex); - UnhandledException?.Invoke(this, ex); - } + NotifyNotFound(); + } + catch (Exception ex) + { + Log.NotFoundRenderFailed(_logger, ex); + UnhandledException?.Invoke(this, ex); } } @@ -248,8 +236,5 @@ public static void RequestingNavigation(ILogger logger, string uri, NavigationOp [LoggerMessage(7, LogLevel.Debug, "Navigation stopped because the session ended when navigating to {Uri}", EventName = "NavigationStoppedSessionEnded")] public static partial void NavigationStoppedSessionEnded(ILogger logger, string uri); - - [LoggerMessage(3, LogLevel.Debug, "Rendering of NotFound canceled", EventName = "NotFoundRenderCanceled")] - public static partial void NotFoundRenderCanceled(ILogger logger); } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs index 1954c66a0c41..6b26c8004dae 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs @@ -88,28 +88,13 @@ public override void Refresh(bool forceReload = false) /// protected override void NotFoundCore() { - _ = PerformNotFoundAsync(); - - async Task PerformNotFoundAsync() + try { - try - { - var shouldContinueNotFound = await NotifyNotFoundAsync(); - - if (!shouldContinueNotFound) - { - Log.NotFoundRenderCanceled(_logger); - return; - } - - NotifyNotFound(); - } - catch (Exception ex) - { - // We shouldn't ever reach this since exceptions thrown from handlers are handled in HandleNotFoundHandlerException. - // But if some other exception gets thrown, we still want to know about it. - Log.NotFoundRenderFailed(_logger, ex); - } + NotifyNotFound(); + } + catch (Exception ex) + { + Log.NotFoundRenderFailed(_logger, ex); } } @@ -118,11 +103,6 @@ protected override void HandleLocationChangingHandlerException(Exception ex, Loc Log.NavigationFailed(_logger, context.TargetLocation, ex); } - protected override void HandleNotFoundHandlerException(Exception ex, NotFoundContext context) - { - Log.NotFoundRenderFailed(_logger, ex); - } - protected override void SetNavigationLockState(bool value) => InternalJSImportMethods.Instance.NavigationManager_SetHasLocationChangingListeners((int)WebRendererId.WebAssembly, value); @@ -136,8 +116,5 @@ private static partial class Log [LoggerMessage(5, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")] public static partial void NotFoundRenderFailed(ILogger logger, Exception exception); - - [LoggerMessage(1, LogLevel.Debug, "NotFound render canceled", EventName = "NotFoundRenderCanceled")] - public static partial void NotFoundRenderCanceled(ILogger logger); } } From 758448e76be33debd7cd1958e55e0c909a117c71 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 5 Mar 2025 15:05:45 +0100 Subject: [PATCH 08/21] Formatting + rename. --- .../test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 7ff6e2001e8f..fd8ef529f739 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -27,13 +27,14 @@ public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); [Fact] - public async Task CanRenderNotFound() + public async Task CansSetNotFoundStatus() { var url = $"{ServerPathBase}/render-not-found-ssr"; Navigate(url); var statusCode = await GetStatusCodeAsync(url); Assert.Equal(404, statusCode); } + private async Task GetStatusCodeAsync(string relativeUrl) { using var client = new HttpClient(); @@ -41,6 +42,7 @@ private async Task GetStatusCodeAsync(string relativeUrl) var response = await client.GetAsync(absoluteUrl); return (int)response.StatusCode; } + [Fact] public void NavigationManagerCanRefreshSSRPageWhenInteractivityNotPresent() { From ca379ebd03023fbf9e8d1ebf26d0d70b959f24fe Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 5 Mar 2025 15:08:40 +0100 Subject: [PATCH 09/21] Formatting. --- src/Components/Endpoints/test/RazorComponentResultTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Endpoints/test/RazorComponentResultTest.cs b/src/Components/Endpoints/test/RazorComponentResultTest.cs index bf730dfbce13..b7ba8e80cd0c 100644 --- a/src/Components/Endpoints/test/RazorComponentResultTest.cs +++ b/src/Components/Endpoints/test/RazorComponentResultTest.cs @@ -510,7 +510,7 @@ class FakeDataProtector : IDataProtector class FakeNavigationManager : NavigationManager, IHostEnvironmentNavigationManager { - public new void Initialize(string baseUri, string uri) + public new void Initialize(string baseUri, string uri) => base.Initialize(baseUri, uri); #nullable enable From ef9409114851ef7253b82268b65d9eeaa4a88ba2 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 5 Mar 2025 15:12:14 +0100 Subject: [PATCH 10/21] Address copilot's feedback. --- src/Components/Components/src/Routing/Router.cs | 6 +++--- .../Server/src/Circuits/RemoteNavigationManager.cs | 6 +++--- .../src/Services/WebAssemblyNavigationManager.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index b1c90d682546..f27226d492a9 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -351,13 +351,13 @@ private static partial class Log [LoggerMessage(1, LogLevel.Debug, $"Displaying {nameof(NotFound)} because path '{{Path}}' with base URI '{{BaseUri}}' does not match any component route", EventName = "DisplayingNotFound")] internal static partial void DisplayingNotFound(ILogger logger, string path, string baseUri); - [LoggerMessage(1, LogLevel.Debug, $"Displaying {nameof(NotFound)} on request", EventName = "DisplayingNotFoundOnRequest")] - internal static partial void DisplayingNotFound(ILogger logger); - [LoggerMessage(2, LogLevel.Debug, "Navigating to component {ComponentType} in response to path '{Path}' with base URI '{BaseUri}'", EventName = "NavigatingToComponent")] internal static partial void NavigatingToComponent(ILogger logger, Type componentType, string path, string baseUri); [LoggerMessage(3, LogLevel.Debug, "Navigating to non-component URI '{ExternalUri}' in response to path '{Path}' with base URI '{BaseUri}'", EventName = "NavigatingToExternalUri")] internal static partial void NavigatingToExternalUri(ILogger logger, string externalUri, string path, string baseUri); + + [LoggerMessage(4, LogLevel.Debug, $"Displaying {nameof(NotFound)} on request", EventName = "DisplayingNotFoundOnRequest")] + internal static partial void DisplayingNotFound(ILogger logger); } } diff --git a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs index dee9e3bc8bff..d326bb766a54 100644 --- a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs +++ b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs @@ -222,9 +222,6 @@ public static void RequestingNavigation(ILogger logger, string uri, NavigationOp [LoggerMessage(4, LogLevel.Error, "Navigation failed when changing the location to {Uri}", EventName = "NavigationFailed")] public static partial void NavigationFailed(ILogger logger, string uri, Exception exception); - [LoggerMessage(5, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")] - public static partial void NotFoundRenderFailed(ILogger logger, Exception exception); - [LoggerMessage(5, LogLevel.Error, "Failed to refresh", EventName = "RefreshFailed")] public static partial void RefreshFailed(ILogger logger, Exception exception); @@ -236,5 +233,8 @@ public static void RequestingNavigation(ILogger logger, string uri, NavigationOp [LoggerMessage(7, LogLevel.Debug, "Navigation stopped because the session ended when navigating to {Uri}", EventName = "NavigationStoppedSessionEnded")] public static partial void NavigationStoppedSessionEnded(ILogger logger, string uri); + + [LoggerMessage(8, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")] + public static partial void NotFoundRenderFailed(ILogger logger, Exception exception); } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs index 6b26c8004dae..35694620c22a 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs @@ -114,7 +114,7 @@ private static partial class Log [LoggerMessage(2, LogLevel.Error, "Navigation failed when changing the location to {Uri}", EventName = "NavigationFailed")] public static partial void NavigationFailed(ILogger logger, string uri, Exception exception); - [LoggerMessage(5, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")] + [LoggerMessage(3, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")] public static partial void NotFoundRenderFailed(ILogger logger, Exception exception); } } From bfa81451daf6e224f3f861e9f8760a8e01804f57 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 10 Mar 2025 14:09:12 +0100 Subject: [PATCH 11/21] Feedback. --- .../Components/src/NavigationManager.cs | 20 ++++--------------- .../src/NotFoundRenderingException.cs | 20 ------------------- .../Components/src/PublicAPI.Unshipped.txt | 3 --- .../src/Circuits/RemoteNavigationManager.cs | 19 ------------------ .../Services/WebAssemblyNavigationManager.cs | 16 --------------- 5 files changed, 4 insertions(+), 74 deletions(-) delete mode 100644 src/Components/Components/src/NotFoundRenderingException.cs diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 1b7816155b9c..5d98000c64b2 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -204,7 +204,10 @@ public virtual void Refresh(bool forceReload = false) /// /// Handles setting the NotFound state. /// - protected virtual void NotFoundCore() => throw new NotImplementedException(); + protected virtual void NotFoundCore() + { + _notFound?.Invoke(this, new EventArgs()); + } /// /// Called to initialize BaseURI and current URI before these values are used for the first time. @@ -337,21 +340,6 @@ protected void NotifyLocationChanged(bool isInterceptedLink) } } - /// - /// Triggers the event with the current URI value. - /// - protected void NotifyNotFound() - { - try - { - _notFound?.Invoke(this, new EventArgs()); - } - catch (Exception ex) - { - throw new NotFoundRenderingException("An exception occurred while dispatching a NotFound event.", ex); - } - } - /// /// Notifies the registered handlers of the current location change. /// diff --git a/src/Components/Components/src/NotFoundRenderingException.cs b/src/Components/Components/src/NotFoundRenderingException.cs deleted file mode 100644 index 005fbc64c710..000000000000 --- a/src/Components/Components/src/NotFoundRenderingException.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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; - -/// -/// Exception thrown when an is not able to render not found page. -/// -public class NotFoundRenderingException : Exception -{ - /// - /// Creates a new instance of . - /// - /// The exception message. - /// The inner exception. - public NotFoundRenderingException(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 7415ac859795..db8e454a3925 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -2,9 +2,6 @@ Microsoft.AspNetCore.Components.IEndpointHtmlRenderer Microsoft.AspNetCore.Components.IEndpointHtmlRenderer.SetNotFoundResponse() -> void Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! -Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFound() -> void -Microsoft.AspNetCore.Components.NotFoundRenderingException -Microsoft.AspNetCore.Components.NotFoundRenderingException.NotFoundRenderingException(string! message, System.Exception! innerException) -> void Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, Microsoft.AspNetCore.Components.IEndpointHtmlRenderer! renderer) -> void virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void virtual Microsoft.AspNetCore.Components.NavigationManager.NotFoundCore() -> void diff --git a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs index d326bb766a54..8dfeabb4959e 100644 --- a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs +++ b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs @@ -160,22 +160,6 @@ async Task RefreshAsync() } } - /// - protected override void NotFoundCore() - { - Log.RequestingNotFound(_logger); - - try - { - NotifyNotFound(); - } - catch (Exception ex) - { - Log.NotFoundRenderFailed(_logger, ex); - UnhandledException?.Invoke(this, ex); - } - } - protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context) { Log.NavigationFailed(_logger, context.TargetLocation, ex); @@ -233,8 +217,5 @@ public static void RequestingNavigation(ILogger logger, string uri, NavigationOp [LoggerMessage(7, LogLevel.Debug, "Navigation stopped because the session ended when navigating to {Uri}", EventName = "NavigationStoppedSessionEnded")] public static partial void NavigationStoppedSessionEnded(ILogger logger, string uri); - - [LoggerMessage(8, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")] - public static partial void NotFoundRenderFailed(ILogger logger, Exception exception); } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs index 35694620c22a..401938dbe076 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs @@ -85,19 +85,6 @@ public override void Refresh(bool forceReload = false) DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(Interop.Refresh, forceReload); } - /// - protected override void NotFoundCore() - { - try - { - NotifyNotFound(); - } - catch (Exception ex) - { - Log.NotFoundRenderFailed(_logger, ex); - } - } - protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context) { Log.NavigationFailed(_logger, context.TargetLocation, ex); @@ -113,8 +100,5 @@ private static partial class Log [LoggerMessage(2, LogLevel.Error, "Navigation failed when changing the location to {Uri}", EventName = "NavigationFailed")] public static partial void NavigationFailed(ILogger logger, string uri, Exception exception); - - [LoggerMessage(3, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")] - public static partial void NotFoundRenderFailed(ILogger logger, Exception exception); } } From 21314b6e7709e4b07c8760d6366e3ec6d7757bc2 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 10 Mar 2025 14:39:06 +0100 Subject: [PATCH 12/21] Feedback - server --- .../Components/src/IEndpointHtmlRenderer.cs | 15 --------------- .../Components/src/PublicAPI.Unshipped.txt | 3 --- .../Routing/IHostEnvironmentNavigationManager.cs | 8 -------- .../DependencyInjection/HttpNavigationManager.cs | 13 ------------- .../src/Rendering/EndpointHtmlRenderer.cs | 12 ++++++++++-- .../Endpoints/test/RazorComponentResultTest.cs | 5 ----- .../src/Circuits/RemoteNavigationManager.cs | 12 ------------ 7 files changed, 10 insertions(+), 58 deletions(-) delete mode 100644 src/Components/Components/src/IEndpointHtmlRenderer.cs diff --git a/src/Components/Components/src/IEndpointHtmlRenderer.cs b/src/Components/Components/src/IEndpointHtmlRenderer.cs deleted file mode 100644 index 1d137e093a89..000000000000 --- a/src/Components/Components/src/IEndpointHtmlRenderer.cs +++ /dev/null @@ -1,15 +0,0 @@ -// 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; - -/// -/// An interface for EndpointHtmlRenderer implementations that allows NavigationManagers call renderer's methods -/// -public interface IEndpointHtmlRenderer -{ - /// - /// Sets the html response to 404 Not Found. - /// - void SetNotFoundResponse(); -} \ No newline at end of file diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index db8e454a3925..5a7ae3de6f3f 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,7 +1,4 @@ #nullable enable -Microsoft.AspNetCore.Components.IEndpointHtmlRenderer -Microsoft.AspNetCore.Components.IEndpointHtmlRenderer.SetNotFoundResponse() -> void Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! -Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, Microsoft.AspNetCore.Components.IEndpointHtmlRenderer! renderer) -> void virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void virtual Microsoft.AspNetCore.Components.NavigationManager.NotFoundCore() -> void diff --git a/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs b/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs index 304812cfe96b..cbe9274f9792 100644 --- a/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs +++ b/src/Components/Components/src/Routing/IHostEnvironmentNavigationManager.cs @@ -15,12 +15,4 @@ public interface IHostEnvironmentNavigationManager /// The base URI. /// The absolute URI. void Initialize(string baseUri, string uri); - - /// - /// Initializes the . - /// - /// The base URI. - /// The absolute URI. - /// The renderer. - void Initialize(string baseUri, string uri, IEndpointHtmlRenderer renderer); } diff --git a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs index ef7709829ad7..e9bb98f2d1c2 100644 --- a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs +++ b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs @@ -7,24 +7,11 @@ namespace Microsoft.AspNetCore.Components.Endpoints; internal sealed class HttpNavigationManager : NavigationManager, IHostEnvironmentNavigationManager { - private IEndpointHtmlRenderer? _renderer; - void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri) => Initialize(baseUri, uri); - void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer) - { - Initialize(baseUri, uri); - _renderer = renderer; - } - protected override void NavigateToCore(string uri, NavigationOptions options) { var absoluteUriString = ToAbsoluteUri(uri).AbsoluteUri; throw new NavigationException(absoluteUriString); } - - protected override void NotFoundCore() - { - _renderer?.SetNotFoundResponse(); - } } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 7d8600b439d6..b2cadba4d772 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints; /// output with prerendering markers so the content can later switch into interactive mode when used with /// blazor.*.js. It also deals with initializing the standard component DI services once per request. /// -internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrerenderer, IEndpointHtmlRenderer +internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrerenderer { private readonly IServiceProvider _services; private readonly RazorComponentsServiceOptions _options; @@ -77,7 +77,15 @@ internal async Task InitializeStandardComponentServicesAsync( IFormCollection? form = null) { var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService(); - navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request), this); + navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); + + if (navigationManager is HttpNavigationManager httpNavigationManager) + { + httpNavigationManager.NotFoundEvent += (sender, args) => + { + SetNotFoundResponse(); + }; + } var authenticationStateProvider = httpContext.RequestServices.GetService(); if (authenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthenticationStateProvider) diff --git a/src/Components/Endpoints/test/RazorComponentResultTest.cs b/src/Components/Endpoints/test/RazorComponentResultTest.cs index b7ba8e80cd0c..cbeb6712402a 100644 --- a/src/Components/Endpoints/test/RazorComponentResultTest.cs +++ b/src/Components/Endpoints/test/RazorComponentResultTest.cs @@ -513,11 +513,6 @@ class FakeNavigationManager : NavigationManager, IHostEnvironmentNavigationManag public new void Initialize(string baseUri, string uri) => base.Initialize(baseUri, uri); -#nullable enable - public void Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer = null) - => base.Initialize(baseUri, uri); -#nullable disable - protected override void NavigateToCore(string uri, NavigationOptions options) { // Equivalent to what RemoteNavigationManager would do diff --git a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs index 8dfeabb4959e..9fcc668d94c1 100644 --- a/src/Components/Server/src/Circuits/RemoteNavigationManager.cs +++ b/src/Components/Server/src/Circuits/RemoteNavigationManager.cs @@ -45,18 +45,6 @@ public RemoteNavigationManager(ILogger logger) NotifyLocationChanged(isInterceptedLink: false); } - /// - /// Initializes the . - /// - /// The base URI. - /// The absolute URI. - /// The optional renderer. - public void Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer = null) - { - base.Initialize(baseUri, uri); - NotifyLocationChanged(isInterceptedLink: false); - } - /// /// Initializes the . /// From 73f0e07c9cb6e7428b67ede1b8d71cd4c40c4d72 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 10 Mar 2025 18:01:50 +0100 Subject: [PATCH 13/21] Fix event registration. --- .../Endpoints/src/Rendering/EndpointHtmlRenderer.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index b2cadba4d772..6ed238310fe1 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -76,16 +76,13 @@ internal async Task InitializeStandardComponentServicesAsync( string? handler = null, IFormCollection? form = null) { - var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService(); - navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); + var navigationManager = httpContext.RequestServices.GetRequiredService(); + ((IHostEnvironmentNavigationManager)navigationManager)?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); - if (navigationManager is HttpNavigationManager httpNavigationManager) + navigationManager?.NotFoundEvent += (sender, args) => { - httpNavigationManager.NotFoundEvent += (sender, args) => - { - SetNotFoundResponse(); - }; - } + SetNotFoundResponse(); + }; var authenticationStateProvider = httpContext.RequestServices.GetService(); if (authenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthenticationStateProvider) From 18773272fe869abdcaac35eb9c928afecdb5edfb Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 10 Mar 2025 19:28:15 +0100 Subject: [PATCH 14/21] Trying to fix the build error. --- .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 2 +- .../Endpoints/src/Rendering/EndpointHtmlRenderer.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index cb3a081861a1..b8b712d66bae 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -74,7 +74,7 @@ private Task ReturnErrorResponse(string detailedMessage) : Task.CompletedTask; } - public void SetNotFoundResponse() + private void SetNotFoundResponse(object sender, EventArgs args) { _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 6ed238310fe1..e08bd448f565 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -79,10 +79,10 @@ internal async Task InitializeStandardComponentServicesAsync( var navigationManager = httpContext.RequestServices.GetRequiredService(); ((IHostEnvironmentNavigationManager)navigationManager)?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); - navigationManager?.NotFoundEvent += (sender, args) => + if (navigationManager != null) { - SetNotFoundResponse(); - }; + navigationManager.NotFoundEvent += SetNotFoundResponse; + } var authenticationStateProvider = httpContext.RequestServices.GetService(); if (authenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthenticationStateProvider) From 3cdd10fabd5026d6b4e6985dab6aff360d80f9a8 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 10 Mar 2025 19:45:18 +0100 Subject: [PATCH 15/21] Nullabillity --- .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index b8b712d66bae..fb96faffcd44 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -74,7 +74,7 @@ private Task ReturnErrorResponse(string detailedMessage) : Task.CompletedTask; } - private void SetNotFoundResponse(object sender, EventArgs args) + private void SetNotFoundResponse(object? sender, EventArgs args) { _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; } From bf6041bc876fa7af573289daddafdb5be5dd6a2d Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 11 Mar 2025 11:45:52 +0100 Subject: [PATCH 16/21] Limit public API. --- src/Components/Components/src/NavigationManager.cs | 5 +---- src/Components/Components/src/PublicAPI.Unshipped.txt | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 5d98000c64b2..1c8c8e01563f 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -201,10 +201,7 @@ public virtual void Refresh(bool forceReload = false) /// public virtual void NotFound() => NotFoundCore(); - /// - /// Handles setting the NotFound state. - /// - protected virtual void NotFoundCore() + private void NotFoundCore() { _notFound?.Invoke(this, new EventArgs()); } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 5a7ae3de6f3f..407dd3a9e3ae 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,4 +1,3 @@ #nullable enable Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void -virtual Microsoft.AspNetCore.Components.NavigationManager.NotFoundCore() -> void From 6ce7bda66d3ae55592198370481e1c172192907c Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 14 Mar 2025 18:24:54 +0100 Subject: [PATCH 17/21] Dispose renderer + move tests. --- .../EndpointHtmlRenderer.EventDispatch.cs | 5 +++ .../src/Rendering/EndpointHtmlRenderer.cs | 6 +++ .../NoInteractivityTest.cs | 17 -------- .../ServerRenderingTests/RenderingTest.cs | 11 ------ .../E2ETest/Tests/GlobalInteractivityTest.cs | 39 +++++++++++++++++++ .../Rendering/NotFoundInteractiveServer.razor | 12 ------ .../NotFoundInteractiveWebassembly.razor | 12 ------ .../Pages/NotFoundInteractiveServer.razor | 17 ++++++++ .../NotFoundInteractiveWebassembly.razor | 17 ++++++++ .../Pages}/NotFoundSSR.razor | 0 10 files changed, 84 insertions(+), 52 deletions(-) delete mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveServer.razor delete mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveWebassembly.razor create mode 100644 src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundInteractiveServer.razor create mode 100644 src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundInteractiveWebassembly.razor rename src/Components/test/testassets/{Components.TestServer/RazorComponents/Pages/Rendering => Components.WasmMinimal/Pages}/NotFoundSSR.razor (100%) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index fb96faffcd44..b9c55c2fe679 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -76,7 +76,12 @@ private Task ReturnErrorResponse(string detailedMessage) private void SetNotFoundResponse(object? sender, EventArgs args) { + if (_httpContext.Response.HasStarted) + { + throw new InvalidOperationException("Cannot set a NotFound response after the response has already started."); + } _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; + StopRenderer(); } private void UpdateNamedSubmitEvents(in RenderBatch renderBatch) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index e08bd448f565..371357ecd230 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -173,6 +173,12 @@ protected override void AddPendingTask(ComponentState? componentState, Task task base.AddPendingTask(componentState, task); } + private void StopRenderer() + { + _nonStreamingPendingTasks.Clear(); + Dispose(); + } + // For tests only internal Task? NonStreamingPendingTasksCompletion; diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index fd8ef529f739..0b30003f4b70 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -26,23 +26,6 @@ public NoInteractivityTest( public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); - [Fact] - public async Task CansSetNotFoundStatus() - { - var url = $"{ServerPathBase}/render-not-found-ssr"; - Navigate(url); - var statusCode = await GetStatusCodeAsync(url); - Assert.Equal(404, statusCode); - } - - private async Task GetStatusCodeAsync(string relativeUrl) - { - using var client = new HttpClient(); - string absoluteUrl = $"{_serverFixture.RootUri}/{relativeUrl}"; - var response = await client.GetAsync(absoluteUrl); - return (int)response.StatusCode; - } - [Fact] public void NavigationManagerCanRefreshSSRPageWhenInteractivityNotPresent() { diff --git a/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs index 2cc8daaa64ba..16a1387feb89 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs @@ -27,17 +27,6 @@ public RenderingTest( public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); - [Theory] - [InlineData("server")] - [InlineData("webassembly")] - public void CanRenderNotFoundInteractive(string renderingMode) - { - Navigate($"{ServerPathBase}/render-not-found-{renderingMode}"); - - var bodyText = Browser.FindElement(By.TagName("body")).Text; - Assert.Contains("There's nothing here", bodyText); - } - [Fact] public void CanRenderLargeComponentsWithServerRenderMode() { diff --git a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs index c35ca3680d8f..a9451f5947fa 100644 --- a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net; +using System.Net.Http; using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; @@ -17,6 +20,42 @@ public class GlobalInteractivityTest( ITestOutputHelper output) : ServerTestBase>>(browserFixture, serverFixture, output) { + + [Theory] + [InlineData("server", true)] + [InlineData("webassembly", true)] + [InlineData("ssr", false)] + public void CanRenderNotFoundInteractive(string renderingMode, bool isInteractive) + { + Navigate($"/subdir/render-not-found-{renderingMode}"); + + if (isInteractive) + { + var buttonId = "trigger-not-found"; + Browser.WaitForElementToBeVisible(By.Id(buttonId)); + Browser.Exists(By.Id(buttonId)).Click(); + } + + var bodyText = Browser.FindElement(By.TagName("body")).Text; + Assert.Contains("There's nothing here", bodyText); + } + + [Fact] + public async Task CanSetNotFoundStatus() + { + var statusCode = await GetStatusCodeAsync($"/subdir/render-not-found-ssr"); + // problem: subscription to OnNotFound queued the render event at the time when the renderer was not disposed and wants to render when it already is (we dispose of it in SetNotFoundResponse) + // fragment rendering does not make sense in this case but how to prevent it? + Assert.Equal(HttpStatusCode.NotFound, statusCode); + } + + private async Task GetStatusCodeAsync(string relativeUrl) + { + using var client = new HttpClient() { BaseAddress = _serverFixture.RootUri }; + var response = await client.GetAsync(relativeUrl, HttpCompletionOption.ResponseHeadersRead); + return response.StatusCode; + } + [Fact] public void CanFindStaticallyRenderedPageAfterClickingBrowserBackButtonOnDynamicallyRenderedPage() { diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveServer.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveServer.razor deleted file mode 100644 index c41776ff528c..000000000000 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveServer.razor +++ /dev/null @@ -1,12 +0,0 @@ -@page "/render-not-found-server" -@rendermode RenderMode.InteractiveServer -@inject NavigationManager NavigationManager - -

    Any content

    - -@code{ - protected override void OnInitialized() - { - NavigationManager.NotFound(); - } -} \ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveWebassembly.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveWebassembly.razor deleted file mode 100644 index b05fa74156a6..000000000000 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundInteractiveWebassembly.razor +++ /dev/null @@ -1,12 +0,0 @@ -@page "/render-not-found-webassembly" -@rendermode RenderMode.InteractiveWebAssembly -@inject NavigationManager NavigationManager - -

    Any content

    - -@code{ - protected override void OnInitialized() - { - NavigationManager.NotFound(); - } -} \ No newline at end of file diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundInteractiveServer.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundInteractiveServer.razor new file mode 100644 index 000000000000..1212703d4b4f --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundInteractiveServer.razor @@ -0,0 +1,17 @@ +@page "/render-not-found-server" +@using Microsoft.AspNetCore.Components; +@using Microsoft.AspNetCore.Components.Web; +@inject NavigationManager NavigationManager + +

    Any content

    +@if(RendererInfo.IsInteractive) +{ + +} + +@code{ + private void TriggerNotFound() + { + NavigationManager.NotFound(); + } +} diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundInteractiveWebassembly.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundInteractiveWebassembly.razor new file mode 100644 index 000000000000..91f31dec78a4 --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundInteractiveWebassembly.razor @@ -0,0 +1,17 @@ +@page "/render-not-found-webassembly" +@using Microsoft.AspNetCore.Components; +@using Microsoft.AspNetCore.Components.Web; +@inject NavigationManager NavigationManager + +

    Any content

    +@if(RendererInfo.IsInteractive) +{ + +} + +@code{ + private void TriggerNotFound() + { + NavigationManager.NotFound(); + } +} \ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundSSR.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundSSR.razor similarity index 100% rename from src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Rendering/NotFoundSSR.razor rename to src/Components/test/testassets/Components.WasmMinimal/Pages/NotFoundSSR.razor From c89cd5723a4d8557eb23bc4d08968f8bee359031 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 17 Mar 2025 13:38:20 +0100 Subject: [PATCH 18/21] @javiercn's fixes --- .../src/Rendering/EndpointHtmlRenderer.cs | 15 ++++++++++++--- .../test/E2ETest/Tests/GlobalInteractivityTest.cs | 2 -- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 2d2278a890e3..0bcf70dc8247 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -43,6 +43,7 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer private Task? _servicesInitializedTask; private HttpContext _httpContext = default!; // Always set at the start of an inbound call private ResourceAssetCollection? _resourceCollection; + private bool _rendererIsStopped; // The underlying Renderer always tracks the pending tasks representing *full* quiescence, i.e., // when everything (regardless of streaming SSR) is fully complete. In this subclass we also track @@ -177,8 +178,16 @@ protected override void AddPendingTask(ComponentState? componentState, Task task private void StopRenderer() { - _nonStreamingPendingTasks.Clear(); - Dispose(); + _rendererIsStopped = true; + } + + protected override void ProcessPendingRender() + { + if (_rendererIsStopped) + { + return; + } + base.ProcessPendingRender(); } // For tests only @@ -188,7 +197,7 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) { UpdateNamedSubmitEvents(in renderBatch); - if (_streamingUpdatesWriter is { } writer) + if (_streamingUpdatesWriter is { } writer && !_rendererIsStopped) { // Important: SendBatchAsStreamingUpdate *must* be invoked synchronously // before any 'await' in this method. That's enforced by the compiler diff --git a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs index a9451f5947fa..2f1f70aa7511 100644 --- a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs @@ -44,8 +44,6 @@ public void CanRenderNotFoundInteractive(string renderingMode, bool isInteractiv public async Task CanSetNotFoundStatus() { var statusCode = await GetStatusCodeAsync($"/subdir/render-not-found-ssr"); - // problem: subscription to OnNotFound queued the render event at the time when the renderer was not disposed and wants to render when it already is (we dispose of it in SetNotFoundResponse) - // fragment rendering does not make sense in this case but how to prevent it? Assert.Equal(HttpStatusCode.NotFound, statusCode); } From 2c380ea677e2146edb1a72b08904ca4e44f20cc7 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 17 Mar 2025 15:46:54 +0100 Subject: [PATCH 19/21] Feedback - rename + comment. --- .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 2 +- .../Endpoints/src/Rendering/EndpointHtmlRenderer.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index b9c55c2fe679..2b8455741f52 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -81,7 +81,7 @@ private void SetNotFoundResponse(object? sender, EventArgs args) throw new InvalidOperationException("Cannot set a NotFound response after the response has already started."); } _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; - StopRenderer(); + SignalRendererToFinishRendering(); } private void UpdateNamedSubmitEvents(in RenderBatch renderBatch) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 0bcf70dc8247..428e84d3ead5 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -176,7 +176,7 @@ protected override void AddPendingTask(ComponentState? componentState, Task task base.AddPendingTask(componentState, task); } - private void StopRenderer() + private void SignalRendererToFinishRendering() { _rendererIsStopped = true; } @@ -185,6 +185,9 @@ protected override void ProcessPendingRender() { if (_rendererIsStopped) { + // When the application triggers a NotFound event, we continue rendering the current batch. + // However, after completing this batch, we do not want to process any further UI updates, + // as we are going to return a 404 status and discard the UI updates generated so far. return; } base.ProcessPendingRender(); From 3ed2b74e0f1508a6c8116ac81cc0b870ab2ad0fa Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 18 Mar 2025 09:53:36 +0100 Subject: [PATCH 20/21] Fix ailing test - do not subscribe if manager is not initialized. --- src/Components/Components/src/NavigationManager.cs | 9 +++++++++ src/Components/Components/src/PublicAPI.Unshipped.txt | 1 + .../Endpoints/src/Rendering/EndpointHtmlRenderer.cs | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 1c8c8e01563f..0e14d248aa41 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -62,6 +62,15 @@ public event EventHandler NotFoundEvent private string? _uri; private bool _isInitialized; + /// + /// Gets a value indicating whether the has been initialized. + /// + /// + /// The must be initialized before it can be used to navigate or handle navigation events. + /// Initialization typically involves setting the base URI and the current URI. + /// + public bool IsInitialized => _isInitialized; + /// /// Gets or sets the current base URI. The is always represented as an absolute URI in string form with trailing slash. /// Typically this corresponds to the 'href' attribute on the document's <base> element. diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 441f46ffe210..29cea6506040 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,6 +1,7 @@ #nullable enable Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void +Microsoft.AspNetCore.Components.NavigationManager.IsInitialized.get -> bool Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.SetPlatformRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 428e84d3ead5..44c5373d337d 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -81,7 +81,7 @@ internal async Task InitializeStandardComponentServicesAsync( var navigationManager = httpContext.RequestServices.GetRequiredService(); ((IHostEnvironmentNavigationManager)navigationManager)?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); - if (navigationManager != null) + if (navigationManager != null && navigationManager.IsInitialized) { navigationManager.NotFoundEvent += SetNotFoundResponse; } From d8751ab041eb3f46d83a492267bc65603e4e62a6 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 18 Mar 2025 13:42:32 +0100 Subject: [PATCH 21/21] Change the test setup instead changing the NavigationManager. --- .../Components/src/NavigationManager.cs | 9 --------- .../Components/src/PublicAPI.Unshipped.txt | 1 - .../src/Rendering/EndpointHtmlRenderer.cs | 2 +- .../test/ComponentTagHelperTest.cs | 17 +++++++++++++---- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 0e14d248aa41..1c8c8e01563f 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -62,15 +62,6 @@ public event EventHandler NotFoundEvent private string? _uri; private bool _isInitialized; - /// - /// Gets a value indicating whether the has been initialized. - /// - /// - /// The must be initialized before it can be used to navigate or handle navigation events. - /// Initialization typically involves setting the base URI and the current URI. - /// - public bool IsInitialized => _isInitialized; - /// /// Gets or sets the current base URI. The is always represented as an absolute URI in string form with trailing slash. /// Typically this corresponds to the 'href' attribute on the document's <base> element. diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 29cea6506040..441f46ffe210 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,7 +1,6 @@ #nullable enable Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void -Microsoft.AspNetCore.Components.NavigationManager.IsInitialized.get -> bool Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.SetPlatformRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 44c5373d337d..428e84d3ead5 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -81,7 +81,7 @@ internal async Task InitializeStandardComponentServicesAsync( var navigationManager = httpContext.RequestServices.GetRequiredService(); ((IHostEnvironmentNavigationManager)navigationManager)?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); - if (navigationManager != null && navigationManager.IsInitialized) + if (navigationManager != null) { navigationManager.NotFoundEvent += SetNotFoundResponse; } diff --git a/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs index 18e7efa00274..cefb5d70a82e 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs @@ -77,9 +77,6 @@ private static TagHelperOutput GetTagHelperOutput() private ViewContext GetViewContext() { - var navManager = new Mock(); - navManager.As(); - var httpContext = new DefaultHttpContext { RequestServices = new ServiceCollection() @@ -89,7 +86,7 @@ private ViewContext GetViewContext() x => x.CreateProtector(It.IsAny()) == Mock.Of())) .AddLogging() .AddScoped() - .AddScoped(_ => navManager.Object) + .AddScoped() .AddScoped() .BuildServiceProvider(), }; @@ -104,4 +101,16 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddContent(0, "Hello from the component"); } } + + class MockNavigationManager : NavigationManager, IHostEnvironmentNavigationManager + { + public MockNavigationManager() + { + Initialize("https://localhost:85/subdir/", "https://localhost:85/subdir/path?query=value#hash"); + } + + void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri) + { + } + } }