Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
2 changes: 1 addition & 1 deletion src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>? properties) -> void
Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>? properties = null) -> void
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type!
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type?
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.set -> void
Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions
Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHandler<Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs!>!
Expand Down
13 changes: 12 additions & 1 deletion src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,15 @@ static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary
/// Gets or sets the content to display when no match is found for the requested route.
/// </summary>
[Parameter]
[Obsolete("NotFound is deprecated. Use NotFoundPage instead.")]
public RenderFragment NotFound { get; set; }

/// <summary>
/// Gets or sets the page content to display when no match is found for the requested route.
/// </summary>
[Parameter]
[DynamicallyAccessedMembers(LinkerFlags.Component)]
public Type NotFoundPage { get; set; } = default!;
public Type? NotFoundPage { get; set; }

/// <summary>
/// Gets or sets the content to display when a match is found for the requested route.
Expand Down Expand Up @@ -143,6 +144,12 @@ public async Task SetParametersAsync(ParameterView parameters)

if (NotFoundPage != null)
{
#pragma warning disable CS0618 // Type or member is obsolete
if (NotFound != null)
{
throw new InvalidOperationException("Both NotFound and NotFoundPage parameters are set on Router component. NotFoundPage is preferred and NotFound will be deprecated. Consider using only NotFoundPage.");
}
#pragma warning restore CS0618 // Type or member is obsolete
if (!typeof(IComponent).IsAssignableFrom(NotFoundPage))
{
throw new InvalidOperationException($"The type {NotFoundPage.FullName} " +
Expand Down Expand Up @@ -401,10 +408,12 @@ private void RenderNotFound()
new RouteData(NotFoundPage, _emptyParametersDictionary));
builder.CloseComponent();
}
#pragma warning disable CS0618 // Type or member is obsolete
else if (NotFound != null)
{
NotFound(builder);
}
#pragma warning restore CS0618 // Type or member is obsolete
else
{
DefaultNotFoundContent(builder);
Expand All @@ -429,6 +438,7 @@ async Task IHandleAfterRender.OnAfterRenderAsync()

private static partial class Log
{
#pragma warning disable CS0618 // Type or member is obsolete
[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);

Expand All @@ -440,5 +450,6 @@ private static partial class Log

[LoggerMessage(4, LogLevel.Debug, $"Displaying {nameof(NotFound)} on request", EventName = "DisplayingNotFoundOnRequest")]
internal static partial void DisplayingNotFound(ILogger logger);
#pragma warning restore CS0618 // Type or member is obsolete
}
}
83 changes: 83 additions & 0 deletions src/Components/Components/test/Routing/RouterTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable CS0618 // Type or member is obsolete

using System.Reflection;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
Expand Down Expand Up @@ -265,6 +267,40 @@ await _renderer.Dispatcher.InvokeAsync(() =>
Assert.Equal("Not found", renderedFrame.TextContent);
}

[Fact]
public async Task ThrowsExceptionWhenBothNotFoundAndNotFoundPageAreSet()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
services.AddSingleton<NavigationManager>(_navigationManager);
services.AddSingleton<INavigationInterception, TestNavigationInterception>();
services.AddSingleton<IScrollToLocationHash, TestScrollToLocationHash>();
var serviceProvider = services.BuildServiceProvider();

var renderer = new TestRenderer(serviceProvider);
renderer.ShouldHandleExceptions = true;
var router = (Router)renderer.InstantiateComponent<Router>();
router.AppAssembly = Assembly.GetExecutingAssembly();
router.Found = routeData => (builder) => builder.AddContent(0, $"Rendering route matching {routeData.PageType}");
renderer.AssignRootComponentId(router);

var parameters = new Dictionary<string, object>
{
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
{ nameof(Router.NotFound), (RenderFragment)(builder => builder.AddContent(0, "Custom not found")) },
{ nameof(Router.NotFoundPage), typeof(NotFoundTestComponent) }
};

// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await renderer.Dispatcher.InvokeAsync(() =>
router.SetParametersAsync(ParameterView.FromDictionary(parameters))));

Assert.Contains("Both NotFound and NotFoundPage parameters are set on Router component", exception.Message);
Assert.Contains("NotFoundPage is preferred and NotFound will be deprecated", exception.Message);
}

internal class TestNavigationManager : NavigationManager
{
public TestNavigationManager() =>
Expand Down Expand Up @@ -306,4 +342,51 @@ public class MatchAnythingComponent : ComponentBase { }

[Route("a/b/c")]
public class MultiSegmentRouteComponent : ComponentBase { }

[Route("not-found")]
public class NotFoundTestComponent : ComponentBase { }

public class TestLogger<T> : ILogger<T>
{
public List<LogEntry> LogEntries { get; } = new List<LogEntry>();

public IDisposable BeginScope<TState>(TState state) => null;

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
LogEntries.Add(new LogEntry
{
LogLevel = logLevel,
EventId = eventId,
Message = formatter(state, exception),
Exception = exception
});
}
}

public class LogEntry
{
public LogLevel LogLevel { get; set; }
public EventId EventId { get; set; }
public string Message { get; set; }
public Exception Exception { get; set; }
}

public class TestLoggerFactory : ILoggerFactory
{
private readonly ILogger _logger;

public TestLoggerFactory(ILogger logger)
{
_logger = logger;
}

public void AddProvider(ILoggerProvider provider) { }

public ILogger CreateLogger(string categoryName) => _logger;

public void Dispose() { }
}
}
Loading