Skip to content

Commit 582c7af

Browse files
committed
Make Router component consume the output from ASP.NET Core routing
1 parent 9aee608 commit 582c7af

File tree

11 files changed

+319
-121
lines changed

11 files changed

+319
-121
lines changed

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.ComponentRenderMo
4949
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.NamedEvent = 10 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
5050
Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary<string!, object?>! routeValues) -> void
5151
Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
52+
Microsoft.AspNetCore.Components.RouteData.Template.get -> string?
53+
Microsoft.AspNetCore.Components.RouteData.Template.set -> void
5254
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider
5355
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider.RouteData.get -> Microsoft.AspNetCore.Components.RouteData?
5456
Microsoft.AspNetCore.Components.RenderModeAttribute

src/Components/Components/src/Routing/RouteData.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,9 @@ public RouteData([DynamicallyAccessedMembers(Component)] Type pageType, IReadOnl
4040
/// Gets route parameter values extracted from the matched route.
4141
/// </summary>
4242
public IReadOnlyDictionary<string, object?> RouteValues { get; }
43+
44+
/// <summary>
45+
/// Gets the route template that was used to match the route if any.
46+
/// </summary>
47+
public string? Template { get; set; }
4348
}
Lines changed: 85 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
5+
using System.Diagnostics.CodeAnalysis;
46
using System.Globalization;
57
using Microsoft.AspNetCore.Routing.Tree;
68

@@ -9,70 +11,106 @@ namespace Microsoft.AspNetCore.Components.Routing;
911
internal sealed class RouteTable(TreeRouter treeRouter)
1012
{
1113
private readonly TreeRouter _router = treeRouter;
14+
private static readonly ConcurrentDictionary<(Type, string), InboundRouteEntry> _routeEntryCache = new();
1215

1316
public TreeRouter? TreeRouter => _router;
1417

18+
[UnconditionalSuppressMessage(
19+
"Trimming",
20+
"IL2077:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations.",
21+
Justification = "We don't trim the user assemblies and this code is only used on the server.")]
22+
internal static RouteData ProcessParameters(RouteData endpointRouteData)
23+
{
24+
if (endpointRouteData.Template != null)
25+
{
26+
var entry = _routeEntryCache.GetOrAdd(
27+
(endpointRouteData.PageType, endpointRouteData.Template),
28+
((Type page, string template) key) => RouteTableFactory.CreateEntry(key.page, key.template));
29+
30+
var routeValueDictionary = new RouteValueDictionary(endpointRouteData.RouteValues);
31+
ProcessParameters(entry, routeValueDictionary);
32+
return new RouteData(endpointRouteData.PageType, routeValueDictionary)
33+
{
34+
Template = endpointRouteData.Template
35+
};
36+
}
37+
else
38+
{
39+
return endpointRouteData;
40+
}
41+
}
42+
1543
public void Route(RouteContext routeContext)
1644
{
1745
_router.Route(routeContext);
1846
if (routeContext.Entry != null)
1947
{
20-
if (routeContext.Entry.UnusedRouteParameterNames != null)
48+
ProcessParameters(routeContext.Entry, routeContext.RouteValues);
49+
}
50+
51+
if (routeContext.RouteValues != null && routeContext.RouteValues.Count == 0)
52+
{
53+
routeContext.RouteValues = null!;
54+
}
55+
return;
56+
}
57+
58+
private static void ProcessParameters(InboundRouteEntry entry, RouteValueDictionary routeValues)
59+
{
60+
// Quirk number 1: Add null values for unused route parameters.
61+
if (entry.UnusedRouteParameterNames != null)
62+
{
63+
foreach (var parameter in entry.UnusedRouteParameterNames)
2164
{
22-
foreach (var parameter in routeContext.Entry.UnusedRouteParameterNames)
23-
{
24-
routeContext.RouteValues[parameter] = null;
25-
}
65+
routeValues[parameter] = null;
2666
}
67+
}
2768

28-
foreach (var parameter in routeContext.Entry.RoutePattern.Parameters)
69+
foreach (var parameter in entry.RoutePattern.Parameters)
70+
{
71+
// Quick number 2: Add null values for optional route parameters that weren't provided.
72+
if (!routeValues.TryGetValue(parameter.Name, out var parameterValue))
2973
{
30-
if (!routeContext.RouteValues.TryGetValue(parameter.Name, out var parameterValue))
31-
{
32-
routeContext.RouteValues.Add(parameter.Name, null);
33-
}
34-
else if (parameter.ParameterPolicies.Count > 0 && !parameter.IsCatchAll)
74+
routeValues.Add(parameter.Name, null);
75+
}
76+
else if (parameter.ParameterPolicies.Count > 0 && !parameter.IsCatchAll)
77+
{
78+
// Quirk number 3: If the parameter has some well-known set of route constraints, then we need to convert the value
79+
// to the target type.
80+
for (var i = 0; i < parameter.ParameterPolicies.Count; i++)
3581
{
36-
for (var i = 0; i < parameter.ParameterPolicies.Count; i++)
82+
var policy = parameter.ParameterPolicies[i];
83+
switch (policy.Content)
3784
{
38-
var policy = parameter.ParameterPolicies[i];
39-
switch (policy.Content)
40-
{
41-
case "bool":
42-
routeContext.RouteValues[parameter.Name] = bool.Parse((string)parameterValue!);
43-
break;
44-
case "datetime":
45-
routeContext.RouteValues[parameter.Name] = DateTime.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
46-
break;
47-
case "decimal":
48-
routeContext.RouteValues[parameter.Name] = decimal.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
49-
break;
50-
case "double":
51-
routeContext.RouteValues[parameter.Name] = double.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
52-
break;
53-
case "float":
54-
routeContext.RouteValues[parameter.Name] = float.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
55-
break;
56-
case "guid":
57-
routeContext.RouteValues[parameter.Name] = Guid.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
58-
break;
59-
case "int":
60-
routeContext.RouteValues[parameter.Name] = int.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
61-
break;
62-
case "long":
63-
routeContext.RouteValues[parameter.Name] = long.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
64-
break;
65-
default:
66-
continue;
67-
}
85+
case "bool":
86+
routeValues[parameter.Name] = bool.Parse((string)parameterValue!);
87+
break;
88+
case "datetime":
89+
routeValues[parameter.Name] = DateTime.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
90+
break;
91+
case "decimal":
92+
routeValues[parameter.Name] = decimal.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
93+
break;
94+
case "double":
95+
routeValues[parameter.Name] = double.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
96+
break;
97+
case "float":
98+
routeValues[parameter.Name] = float.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
99+
break;
100+
case "guid":
101+
routeValues[parameter.Name] = Guid.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
102+
break;
103+
case "int":
104+
routeValues[parameter.Name] = int.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
105+
break;
106+
case "long":
107+
routeValues[parameter.Name] = long.Parse((string)parameterValue!, CultureInfo.InvariantCulture);
108+
break;
109+
default:
110+
continue;
68111
}
69112
}
70113
}
71114
}
72-
if (routeContext.RouteValues != null && routeContext.RouteValues.Count == 0)
73-
{
74-
routeContext.RouteValues = null!;
75-
}
76-
return;
77115
}
78116
}

src/Components/Components/src/Routing/RouteTableFactory.cs

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Microsoft.Extensions.DependencyInjection;
1313
using Microsoft.Extensions.Logging;
1414
using Microsoft.Extensions.Options;
15+
using static Microsoft.AspNetCore.Internal.LinkerFlags;
1516

1617
namespace Microsoft.AspNetCore.Components;
1718

@@ -86,19 +87,26 @@ internal static RouteTable Create(List<Type> componentTypes, IServiceProvider se
8687
//
8788
// RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an
8889
// ambiguity. You end up with two components (base class and derived class) with the same route.
89-
var routeAttributes = componentType.GetCustomAttributes(typeof(RouteAttribute), inherit: false);
90-
var templates = new string[routeAttributes.Length];
91-
for (var i = 0; i < routeAttributes.Length; i++)
92-
{
93-
var attribute = (RouteAttribute)routeAttributes[i];
94-
templates[i] = attribute.Template;
95-
}
90+
var templates = GetTemplates(componentType);
9691

9792
templatesByHandler.Add(componentType, templates);
9893
}
9994
return Create(templatesByHandler, serviceProvider);
10095
}
10196

97+
private static string[] GetTemplates(Type componentType)
98+
{
99+
var routeAttributes = componentType.GetCustomAttributes(typeof(RouteAttribute), inherit: false);
100+
var templates = new string[routeAttributes.Length];
101+
for (var i = 0; i < routeAttributes.Length; i++)
102+
{
103+
var attribute = (RouteAttribute)routeAttributes[i];
104+
templates[i] = attribute.Template;
105+
}
106+
107+
return templates;
108+
}
109+
102110
[UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Application code does not get trimmed, and the framework does not define routable components.")]
103111
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler, IServiceProvider serviceProvider)
104112
{
@@ -108,19 +116,10 @@ internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler,
108116

109117
foreach (var (type, templates) in templatesByHandler)
110118
{
111-
var allRouteParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
112-
var parsedTemplates = new (RoutePattern, HashSet<string>)[templates.Length];
113-
for (var i = 0; i < templates.Length; i++)
114-
{
115-
var parsedTemplate = RoutePatternParser.Parse(templates[i]);
116-
var parameterNames = GetParameterNames(parsedTemplate);
117-
parsedTemplates[i] = (parsedTemplate, parameterNames);
119+
var result = ComputeTemplateGroupInfo(templates);
118120

119-
foreach (var parameterName in parameterNames)
120-
{
121-
allRouteParameterNames.Add(parameterName);
122-
}
123-
}
121+
var parsedTemplates = result.ParsedTemplates;
122+
var allRouteParameterNames = result.AllRouteParameterNames;
124123

125124
foreach (var (parsedTemplate, routeParameterNames) in parsedTemplates)
126125
{
@@ -131,10 +130,64 @@ internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler,
131130

132131
DetectAmbiguousRoutes(builder);
133132

134-
//builder.InboundEntries.Sort(RouteOrder);
135133
return new RouteTable(builder.Build());
136134
}
137135

136+
private static TemplateGroupInfo ComputeTemplateGroupInfo(string[] templates)
137+
{
138+
var result = new TemplateGroupInfo(templates);
139+
for (var i = 0; i < templates.Length; i++)
140+
{
141+
var parsedTemplate = RoutePatternParser.Parse(templates[i]);
142+
var parameterNames = GetParameterNames(parsedTemplate);
143+
result.ParsedTemplates[i] = (parsedTemplate, parameterNames);
144+
145+
foreach (var parameterName in parameterNames)
146+
{
147+
result.AllRouteParameterNames.Add(parameterName);
148+
}
149+
}
150+
151+
return result;
152+
}
153+
154+
private struct TemplateGroupInfo(string[] templates)
155+
{
156+
public HashSet<string> AllRouteParameterNames { get; set; } = new(StringComparer.OrdinalIgnoreCase);
157+
public (RoutePattern, HashSet<string>)[] ParsedTemplates { get; set; } = new (RoutePattern, HashSet<string>)[templates.Length];
158+
}
159+
160+
internal static InboundRouteEntry CreateEntry([DynamicallyAccessedMembers(Component)] Type pageType, string template)
161+
{
162+
var templates = GetTemplates(pageType);
163+
var result = ComputeTemplateGroupInfo(templates);
164+
165+
RoutePattern? parsedTemplate = null;
166+
HashSet<string>? routeParameterNames = null;
167+
for (var i = 0; i < result.ParsedTemplates.Length; i++)
168+
{
169+
var (parsed, parameters) = result.ParsedTemplates[i];
170+
if (string.Equals(parsed.RawText, template, StringComparison.OrdinalIgnoreCase))
171+
{
172+
parsedTemplate = parsed;
173+
routeParameterNames = parameters;
174+
break;
175+
}
176+
}
177+
178+
if (parsedTemplate != null || routeParameterNames != null)
179+
{
180+
throw new InvalidOperationException($"Unable to find the provided template '{template}'");
181+
}
182+
183+
return new InboundRouteEntry()
184+
{
185+
Handler = pageType,
186+
RoutePattern = parsedTemplate,
187+
UnusedRouteParameterNames = GetUnusedParameterNames(result.AllRouteParameterNames, routeParameterNames!),
188+
};
189+
}
190+
138191
private static void DetectAmbiguousRoutes(TreeRouteBuilder builder)
139192
{
140193
for (var i = 0; i < builder.InboundEntries.Count; i++)

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,16 @@ internal virtual void Refresh(bool isNavigationIntercepted)
204204
// In order to avoid routing twice we check for RouteData
205205
if (RoutingStateProvider?.RouteData is { } endpointRouteData)
206206
{
207+
// Other routers shouldn't provide RouteData, this is specific to our router component
208+
// and must abide by our syntax and behaviors.
209+
// Other routers must create their own abstractions to flow data from their SSR routing
210+
// scheme to their interactive router.
207211
Log.NavigatingToComponent(_logger, endpointRouteData.PageType, locationPath, _baseUri);
208-
212+
// Post process the entry to add Blazor specific behaviors:
213+
// - Add 'null' for unused route parameters.
214+
// - Convert constrained parameters with (int, double, etc) to the target type.
215+
endpointRouteData = RouteTable.ProcessParameters(endpointRouteData);
209216
_renderHandle.Render(Found(endpointRouteData));
210-
211217
return;
212218
}
213219

@@ -229,6 +235,7 @@ internal virtual void Refresh(bool isNavigationIntercepted)
229235
var routeData = new RouteData(
230236
context.Handler,
231237
context.Parameters ?? _emptyParametersDictionary);
238+
232239
_renderHandle.Render(Found(routeData));
233240

234241
// If you navigate to a different path, then after the next render we'll update the scroll position

src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.AspNetCore.Http;
88
using Microsoft.AspNetCore.Routing;
99
using Microsoft.AspNetCore.Routing.Patterns;
10+
using Microsoft.Extensions.DependencyInjection;
1011

1112
namespace Microsoft.AspNetCore.Components.Endpoints;
1213

@@ -62,7 +63,11 @@ internal void AddEndpoints(
6263
// The display name is for debug purposes by endpoint routing.
6364
builder.DisplayName = $"{builder.RoutePattern.RawText} ({pageDefinition.DisplayName})";
6465

65-
builder.RequestDelegate = httpContext => new RazorComponentEndpointInvoker(httpContext, rootComponent, pageDefinition.Type).RenderComponent();
66+
builder.RequestDelegate = httpContext =>
67+
{
68+
var invoker = httpContext.RequestServices.GetRequiredService<IRazorComponentEndpointInvoker>();
69+
return invoker.Render(httpContext);
70+
};
6671

6772
endpoints.Add(builder.Build());
6873
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components.Routing;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.Components.Endpoints;
8+
9+
/// <summary>
10+
/// Invokes a Razor component endpoint to render the given root component and populates the
11+
/// <see cref="IRoutingStateProvider"/> with the given metadata (if any) to render a given
12+
/// page.
13+
/// </summary>
14+
/// <remarks>
15+
/// Razor component endpoints provide the root component via the <see cref="RootComponentMetadata"/>
16+
/// metadata in the endpoint.
17+
/// The page component is provided via the <see cref="ComponentTypeMetadata"/>.
18+
/// </remarks>
19+
public interface IRazorComponentEndpointInvoker
20+
{
21+
/// <summary>
22+
/// Invokes the Razor component endpoint.
23+
/// </summary>
24+
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
25+
/// <returns>A <see cref="Task"/> that completes when the endpoint has been invoked and the component
26+
/// has been rendered into the response.</returns>
27+
Task Render(HttpContext context);
28+
}

src/Components/Endpoints/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Microsoft.AspNetCore.Components.Endpoints.IComponentPrerenderer
4646
Microsoft.AspNetCore.Components.Endpoints.IComponentPrerenderer.Dispatcher.get -> Microsoft.AspNetCore.Components.Dispatcher!
4747
Microsoft.AspNetCore.Components.Endpoints.IComponentPrerenderer.PrerenderComponentAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Type! componentType, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode, Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Html.IHtmlAsyncContent!>
4848
Microsoft.AspNetCore.Components.Endpoints.IComponentPrerenderer.PrerenderPersistedStateAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.AspNetCore.Components.PersistedStateSerializationMode serializationMode) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Html.IHtmlContent!>
49+
Microsoft.AspNetCore.Components.Endpoints.IRazorComponentEndpointInvoker
50+
Microsoft.AspNetCore.Components.Endpoints.IRazorComponentEndpointInvoker.Render(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task!
4951
Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder
5052
Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
5153
Microsoft.AspNetCore.Components.Endpoints.RazorComponentDataSourceOptions

0 commit comments

Comments
 (0)