Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Components/Components/src/IEndpointHtmlRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// An interface for EndpointHtmlRenderer implementations that allows NavigationManagers call renderer's methods
/// </summary>
public interface IEndpointHtmlRenderer
{
/// <summary>
/// Sets the html response to 404 Not Found.
/// </summary>
void SetNotFoundResponse();
}
44 changes: 44 additions & 0 deletions src/Components/Components/src/NavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ public event EventHandler<LocationChangedEventArgs> LocationChanged

private CancellationTokenSource? _locationChangingCts;

/// <summary>
/// An event that fires when the page is not found.
/// </summary>
public event EventHandler<EventArgs> NotFoundEvent
{
add
{
AssertInitialized();
_notFound += value;
}
remove
{
AssertInitialized();
_notFound -= value;
}
}

private EventHandler<EventArgs>? _notFound;

// 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;
Expand Down Expand Up @@ -177,6 +196,16 @@ protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)]
public virtual void Refresh(bool forceReload = false)
=> NavigateTo(Uri, forceLoad: true, replace: true);

/// <summary>
/// Handles setting the NotFound state.
/// </summary>
public virtual void NotFound() => NotFoundCore();

/// <summary>
/// Handles setting the NotFound state.
/// </summary>
protected virtual void NotFoundCore() => throw new NotImplementedException();

/// <summary>
/// Called to initialize BaseURI and current URI before these values are used for the first time.
/// Override <see cref="EnsureInitialized" /> and call this method to dynamically calculate these values.
Expand Down Expand Up @@ -308,6 +337,21 @@ protected void NotifyLocationChanged(bool isInterceptedLink)
}
}

/// <summary>
/// Triggers the <see cref="NotFound"/> event with the current URI value.
/// </summary>
protected void NotifyNotFound()
{
try
{
_notFound?.Invoke(this, new EventArgs());
}
catch (Exception ex)
{
throw new NotFoundRenderingException("An exception occurred while dispatching a NotFound event.", ex);
}
}

/// <summary>
/// Notifies the registered handlers of the current location change.
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions src/Components/Components/src/NotFoundRenderingException.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Exception thrown when an <see cref="NavigationManager"/> is not able to render not found page.
/// </summary>
public class NotFoundRenderingException : Exception
{
/// <summary>
/// Creates a new instance of <see cref="NotFoundRenderingException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The inner exception.</param>
public NotFoundRenderingException(string message, Exception innerException)
: base(message, innerException)
{
}
}
9 changes: 9 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
#nullable enable
Microsoft.AspNetCore.Components.IEndpointHtmlRenderer
Microsoft.AspNetCore.Components.IEndpointHtmlRenderer.SetNotFoundResponse() -> void
Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler<System.EventArgs!>!
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ public interface IHostEnvironmentNavigationManager
/// <param name="baseUri">The base URI.</param>
/// <param name="uri">The absolute URI.</param>
void Initialize(string baseUri, string uri);

/// <summary>
/// Initializes the <see cref="NavigationManager" />.
/// </summary>
/// <param name="baseUri">The base URI.</param>
/// <param name="uri">The absolute URI.</param>
/// <param name="renderer">The renderer.</param>
void Initialize(string baseUri, string uri, IEndpointHtmlRenderer renderer);
}
14 changes: 14 additions & 0 deletions src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public void Attach(RenderHandle renderHandle)
_baseUri = NavigationManager.BaseUri;
_locationAbsolute = NavigationManager.Uri;
NavigationManager.LocationChanged += OnLocationChanged;
NavigationManager.NotFoundEvent += OnNotFound;
RoutingStateProvider = ServiceProvider.GetService<IRoutingStateProvider>();

if (HotReloadManager.Default.MetadataUpdateSupported)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -320,6 +322,15 @@ private void OnLocationChanged(object sender, LocationChangedEventArgs args)
}
}

private void OnNotFound(object sender, EventArgs args)
{
if (_renderHandle.IsInitialized)
{
Log.DisplayingNotFound(_logger);
_renderHandle.Render(NotFound ?? DefaultNotFoundContent);
}
}

async Task IHandleAfterRender.OnAfterRenderAsync()
{
if (!_navigationInterceptionEnabled)
Expand All @@ -345,5 +356,8 @@ private static partial class Log

[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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,24 @@ 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private async Task RenderComponentCore(HttpContext context)
return Task.CompletedTask;
});

await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync(
await _renderer.InitializeStandardComponentServicesAsync(
context,
componentType: pageComponent,
handler: result.HandlerName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ private Task ReturnErrorResponse(string detailedMessage)
: Task.CompletedTask;
}

public void SetNotFoundResponse()
{
_httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
}

private void UpdateNamedSubmitEvents(in RenderBatch renderBatch)
{
if (renderBatch.NamedEventChanges is { } changes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrerenderer
internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrerenderer, IEndpointHtmlRenderer
{
private readonly IServiceProvider _services;
private readonly RazorComponentsServiceOptions _options;
Expand Down Expand Up @@ -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>();
navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request));
navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request), this);

var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>();
if (authenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthenticationStateProvider)
Expand Down
5 changes: 5 additions & 0 deletions src/Components/Endpoints/test/RazorComponentResultTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,11 @@ 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
Expand Down
34 changes: 34 additions & 0 deletions src/Components/Server/src/Circuits/RemoteNavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ public RemoteNavigationManager(ILogger<RemoteNavigationManager> logger)
NotifyLocationChanged(isInterceptedLink: false);
}

/// <summary>
/// Initializes the <see cref="NavigationManager" />.
/// </summary>
/// <param name="baseUri">The base URI.</param>
/// <param name="uri">The absolute URI.</param>
/// <param name="renderer">The optional renderer.</param>
public void Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer = null)
{
base.Initialize(baseUri, uri);
NotifyLocationChanged(isInterceptedLink: false);
}

/// <summary>
/// Initializes the <see cref="RemoteNavigationManager"/>.
/// </summary>
Expand Down Expand Up @@ -148,6 +160,22 @@ async Task RefreshAsync()
}
}

/// <inheritdoc />
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);
Expand Down Expand Up @@ -197,10 +225,16 @@ public static void RequestingNavigation(ILogger logger, string uri, NavigationOp
[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(8, LogLevel.Error, "Failed to render NotFound", EventName = "NotFoundRenderFailed")]
public static partial void NotFoundRenderFailed(ILogger logger, Exception exception);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ public override void Refresh(bool forceReload = false)
DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(Interop.Refresh, forceReload);
}

/// <inheritdoc />
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);
Expand All @@ -100,5 +113,8 @@ 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,6 +26,23 @@ 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<int> 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()
{
Expand Down
11 changes: 11 additions & 0 deletions src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading
Loading