Skip to content

Commit 0c51c1f

Browse files
Merge main into darc-main-524988a9-5adc-4eff-befd-e7a92585aa9f
2 parents 7d7e819 + 285f843 commit 0c51c1f

File tree

97 files changed

+2384
-2241
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+2384
-2241
lines changed

src/Components/Components/src/ComponentsActivitySource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public static ComponentsActivityHandle StartHandleEventActivity(string? componen
7878
}
7979
if (methodName != null)
8080
{
81-
activity.SetTag("aspnetcore.components.method", methodName);
81+
activity.SetTag("code.function.name", methodName);
8282
}
8383
if (attributeName != null)
8484
{

src/Components/Components/src/ComponentsMetrics.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public async Task CaptureEventDuration(Task task, long startTimestamp, string? c
8383
var tags = new TagList
8484
{
8585
{ "aspnetcore.components.type", componentType ?? "unknown" },
86-
{ "aspnetcore.components.method", methodName ?? "unknown" },
86+
{ "code.function.name", methodName ?? "unknown" },
8787
{ "aspnetcore.components.attribute.name", attributeName ?? "unknown" }
8888
};
8989

@@ -104,7 +104,7 @@ public void FailEventSync(Exception ex, long startTimestamp, string? componentTy
104104
var tags = new TagList
105105
{
106106
{ "aspnetcore.components.type", componentType ?? "unknown" },
107-
{ "aspnetcore.components.method", methodName ?? "unknown" },
107+
{ "code.function.name", methodName ?? "unknown" },
108108
{ "aspnetcore.components.attribute.name", attributeName ?? "unknown" },
109109
{ "error.type", ex.GetType().FullName ?? "unknown" }
110110
};

src/Components/Components/src/Routing/Router.cs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -391,13 +391,55 @@ private void OnLocationChanged(object sender, LocationChangedEventArgs args)
391391

392392
private void OnNotFound(object sender, NotFoundEventArgs args)
393393
{
394-
if (_renderHandle.IsInitialized && NotFoundPage != null)
394+
bool renderContentIsProvided = NotFoundPage != null || args.Path != null;
395+
if (_renderHandle.IsInitialized && renderContentIsProvided)
395396
{
396-
// setting the path signals to the endpoint renderer that router handled rendering
397-
args.Path = _notFoundPageRoute;
398-
Log.DisplayingNotFound(_logger);
399-
RenderNotFound();
397+
if (!string.IsNullOrEmpty(args.Path))
398+
{
399+
// The path can be set by a subscriber not defined in blazor framework.
400+
_renderHandle.Render(builder => RenderComponentByRoute(builder, args.Path));
401+
}
402+
else
403+
{
404+
// Having the path set signals to the endpoint renderer that router handled rendering.
405+
args.Path = _notFoundPageRoute;
406+
RenderNotFound();
407+
}
408+
Log.DisplayingNotFound(_logger, args.Path);
409+
}
410+
}
411+
412+
internal void RenderComponentByRoute(RenderTreeBuilder builder, string route)
413+
{
414+
var componentType = FindComponentTypeByRoute(route);
415+
416+
if (componentType is null)
417+
{
418+
throw new InvalidOperationException($"No component found for route '{route}'. " +
419+
$"Ensure the route matches a component with a [Route] attribute.");
400420
}
421+
422+
builder.OpenComponent<RouteView>(0);
423+
builder.AddAttribute(1, nameof(RouteView.RouteData),
424+
new RouteData(componentType, new Dictionary<string, object>()));
425+
builder.CloseComponent();
426+
}
427+
428+
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
429+
internal Type? FindComponentTypeByRoute(string route)
430+
{
431+
RefreshRouteTable();
432+
var normalizedRoute = route.StartsWith('/') ? route : $"/{route}";
433+
434+
var context = new RouteContext(normalizedRoute);
435+
Routes.Route(context);
436+
437+
if (context.Handler is not null && typeof(IComponent).IsAssignableFrom(context.Handler))
438+
{
439+
return context.Handler;
440+
}
441+
442+
return null;
401443
}
402444

403445
private void RenderNotFound()
@@ -451,8 +493,8 @@ private static partial class Log
451493
[LoggerMessage(3, LogLevel.Debug, "Navigating to non-component URI '{ExternalUri}' in response to path '{Path}' with base URI '{BaseUri}'", EventName = "NavigatingToExternalUri")]
452494
internal static partial void NavigatingToExternalUri(ILogger logger, string externalUri, string path, string baseUri);
453495

454-
[LoggerMessage(4, LogLevel.Debug, $"Displaying {nameof(NotFound)} on request", EventName = "DisplayingNotFoundOnRequest")]
455-
internal static partial void DisplayingNotFound(ILogger logger);
496+
[LoggerMessage(4, LogLevel.Debug, $"Displaying contents of {{displayedContentPath}} on request", EventName = "DisplayingNotFoundOnRequest")]
497+
internal static partial void DisplayingNotFound(ILogger logger, string displayedContentPath);
456498
#pragma warning restore CS0618 // Type or member is obsolete
457499
}
458500
}

src/Components/Components/test/ComponentsActivitySourceTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public void StartEventActivity_CreatesAndStartsActivity()
9696
Assert.Equal(ActivityKind.Internal, activity.Kind);
9797
Assert.True(activity.IsAllDataRequested);
9898
Assert.Equal(componentType, activity.GetTagItem("aspnetcore.components.type"));
99-
Assert.Equal(methodName, activity.GetTagItem("aspnetcore.components.method"));
99+
Assert.Equal(methodName, activity.GetTagItem("code.function.name"));
100100
Assert.Equal(attributeName, activity.GetTagItem("aspnetcore.components.attribute.name"));
101101
Assert.False(activity.IsStopped);
102102

src/Components/Components/test/ComponentsMetricsTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp,
8787
Assert.Single(measurements);
8888
Assert.True(measurements[0].Value > 0);
8989
Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
90-
Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", measurements[0].Tags));
90+
Assert.Equal("OnClick", Assert.Contains("code.function.name", measurements[0].Tags));
9191
Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", measurements[0].Tags));
9292
Assert.DoesNotContain("error.type", measurements[0].Tags);
9393
}
@@ -112,7 +112,7 @@ await componentsMetrics.CaptureEventDuration(Task.FromException(new InvalidOpera
112112
Assert.Single(measurements);
113113
Assert.True(measurements[0].Value > 0);
114114
Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
115-
Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", measurements[0].Tags));
115+
Assert.Equal("OnClick", Assert.Contains("code.function.name", measurements[0].Tags));
116116
Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", measurements[0].Tags));
117117
Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
118118
}
@@ -137,7 +137,7 @@ public void FailEventSync_RecordsErrorMetric()
137137
Assert.Single(measurements);
138138
Assert.True(measurements[0].Value > 0);
139139
Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
140-
Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", measurements[0].Tags));
140+
Assert.Equal("OnClick", Assert.Contains("code.function.name", measurements[0].Tags));
141141
Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", measurements[0].Tags));
142142
Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
143143
}
@@ -372,7 +372,7 @@ await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp2
372372
// Check event duration
373373
Assert.True(eventMeasurements[0].Value > 0);
374374
Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", eventMeasurements[0].Tags));
375-
Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", eventMeasurements[0].Tags));
375+
Assert.Equal("OnClick", Assert.Contains("code.function.name", eventMeasurements[0].Tags));
376376
Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", eventMeasurements[0].Tags));
377377

378378
// Check parameters duration

src/Components/Components/test/Routing/RouterTest.cs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System.Reflection;
77
using Microsoft.AspNetCore.Components.RenderTree;
8+
using Microsoft.AspNetCore.Components.Rendering;
89
using Microsoft.AspNetCore.Components.Test.Helpers;
910
using Microsoft.Extensions.DependencyInjection;
1011
using Microsoft.Extensions.Logging;
@@ -301,6 +302,192 @@ await renderer.Dispatcher.InvokeAsync(() =>
301302
Assert.Contains("Use either NotFound or NotFoundPage", exception.Message);
302303
}
303304

305+
[Fact]
306+
public async Task OnNotFound_WithNotFoundPageSet_UsesNotFoundPage()
307+
{
308+
// Create a new router instance for this test to control Attach() timing
309+
var services = new ServiceCollection();
310+
var testNavManager = new TestNavigationManager();
311+
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
312+
services.AddSingleton<NavigationManager>(testNavManager);
313+
services.AddSingleton<INavigationInterception, TestNavigationInterception>();
314+
services.AddSingleton<IScrollToLocationHash, TestScrollToLocationHash>();
315+
var serviceProvider = services.BuildServiceProvider();
316+
317+
var testRenderer = new TestRenderer(serviceProvider);
318+
testRenderer.ShouldHandleExceptions = true;
319+
var testRouter = (Router)testRenderer.InstantiateComponent<Router>();
320+
testRouter.AppAssembly = Assembly.GetExecutingAssembly();
321+
testRouter.Found = routeData => (builder) => builder.AddContent(0, $"Rendering route matching {routeData.PageType}");
322+
323+
var parameters = new Dictionary<string, object>
324+
{
325+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
326+
{ nameof(Router.NotFoundPage), typeof(NotFoundTestComponent) }
327+
};
328+
329+
// Assign the root component ID which will call Attach()
330+
testRenderer.AssignRootComponentId(testRouter);
331+
332+
// Act
333+
await testRenderer.Dispatcher.InvokeAsync(() =>
334+
testRouter.SetParametersAsync(ParameterView.FromDictionary(parameters)));
335+
336+
// Trigger the NavigationManager's OnNotFound event
337+
await testRenderer.Dispatcher.InvokeAsync(() => testNavManager.TriggerNotFound());
338+
339+
// Assert
340+
var lastBatch = testRenderer.Batches.Last();
341+
var renderedFrame = lastBatch.ReferenceFrames.First();
342+
Assert.Equal(RenderTreeFrameType.Component, renderedFrame.FrameType);
343+
Assert.Equal(typeof(RouteView), renderedFrame.ComponentType);
344+
345+
// Verify that the RouteData contains the NotFoundTestComponent
346+
var routeViewFrame = lastBatch.ReferenceFrames.Skip(1).First();
347+
Assert.Equal(RenderTreeFrameType.Attribute, routeViewFrame.FrameType);
348+
var routeData = (RouteData)routeViewFrame.AttributeValue;
349+
Assert.Equal(typeof(NotFoundTestComponent), routeData.PageType);
350+
}
351+
352+
[Fact]
353+
public async Task OnNotFound_WithArgsPathSet_RendersComponentByRoute()
354+
{
355+
// Create a new router instance for this test to control Attach() timing
356+
var services = new ServiceCollection();
357+
var testNavManager = new TestNavigationManager();
358+
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
359+
services.AddSingleton<NavigationManager>(testNavManager);
360+
services.AddSingleton<INavigationInterception, TestNavigationInterception>();
361+
services.AddSingleton<IScrollToLocationHash, TestScrollToLocationHash>();
362+
var serviceProvider = services.BuildServiceProvider();
363+
364+
var testRenderer = new TestRenderer(serviceProvider);
365+
testRenderer.ShouldHandleExceptions = true;
366+
var testRouter = (Router)testRenderer.InstantiateComponent<Router>();
367+
testRouter.AppAssembly = Assembly.GetExecutingAssembly();
368+
testRouter.Found = routeData => (builder) => builder.AddContent(0, $"Rendering route matching {routeData.PageType}");
369+
370+
var parameters = new Dictionary<string, object>
371+
{
372+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly }
373+
};
374+
375+
// Subscribe to OnNotFound event BEFORE router attaches and set args.Path
376+
testNavManager.OnNotFound += (sender, args) =>
377+
{
378+
args.Path = "/jan"; // Point to an existing route
379+
};
380+
381+
// Assign the root component ID which will call Attach()
382+
testRenderer.AssignRootComponentId(testRouter);
383+
384+
// Act
385+
await testRenderer.Dispatcher.InvokeAsync(() =>
386+
testRouter.SetParametersAsync(ParameterView.FromDictionary(parameters)));
387+
388+
// Trigger the NavigationManager's OnNotFound event
389+
await testRenderer.Dispatcher.InvokeAsync(() => testNavManager.TriggerNotFound());
390+
391+
// Assert
392+
var lastBatch = testRenderer.Batches.Last();
393+
var renderedFrame = lastBatch.ReferenceFrames.First();
394+
Assert.Equal(RenderTreeFrameType.Component, renderedFrame.FrameType);
395+
Assert.Equal(typeof(RouteView), renderedFrame.ComponentType);
396+
397+
// Verify that the RouteData contains the correct component type
398+
var routeViewFrame = lastBatch.ReferenceFrames.Skip(1).First();
399+
Assert.Equal(RenderTreeFrameType.Attribute, routeViewFrame.FrameType);
400+
var routeData = (RouteData)routeViewFrame.AttributeValue;
401+
Assert.Equal(typeof(JanComponent), routeData.PageType);
402+
}
403+
404+
[Fact]
405+
public async Task OnNotFound_WithBothNotFoundPageAndArgsPath_PreferArgs()
406+
{
407+
// Create a new router instance for this test to control Attach() timing
408+
var services = new ServiceCollection();
409+
var testNavManager = new TestNavigationManager();
410+
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
411+
services.AddSingleton<NavigationManager>(testNavManager);
412+
services.AddSingleton<INavigationInterception, TestNavigationInterception>();
413+
services.AddSingleton<IScrollToLocationHash, TestScrollToLocationHash>();
414+
var serviceProvider = services.BuildServiceProvider();
415+
416+
var testRenderer = new TestRenderer(serviceProvider);
417+
testRenderer.ShouldHandleExceptions = true;
418+
var testRouter = (Router)testRenderer.InstantiateComponent<Router>();
419+
testRouter.AppAssembly = Assembly.GetExecutingAssembly();
420+
testRouter.Found = routeData => (builder) => builder.AddContent(0, $"Rendering route matching {routeData.PageType}");
421+
422+
var parameters = new Dictionary<string, object>
423+
{
424+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
425+
{ nameof(Router.NotFoundPage), typeof(NotFoundTestComponent) }
426+
};
427+
428+
// Subscribe to OnNotFound event BEFORE router attaches and sets up its own subscription
429+
testNavManager.OnNotFound += (sender, args) =>
430+
{
431+
args.Path = "/jan"; // This should take precedence over NotFoundPage
432+
};
433+
434+
// Now assign the root component ID which will call Attach()
435+
testRenderer.AssignRootComponentId(testRouter);
436+
437+
await testRenderer.Dispatcher.InvokeAsync(() =>
438+
testRouter.SetParametersAsync(ParameterView.FromDictionary(parameters)));
439+
440+
// trigger the NavigationManager's OnNotFound event
441+
await testRenderer.Dispatcher.InvokeAsync(() => testNavManager.TriggerNotFound());
442+
443+
// The Router should have rendered using RenderComponentByRoute (args.Path) instead of NotFoundPage
444+
var lastBatch = testRenderer.Batches.Last();
445+
var renderedFrame = lastBatch.ReferenceFrames.First();
446+
Assert.Equal(RenderTreeFrameType.Component, renderedFrame.FrameType);
447+
Assert.Equal(typeof(RouteView), renderedFrame.ComponentType);
448+
449+
// Verify that the RouteData contains the JanComponent (from args.Path), not NotFoundTestComponent
450+
var routeViewFrame = lastBatch.ReferenceFrames.Skip(1).First();
451+
Assert.Equal(RenderTreeFrameType.Attribute, routeViewFrame.FrameType);
452+
var routeData = (RouteData)routeViewFrame.AttributeValue;
453+
Assert.Equal(typeof(JanComponent), routeData.PageType);
454+
}
455+
456+
[Fact]
457+
public async Task FindComponentTypeByRoute_WithValidRoute_ReturnsComponentType()
458+
{
459+
var parameters = new Dictionary<string, object>
460+
{
461+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly }
462+
};
463+
464+
await _renderer.Dispatcher.InvokeAsync(() =>
465+
_router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
466+
467+
var result = _router.FindComponentTypeByRoute("/jan");
468+
Assert.Equal(typeof(JanComponent), result);
469+
}
470+
471+
[Fact]
472+
public async Task RenderComponentByRoute_WithInvalidRoute_ThrowsException()
473+
{
474+
var parameters = new Dictionary<string, object>
475+
{
476+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly }
477+
};
478+
479+
await _renderer.Dispatcher.InvokeAsync(() =>
480+
_router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
481+
482+
var builder = new RenderTreeBuilder();
483+
484+
var exception = Assert.Throws<InvalidOperationException>(() =>
485+
{
486+
_router.RenderComponentByRoute(builder, "/nonexistent-route");
487+
});
488+
Assert.Contains("No component found for route '/nonexistent-route'", exception.Message);
489+
}
490+
304491
internal class TestNavigationManager : NavigationManager
305492
{
306493
public TestNavigationManager() =>
@@ -311,6 +498,11 @@ public void NotifyLocationChanged(string uri, bool intercepted, string state = n
311498
Uri = uri;
312499
NotifyLocationChanged(intercepted);
313500
}
501+
502+
public void TriggerNotFound()
503+
{
504+
base.NotFound();
505+
}
314506
}
315507

316508
internal sealed class TestNavigationInterception : INavigationInterception

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ internal async Task InitializeStandardComponentServicesAsync(
8484
IFormCollection? form = null)
8585
{
8686
var navigationManager = httpContext.RequestServices.GetRequiredService<NavigationManager>();
87-
((IHostEnvironmentNavigationManager)navigationManager)?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request), OnNavigateTo);
87+
((IHostEnvironmentNavigationManager)navigationManager)?.Initialize(
88+
GetContextBaseUri(httpContext.Request),
89+
GetFullUri(httpContext.Request),
90+
uri => GetErrorHandledTask(OnNavigateTo(uri)));
8891

8992
navigationManager?.OnNotFound += (sender, args) => NotFoundEventArgs = args;
9093

0 commit comments

Comments
 (0)