From b8f009ea9f7859023e83cffb5fde7c8a5de02b73 Mon Sep 17 00:00:00 2001 From: MohamedHasan3644 <102596726+MohamedHasan3644@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:45:54 +0530 Subject: [PATCH 1/7] 1032820: Implemented type safe navigation in Blazor --- .../src/NavigationManagerExtensions.cs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index f653de889aa8..0e27b3b8ff54 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using System.Collections.Concurrent; using System.Globalization; using System.Linq; +using System.Reflection; using System.Text; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Internal; @@ -38,6 +40,9 @@ public static class NavigationManagerExtensions [typeof(long)] = value => Format((long)value), }; + // Cache for component route templates to avoid repeated reflection + private static readonly ConcurrentDictionary _componentRouteTemplates = new(); + private static string? Format(string? value) => value; @@ -832,4 +837,89 @@ public static string GetUriWithHash(this NavigationManager navigationManager, st hashValue.AsSpan().CopyTo(chars[position..]); }); } + + /// + /// Gets the URI for the specified component type with optional route parameters. + /// + /// The component type to navigate to. + /// The . + /// The route parameters to include in the URI. + /// The URI for the specified component. + /// Thrown when the component type does not have a route attribute. + public static string GetUri(this NavigationManager navigationManager, params object[] parameters) + { + ArgumentNullException.ThrowIfNull(navigationManager); + + var componentType = typeof(TComponent); + var routeTemplates = GetComponentRouteTemplates(componentType); + + if (routeTemplates.Length == 0) + { + throw new InvalidOperationException($"Component '{componentType.FullName}' does not have a Route attribute."); + } + + // For now, use the first route template (future enhancement: allow specifying which route to use) + var routeTemplate = routeTemplates[0]; + return BindParametersToRouteTemplate(routeTemplate, parameters); + } + + /// + /// Navigates to the specified component type with optional route parameters. + /// + /// The component type to navigate to. + /// The . + /// The route parameters to include in the URI. + /// Thrown when the component type does not have a route attribute. + public static void NavigateTo(this NavigationManager navigationManager, params object[] parameters) + { + ArgumentNullException.ThrowIfNull(navigationManager); + + var uri = navigationManager.GetUri(parameters); + navigationManager.NavigateTo(uri); + } + + /// + /// Gets the route templates for the specified component type. + /// + /// The component type. + /// An array of route templates. + private static string[] GetComponentRouteTemplates(Type componentType) + { + return _componentRouteTemplates.GetOrAdd(componentType, type => + { + var routeAttributes = type.GetCustomAttributes(inherit: false); + if (routeAttributes.Length == 0) + { + return Array.Empty(); + } + + var templates = new string[routeAttributes.Length]; + for (var i = 0; i < routeAttributes.Length; i++) + { + templates[i] = routeAttributes[i].Template; + } + + return templates; + }); + } + + /// + /// Binds parameters to a route template. + /// + /// The route template. + /// The parameters to bind. + /// The route template with parameters bound. + private static string BindParametersToRouteTemplate(string routeTemplate, object[] parameters) + { + var result = routeTemplate; + + // Replace parameter placeholders with actual values + for (var i = 0; i < parameters.Length; i++) + { + var parameterValue = parameters[i]?.ToString() ?? string.Empty; + result = result.Replace($"{{{i}}}", parameterValue); + } + + return result; + } } From 583e95ea980d5731a50b6bdedc28af8687653b0d Mon Sep 17 00:00:00 2001 From: MohamedHasan3644 <102596726+MohamedHasan3644@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:41:03 +0530 Subject: [PATCH 2/7] 1032820: Resolved build errors --- .../Components/src/NavigationManagerExtensions.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 0e27b3b8ff54..00b68ca64fa2 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -888,15 +888,16 @@ private static string[] GetComponentRouteTemplates(Type componentType) return _componentRouteTemplates.GetOrAdd(componentType, type => { var routeAttributes = type.GetCustomAttributes(inherit: false); - if (routeAttributes.Length == 0) + var routeAttributesArray = routeAttributes as RouteAttribute[] ?? routeAttributes.ToArray(); + if (routeAttributesArray.Length == 0) { return Array.Empty(); } - var templates = new string[routeAttributes.Length]; - for (var i = 0; i < routeAttributes.Length; i++) + var templates = new string[routeAttributesArray.Length]; + for (var i = 0; i < routeAttributesArray.Length; i++) { - templates[i] = routeAttributes[i].Template; + templates[i] = routeAttributesArray[i].Template; } return templates; From cb28b6501c5c5ca7e9638b675a5e412f392434c3 Mon Sep 17 00:00:00 2001 From: MohamedHasan3644 <102596726+MohamedHasan3644@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:21:56 +0530 Subject: [PATCH 3/7] 1032820: Fixed exceptions --- .../src/NavigationManagerExtensions.cs | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 00b68ca64fa2..371c6dbe78ae 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -912,15 +912,51 @@ private static string[] GetComponentRouteTemplates(Type componentType) /// The route template with parameters bound. private static string BindParametersToRouteTemplate(string routeTemplate, object[] parameters) { - var result = routeTemplate; + if (parameters == null || parameters.Length == 0) + { + return routeTemplate; + } - // Replace parameter placeholders with actual values - for (var i = 0; i < parameters.Length; i++) + var sb = new StringBuilder(capacity: routeTemplate.Length + parameters.Length * 8); + var pos = 0; + var paramIndex = 0; + + while (pos < routeTemplate.Length) { - var parameterValue = parameters[i]?.ToString() ?? string.Empty; - result = result.Replace($"{{{i}}}", parameterValue); + var open = routeTemplate.IndexOf('{', pos); + if (open < 0) + { + // No more placeholders + sb.Append(routeTemplate.AsSpan(pos)); + break; + } + + // Append literal text before the placeholder + sb.Append(routeTemplate.AsSpan(pos, open - pos)); + + var close = routeTemplate.IndexOf('}', open + 1); + if (close < 0) + { + // Malformed template; append rest and break + sb.Append(routeTemplate.AsSpan(open)); + break; + } + + // If no more parameter values provided, leave placeholder as-is + if (paramIndex >= parameters.Length) + { + sb.Append(routeTemplate.AsSpan(open, close - open + 1)); + } + else + { + var parameterValue = parameters[paramIndex]?.ToString() ?? string.Empty; + sb.Append(parameterValue); + paramIndex++; + } + + pos = close + 1; } - return result; + return sb.ToString(); } } From dcaa459a5e24cfc04730058ca8cab88b0da020aa Mon Sep 17 00:00:00 2001 From: MohamedHasan3644 <102596726+MohamedHasan3644@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:11:25 +0530 Subject: [PATCH 4/7] 1032820: Moved implementations to Navigation Manager --- .../Components/src/NavigationManager.cs | 127 ++++++++++++++++- .../src/NavigationManagerExtensions.cs | 128 +----------------- 2 files changed, 127 insertions(+), 128 deletions(-) diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 260c38496c15..bd0148871f6c 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -2,7 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; using Microsoft.AspNetCore.Components.Routing; namespace Microsoft.AspNetCore.Components; @@ -206,14 +210,14 @@ internal string ResolveRelativeToCurrentPath(string relativeUri) var queryOrFragmentIndex = currentUri.IndexOfAny('?', '#'); var pathOnlyLength = queryOrFragmentIndex >= 0 ? queryOrFragmentIndex : currentUri.Length; var lastSlashIndex = currentUri[..pathOnlyLength].LastIndexOf('/'); - + if (lastSlashIndex < 0) { // No slash found - this shouldn't happen for valid absolute URIs // In this edge case, just append to the current URI return string.Concat(_uri, relativeUri); } - + // Keep everything up to and including the last slash, then append the relative URI var basePathLength = lastSlashIndex + 1; return string.Concat(currentUri[..basePathLength], relativeUri.AsSpan()); @@ -640,6 +644,125 @@ private static bool TryGetLengthOfBaseUriPrefix(Uri baseUri, string uri, out int return false; } + // Cache for component route templates to avoid repeated reflection + private static readonly ConcurrentDictionary _componentRouteTemplates = new(); + + /// + /// Gets the URI for the specified component type with optional route parameters. + /// + /// The component type to navigate to. + /// The route parameters to include in the URI. + /// The URI for the specified component. + /// Thrown when the component type does not have a route attribute. + public string GetUri(params object[] parameters) + { + var componentType = typeof(TComponent); + var routeTemplates = GetComponentRouteTemplates(componentType); + + if (routeTemplates.Length == 0) + { + throw new InvalidOperationException($"Component '{componentType.FullName}' does not have a Route attribute."); + } + + // For now, use the first route template (future enhancement: allow specifying which route to use) + var routeTemplate = routeTemplates[0]; + return BindParametersToRouteTemplate(routeTemplate, parameters); + } + + /// + /// Navigates to the specified component type with optional route parameters. + /// + /// The component type to navigate to. + /// The route parameters to include in the URI. + /// Thrown when the component type does not have a route attribute. + public void NavigateTo(params object[] parameters) + { + var uri = GetUri(parameters); + NavigateTo(uri); + } + + /// + /// Gets the route templates for the specified component type. + /// + /// The component type. + /// An array of route templates. + private static string[] GetComponentRouteTemplates(Type componentType) + { + return _componentRouteTemplates.GetOrAdd(componentType, type => + { + var routeAttributes = type.GetCustomAttributes(inherit: false); + var routeAttributesArray = routeAttributes as RouteAttribute[] ?? routeAttributes.ToArray(); + if (routeAttributesArray.Length == 0) + { + return Array.Empty(); + } + + var templates = new string[routeAttributesArray.Length]; + for (var i = 0; i < routeAttributesArray.Length; i++) + { + templates[i] = routeAttributesArray[i].Template; + } + + return templates; + }); + } + + /// + /// Binds parameters to a route template. + /// + /// The route template. + /// The parameters to bind. + /// The route template with parameters bound. + private static string BindParametersToRouteTemplate(string routeTemplate, object[] parameters) + { + if (parameters == null || parameters.Length == 0) + { + return routeTemplate; + } + + var sb = new StringBuilder(capacity: routeTemplate.Length + parameters.Length * 8); + var pos = 0; + var paramIndex = 0; + + while (pos < routeTemplate.Length) + { + var open = routeTemplate.IndexOf('{', pos); + if (open < 0) + { + // No more placeholders + sb.Append(routeTemplate.AsSpan(pos)); + break; + } + + // Append literal text before the placeholder + sb.Append(routeTemplate.AsSpan(pos, open - pos)); + + var close = routeTemplate.IndexOf('}', open + 1); + if (close < 0) + { + // Malformed template; append rest and break + sb.Append(routeTemplate.AsSpan(open)); + break; + } + + // If no more parameter values provided, leave placeholder as-is + if (paramIndex >= parameters.Length) + { + sb.Append(routeTemplate.AsSpan(open, close - open + 1)); + } + else + { + var parameterValue = parameters[paramIndex]?.ToString() ?? string.Empty; + sb.Append(parameterValue); + paramIndex++; + } + + pos = close + 1; + } + + return sb.ToString(); + } + private static void Validate(Uri? baseUri, string uri) { if (baseUri == null || uri == null) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 371c6dbe78ae..02ba40b30282 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; -using System.Collections.Concurrent; using System.Globalization; using System.Linq; -using System.Reflection; using System.Text; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Internal; @@ -40,8 +38,7 @@ public static class NavigationManagerExtensions [typeof(long)] = value => Format((long)value), }; - // Cache for component route templates to avoid repeated reflection - private static readonly ConcurrentDictionary _componentRouteTemplates = new(); + private static string? Format(string? value) => value; @@ -837,126 +834,5 @@ public static string GetUriWithHash(this NavigationManager navigationManager, st hashValue.AsSpan().CopyTo(chars[position..]); }); } - - /// - /// Gets the URI for the specified component type with optional route parameters. - /// - /// The component type to navigate to. - /// The . - /// The route parameters to include in the URI. - /// The URI for the specified component. - /// Thrown when the component type does not have a route attribute. - public static string GetUri(this NavigationManager navigationManager, params object[] parameters) - { - ArgumentNullException.ThrowIfNull(navigationManager); - - var componentType = typeof(TComponent); - var routeTemplates = GetComponentRouteTemplates(componentType); - - if (routeTemplates.Length == 0) - { - throw new InvalidOperationException($"Component '{componentType.FullName}' does not have a Route attribute."); - } - - // For now, use the first route template (future enhancement: allow specifying which route to use) - var routeTemplate = routeTemplates[0]; - return BindParametersToRouteTemplate(routeTemplate, parameters); - } - - /// - /// Navigates to the specified component type with optional route parameters. - /// - /// The component type to navigate to. - /// The . - /// The route parameters to include in the URI. - /// Thrown when the component type does not have a route attribute. - public static void NavigateTo(this NavigationManager navigationManager, params object[] parameters) - { - ArgumentNullException.ThrowIfNull(navigationManager); - - var uri = navigationManager.GetUri(parameters); - navigationManager.NavigateTo(uri); - } - - /// - /// Gets the route templates for the specified component type. - /// - /// The component type. - /// An array of route templates. - private static string[] GetComponentRouteTemplates(Type componentType) - { - return _componentRouteTemplates.GetOrAdd(componentType, type => - { - var routeAttributes = type.GetCustomAttributes(inherit: false); - var routeAttributesArray = routeAttributes as RouteAttribute[] ?? routeAttributes.ToArray(); - if (routeAttributesArray.Length == 0) - { - return Array.Empty(); - } - - var templates = new string[routeAttributesArray.Length]; - for (var i = 0; i < routeAttributesArray.Length; i++) - { - templates[i] = routeAttributesArray[i].Template; - } - - return templates; - }); - } - - /// - /// Binds parameters to a route template. - /// - /// The route template. - /// The parameters to bind. - /// The route template with parameters bound. - private static string BindParametersToRouteTemplate(string routeTemplate, object[] parameters) - { - if (parameters == null || parameters.Length == 0) - { - return routeTemplate; - } - - var sb = new StringBuilder(capacity: routeTemplate.Length + parameters.Length * 8); - var pos = 0; - var paramIndex = 0; - - while (pos < routeTemplate.Length) - { - var open = routeTemplate.IndexOf('{', pos); - if (open < 0) - { - // No more placeholders - sb.Append(routeTemplate.AsSpan(pos)); - break; - } - - // Append literal text before the placeholder - sb.Append(routeTemplate.AsSpan(pos, open - pos)); - - var close = routeTemplate.IndexOf('}', open + 1); - if (close < 0) - { - // Malformed template; append rest and break - sb.Append(routeTemplate.AsSpan(open)); - break; - } - - // If no more parameter values provided, leave placeholder as-is - if (paramIndex >= parameters.Length) - { - sb.Append(routeTemplate.AsSpan(open, close - open + 1)); - } - else - { - var parameterValue = parameters[paramIndex]?.ToString() ?? string.Empty; - sb.Append(parameterValue); - paramIndex++; - } - - pos = close + 1; - } - - return sb.ToString(); - } + } From 1bdbf1dc6f6eb5d0e62c6dbcc151452088b2bf2c Mon Sep 17 00:00:00 2001 From: MohamedHasan3644 <102596726+MohamedHasan3644@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:14:30 +0530 Subject: [PATCH 5/7] 1032820: Removed unwanted spaces --- src/Components/Components/src/NavigationManagerExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Components/Components/src/NavigationManagerExtensions.cs b/src/Components/Components/src/NavigationManagerExtensions.cs index 02ba40b30282..f653de889aa8 100644 --- a/src/Components/Components/src/NavigationManagerExtensions.cs +++ b/src/Components/Components/src/NavigationManagerExtensions.cs @@ -38,8 +38,6 @@ public static class NavigationManagerExtensions [typeof(long)] = value => Format((long)value), }; - - private static string? Format(string? value) => value; @@ -834,5 +832,4 @@ public static string GetUriWithHash(this NavigationManager navigationManager, st hashValue.AsSpan().CopyTo(chars[position..]); }); } - } From 07faefdbdd0531ea5e54a42e3523e912004616eb Mon Sep 17 00:00:00 2001 From: MohamedHasan3644 <102596726+MohamedHasan3644@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:00:12 +0530 Subject: [PATCH 6/7] 1032820: Added Testcases for type safe navigation --- .../Components/test/NavigationManagerTest.cs | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/src/Components/Components/test/NavigationManagerTest.cs b/src/Components/Components/test/NavigationManagerTest.cs index 99c17847dec6..1fabc70fd61a 100644 --- a/src/Components/Components/test/NavigationManagerTest.cs +++ b/src/Components/Components/test/NavigationManagerTest.cs @@ -1109,6 +1109,98 @@ public void ResolveRelativeToCurrentPath_ThrowsForNullUri() testNavManager.ResolveRelativeToCurrentPath(null!)); } + // Type-safe navigation tests (valid when type-safe APIs from the PR are present) + [Fact] + public void GetUri_Type_NoParameters_ReturnsExpectedAbsoluteUri() + { + var nav = new TestNavigationManager("http://example.com/"); + + var uri = nav.GetUri(); + + Assert.Equal("/list", uri); + } + + [Fact] + public void GetUri_Type_SingleParameter_ReturnsExpectedAbsoluteUri() + { + var nav = new TestNavigationManager("http://example.com/"); + + var uri = nav.GetUri("123"); + + Assert.Equal("/user/123", uri); + } + + [Fact] + public void GetUri_Type_MultipleParameters_ReturnsExpectedAbsoluteUri() + { + var nav = new TestNavigationManager("http://example.com/"); + + var uri = nav.GetUri("p1", "books"); + + Assert.Equal("/product/p1/books", uri); + } + + [Fact] + public void NavigateTo_Generic_InvokesNavigateToCore_WithResolvedUri() + { + var nav = new TestNavigationManagerWithNavigationTracking("http://example.com/"); + + nav.NavigateTo(); + + Assert.Single(nav.Navigations); + Assert.Equal("/list", nav.Navigations[0].uri); + } + + [Fact] + public void NavigateTo_Generic_WithParameters_InvokesNavigateToCore_WithResolvedUri() + { + var nav = new TestNavigationManagerWithNavigationTracking("http://example.com/"); + + nav.NavigateTo("123"); + + Assert.Single(nav.Navigations); + Assert.Equal("/user/123", nav.Navigations[0].uri); + } + + [Fact] + public void GetUri_WithoutRoute_ThrowsInvalidOperationException() + { + var nav = new TestNavigationManager("http://example.com/"); + + var ex = Assert.Throws(() => nav.GetUri()); + + Assert.Contains("route", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + // Sample component types used for type-safe navigation tests + [Route("/list")] + private class ListSampleComponent : IComponent + { + public void Attach(RenderHandle renderHandle) { } + public Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask; + } + + [Route("/user/{userId}")] + private class UserSampleComponent : IComponent + { + public void Attach(RenderHandle renderHandle) { } + public Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask; + } + + [Route("/product/{productId}/{category}")] + private class ProductSampleComponent : IComponent + { + public void Attach(RenderHandle renderHandle) { } + public Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask; + } + + // Component with no route attribute to assert negative behavior + private class NoRouteComponent : IComponent + { + public void Attach(RenderHandle renderHandle) { } + public Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask; + } + private class TestNavigationManager : NavigationManager { public TestNavigationManager() @@ -1130,7 +1222,7 @@ public async Task RunNotifyLocationChangingAsync(string uri, string state, protected override void NavigateToCore(string uri, bool forceLoad) { - throw new System.NotImplementedException(); + base.NavigateToCore(uri, forceLoad); } protected override void SetNavigationLockState(bool value) From b915801d50a905a735e0eea1aaedd4a52cbe2faf Mon Sep 17 00:00:00 2001 From: MohamedHasan3644 <102596726+MohamedHasan3644@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:16:29 +0530 Subject: [PATCH 7/7] 1032820: Updated PublicAPI.Unshipped.txt file --- src/Components/Components/src/PublicAPI.Unshipped.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 1860c583402f..bcf854a9677d 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -5,6 +5,8 @@ Microsoft.AspNetCore.Components.CascadingParameterSubscription Microsoft.AspNetCore.Components.CascadingParameterSubscription.CascadingParameterSubscription() -> void Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.SetAttributeValue(int frameIndex, object? value) -> void static Microsoft.AspNetCore.Components.NavigationManagerExtensions.GetUriWithHash(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! hash) -> string! +Microsoft.AspNetCore.Components.NavigationManager.GetUri(params object![]! parameters) -> string! +Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(params object![]! parameters) -> void Microsoft.AspNetCore.Components.NavigationOptions.RelativeToCurrentUri.get -> bool Microsoft.AspNetCore.Components.NavigationOptions.RelativeToCurrentUri.init -> void Microsoft.AspNetCore.Components.IComponentPropertyActivator