Skip to content

Commit f1cfa8e

Browse files
authored
[Blazor] Unified server-side rendering and interactive routing (#49622)
* Unifies the routing implementation between ASP.NET Core routing and the Blazor interactive router. * Adds support for: * Complex segments. * Default values. * All the built-in constraints in ASP.NET Core. * Enables the interactive router to reuse the results from the ASP.NET Core router. * Updates the results of the ASP.NET Core router to match the specific Blazor interactive router quirks. * Adding `null` for all the unused parameters in a given route group for a given handler. * Adding `null` for all optional parameters that were not provided. * Converting some constraint values to their "native" type. * For example, a parameter with an `int` constraint gets converted to an integer from the parsed string. * Includes an extensibility point to support third-party routers via dynamic routing. Meaningful behavior changes are: * Optional parameters won't throw if there are non-optional segments after them, the optionality will just be ignored. * Matching constrained catch-alls won't match on a per segment basis. This behavior was different in Blazor routing and is really incompatible with the ASP.NET behavior, so we rather standardize on the ASP.NET behavior. * To provide a concrete example, if you had `/{*catchall:int}` and the path /1/2/3/4/5, it would match on Blazor, but not on ASP.NET. Implementation notes: * We use TreeRouter which is the older implementation of the routing algorithm before we had the DFA, as it's a simpler and equally compatible/conformant implementation. * We use `RoutePattern` directly instead of `RouteTemplate` as that's the most modern approach `(RouteTemplate is legacy from the ASP.NET times)` * We don't share the resources dictionary from routing to avoid adding extra resources that are not used.
1 parent 771532a commit f1cfa8e

File tree

97 files changed

+2280
-889
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

+2280
-889
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<Project>
2+
3+
<PropertyGroup>
4+
<RoutingSourceRoot>$(RepoRoot)\src\Http\Routing\src\</RoutingSourceRoot>
5+
<RoutingAbstractionsSourceRoot>$(RepoRoot)\src\Http\Routing.Abstractions\src\</RoutingAbstractionsSourceRoot>
6+
<HttpAbstractionsSourceRoot>$(RepoRoot)\src\Http\Http.Abstractions\src\</HttpAbstractionsSourceRoot>
7+
</PropertyGroup>
8+
9+
<ItemGroup Label="Routing">
10+
<!-- Abstractions -->
11+
<Compile Include="$(HttpAbstractionsSourceRoot)Routing\RouteValueDictionary.cs" LinkBase="Routing" />
12+
<Compile Include="$(RoutingAbstractionsSourceRoot)IRouteConstraint.cs" LinkBase="Routing" />
13+
<Compile Include="$(RoutingAbstractionsSourceRoot)IParameterPolicy.cs" LinkBase="Routing" />
14+
<!-- Infrastructure -->
15+
<Compile Include="$(RoutingSourceRoot)RouteValueEqualityComparer.cs" LinkBase="Routing" />
16+
<Compile Include="$(RoutingSourceRoot)RouteCreationException.cs" LinkBase="Routing" />
17+
<!-- Tree router for matching -->
18+
<Compile Include="$(RoutingSourceRoot)PathTokenizer.cs" LinkBase="Routing" />
19+
<Compile Include="$(RoutingSourceRoot)Tree\*.cs" LinkBase="Routing\Tree" />
20+
<Compile Remove="$(RoutingSourceRoot)Tree\Outbound*.cs" LinkBase="Routing\Tree" />
21+
<Compile Remove="$(RoutingSourceRoot)Tree\LinkGeneration*.cs" LinkBase="Routing\Tree" />
22+
<!-- Route patterns -->
23+
<Compile Include="$(RoutingSourceRoot)Template\RoutePrecedence.cs" LinkBase="Routing\Patterns" />
24+
<Compile Include="$(RoutingSourceRoot)Patterns\*.cs" LinkBase="Routing\Patterns" />
25+
<Compile Remove="$(RoutingSourceRoot)Patterns\*RoutePatternTransformer.cs" />
26+
<!-- Route constraints -->
27+
<Compile Include="$(RoutingSourceRoot)DefaultInlineConstraintResolver.cs" LinkBase="Routing" />
28+
<Compile Include="$(RoutingSourceRoot)ParameterPolicyActivator.cs" LinkBase="Routing" />
29+
<Compile Include="$(RoutingSourceRoot)RouteConstraintBuilder.cs" LinkBase="Routing" />
30+
<Compile Include="$(RoutingSourceRoot)RouteConstraintMatcher.cs" LinkBase="Routing" />
31+
<Compile Include="$(RoutingSourceRoot)RouteOptions.cs" LinkBase="Routing" />
32+
<Compile Include="$(RoutingSourceRoot)IInlineConstraintResolver.cs" LinkBase="Routing" />
33+
<Compile Include="$(RoutingSourceRoot)Constraints\**\*.cs" LinkBase="Routing\Constraints" />
34+
35+
<Compile Remove="$(RoutingSourceRoot)Constraints\HttpMethodRouteConstraint.cs" />
36+
<Compile Remove="$(RoutingSourceRoot)Constraints\RegexErrorStubRouteConstraint.cs" />
37+
<Compile Remove="$(RoutingSourceRoot)Constraints\RequiredRouteConstraint.cs" />
38+
<Compile Remove="$(RoutingSourceRoot)Constraints\StringRouteConstraint.cs" />
39+
</ItemGroup>
40+
</Project>

src/Components/Components/src/Microsoft.AspNetCore.Components.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<Nullable>enable</Nullable>
99
<IsTrimmable>true</IsTrimmable>
1010
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
11+
<DefineConstants>$(DefineConstants);COMPONENTS</DefineConstants>
1112
<!-- TODO: Address Native AOT analyzer warnings https://github.com/dotnet/aspnetcore/issues/45473 -->
1213
<EnableAOTAnalyzer>false</EnableAOTAnalyzer>
1314
</PropertyGroup>
@@ -19,6 +20,8 @@
1920
<Compile Include="$(SharedSourceRoot)QueryStringEnumerable.cs" LinkBase="Shared" />
2021
</ItemGroup>
2122

23+
<Import Project="Microsoft.AspNetCore.Components.Routing.targets" />
24+
2225
<ItemGroup>
2326
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
2427
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />

src/Components/Components/src/NavigationManager.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,32 @@ public string ToBaseRelativePath(string uri)
240240
throw new ArgumentException(message);
241241
}
242242

243+
internal ReadOnlySpan<char> ToBaseRelativePath(ReadOnlySpan<char> uri)
244+
{
245+
if (MemoryExtensions.StartsWith(uri, _baseUri!.OriginalString.AsSpan(), StringComparison.Ordinal))
246+
{
247+
// The absolute URI must be of the form "{baseUri}something" (where
248+
// baseUri ends with a slash), and from that we return "something"
249+
return uri[_baseUri.OriginalString.Length..];
250+
}
251+
252+
var pathEndIndex = uri.IndexOfAny('#', '?');
253+
var uriPathOnly = pathEndIndex < 0 ? uri : uri[..pathEndIndex];
254+
if (_baseUri.OriginalString.EndsWith('/') && MemoryExtensions.Equals(uriPathOnly, _baseUri.OriginalString.AsSpan(0, _baseUri.OriginalString.Length - 1), StringComparison.Ordinal))
255+
{
256+
// Special case: for the base URI "/something/", if you're at
257+
// "/something" then treat it as if you were at "/something/" (i.e.,
258+
// with the trailing slash). It's a bit ambiguous because we don't know
259+
// whether the server would return the same page whether or not the
260+
// slash is present, but ASP.NET Core at least does by default when
261+
// using PathBase.
262+
return uri[(_baseUri.OriginalString.Length - 1)..];
263+
}
264+
265+
var message = $"The URI '{uri}' is not contained by the base URI '{_baseUri}'.";
266+
throw new ArgumentException(message);
267+
}
268+
243269
internal static string NormalizeBaseUri(string baseUri)
244270
{
245271
var lastSlashIndex = baseUri.LastIndexOf('/');

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
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
namespace Microsoft.AspNetCore.Components.Routing;
5+
6+
internal readonly struct PathString(string? value)
7+
{
8+
public string? Value { get; } = value;
9+
10+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<root>
3+
<!--
4+
Microsoft ResX Schema
5+
6+
Version 2.0
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
11+
associated with the data types.
12+
13+
Example:
14+
15+
... ado.net/XML headers & schema ...
16+
<resheader name="resmimetype">text/microsoft-resx</resheader>
17+
<resheader name="version">2.0</resheader>
18+
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
19+
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
20+
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
21+
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
22+
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
23+
<value>[base64 mime encoded serialized .NET Framework object]</value>
24+
</data>
25+
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
26+
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
27+
<comment>This is a comment</comment>
28+
</data>
29+
30+
There are any number of "resheader" rows that contain simple
31+
name/value pairs.
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
37+
mimetype set.
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
41+
extensible. For a given mimetype the value must be set accordingly:
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
45+
read any of the formats listed below.
46+
47+
mimetype: application/x-microsoft.net.object.binary.base64
48+
value : The object must be serialized with
49+
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
50+
: and then encoded with base64 encoding.
51+
52+
mimetype: application/x-microsoft.net.object.soap.base64
53+
value : The object must be serialized with
54+
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
55+
: and then encoded with base64 encoding.
56+
57+
mimetype: application/x-microsoft.net.object.bytearray.base64
58+
value : The object must be serialized into a byte array
59+
: using a System.ComponentModel.TypeConverter
60+
: and then encoded with base64 encoding.
61+
-->
62+
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
63+
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
64+
<xsd:element name="root" msdata:IsDataSet="true">
65+
<xsd:complexType>
66+
<xsd:choice maxOccurs="unbounded">
67+
<xsd:element name="metadata">
68+
<xsd:complexType>
69+
<xsd:sequence>
70+
<xsd:element name="value" type="xsd:string" minOccurs="0" />
71+
</xsd:sequence>
72+
<xsd:attribute name="name" use="required" type="xsd:string" />
73+
<xsd:attribute name="type" type="xsd:string" />
74+
<xsd:attribute name="mimetype" type="xsd:string" />
75+
<xsd:attribute ref="xml:space" />
76+
</xsd:complexType>
77+
</xsd:element>
78+
<xsd:element name="assembly">
79+
<xsd:complexType>
80+
<xsd:attribute name="alias" type="xsd:string" />
81+
<xsd:attribute name="name" type="xsd:string" />
82+
</xsd:complexType>
83+
</xsd:element>
84+
<xsd:element name="data">
85+
<xsd:complexType>
86+
<xsd:sequence>
87+
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
88+
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
89+
</xsd:sequence>
90+
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
91+
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
92+
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
93+
<xsd:attribute ref="xml:space" />
94+
</xsd:complexType>
95+
</xsd:element>
96+
<xsd:element name="resheader">
97+
<xsd:complexType>
98+
<xsd:sequence>
99+
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
100+
</xsd:sequence>
101+
<xsd:attribute name="name" type="xsd:string" use="required" />
102+
</xsd:complexType>
103+
</xsd:element>
104+
</xsd:choice>
105+
</xsd:complexType>
106+
</xsd:element>
107+
</xsd:schema>
108+
<resheader name="resmimetype">
109+
<value>text/microsoft-resx</value>
110+
</resheader>
111+
<resheader name="version">
112+
<value>2.0</value>
113+
</resheader>
114+
<resheader name="reader">
115+
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
116+
</resheader>
117+
<resheader name="writer">
118+
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119+
</resheader>
120+
<data name="RangeConstraint_MinShouldBeLessThanOrEqualToMax" xml:space="preserve">
121+
<value>The value for argument '{0}' should be less than or equal to the value for the argument '{1}'.</value>
122+
</data>
123+
<data name="DefaultInlineConstraintResolver_AmbiguousCtors" xml:space="preserve">
124+
<value>The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.</value>
125+
</data>
126+
<data name="DefaultInlineConstraintResolver_CouldNotFindCtor" xml:space="preserve">
127+
<value>Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.</value>
128+
</data>
129+
<data name="DefaultInlineConstraintResolver_TypeNotConstraint" xml:space="preserve">
130+
<value>The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.</value>
131+
</data>
132+
<data name="RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
133+
<value>The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.</value>
134+
</data>
135+
<data name="RouteConstraintBuilder_CouldNotResolveConstraint" xml:space="preserve">
136+
<value>The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.</value>
137+
</data>
138+
<data name="RoutePatternBuilder_CollectionCannotBeEmpty" xml:space="preserve">
139+
<value>The collection cannot be empty.</value>
140+
</data>
141+
<data name="ConstraintMustBeStringOrConstraint" xml:space="preserve">
142+
<value>The constraint entry '{0}' - '{1}' must have a string value or be of a type which implements '{2}'.</value>
143+
</data>
144+
<data name="RoutePattern_InvalidConstraintReference" xml:space="preserve">
145+
<value>Invalid constraint '{0}'. A constraint must be of type 'string' or '{1}'.</value>
146+
</data>
147+
<data name="RoutePattern_InvalidParameterConstraintReference" xml:space="preserve">
148+
<value>Invalid constraint '{0}' for parameter '{1}'. A constraint must be of type 'string', '{2}', or '{3}'.</value>
149+
</data>
150+
<data name="RoutePattern_ConstraintReferenceNotFound" xml:space="preserve">
151+
<value>The constraint reference '{0}' could not be resolved to a type. Register the constraint type with '{1}.{2}'.</value>
152+
</data>
153+
<data name="RoutePattern_InvalidStringConstraintReference" xml:space="preserve">
154+
<value>Invalid constraint type '{0}' registered as '{1}'. A constraint type must either implement '{2}', or inherit from '{3}'.</value>
155+
</data>
156+
<data name="RegexRouteContraint_NotConfigured" xml:space="preserve">
157+
<value>A route parameter uses the regex constraint, which isn't registered. If this application was configured using CreateSlimBuilder(...) or AddRoutingCore(...) then this constraint is not registered by default. To use the regex constraint, configure route options at app startup: services.Configure&lt;RouteOptions&gt;(options =&gt; options.SetParameterPolicy&lt;RegexInlineRouteConstraint&gt;("regex"));</value>
158+
</data>
159+
<data name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve">
160+
<value>Value must be greater than or equal to {0}.</value>
161+
</data>
162+
<data name="Argument_NullOrEmpty" xml:space="preserve">
163+
<value>Value cannot be null or empty.</value>
164+
</data>
165+
<data name="TemplateRoute_CannotHaveCatchAllInMultiSegment" xml:space="preserve">
166+
<value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value>
167+
</data>
168+
<data name="TemplateRoute_CannotHaveConsecutiveParameters" xml:space="preserve">
169+
<value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value>
170+
</data>
171+
<data name="TemplateRoute_CannotHaveConsecutiveSeparators" xml:space="preserve">
172+
<value>The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.</value>
173+
</data>
174+
<data name="TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly" xml:space="preserve">
175+
<value>The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.</value>
176+
</data>
177+
<data name="TemplateRoute_CatchAllCannotBeOptional" xml:space="preserve">
178+
<value>A catch-all parameter cannot be marked optional.</value>
179+
</data>
180+
<data name="TemplateRoute_CatchAllMustBeLast" xml:space="preserve">
181+
<value>A catch-all parameter can only appear as the last segment of the route template.</value>
182+
</data>
183+
<data name="TemplateRoute_Exception" xml:space="preserve">
184+
<value>An error occurred while creating the route with name '{0}' and template '{1}'.</value>
185+
</data>
186+
<data name="TemplateRoute_InvalidLiteral" xml:space="preserve">
187+
<value>The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.</value>
188+
</data>
189+
<data name="TemplateRoute_InvalidParameterName" xml:space="preserve">
190+
<value>The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter.</value>
191+
</data>
192+
<data name="TemplateRoute_InvalidRouteTemplate" xml:space="preserve">
193+
<value>The route template cannot start with a '~' character unless followed by a '/'.</value>
194+
</data>
195+
<data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
196+
<value>There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.</value>
197+
</data>
198+
<data name="TemplateRoute_OptionalCannotHaveDefaultValue" xml:space="preserve">
199+
<value>An optional parameter cannot have default value.</value>
200+
</data>
201+
<data name="TemplateRoute_OptionalParameterCanbBePrecededByPeriod" xml:space="preserve">
202+
<value>In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.</value>
203+
</data>
204+
<data name="TemplateRoute_OptionalParameterHasTobeTheLast" xml:space="preserve">
205+
<value>An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.</value>
206+
</data>
207+
<data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
208+
<value>The route parameter name '{0}' appears more than one time in the route template.</value>
209+
</data>
210+
<data name="TemplateRoute_UnescapedBrace" xml:space="preserve">
211+
<value>In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.</value>
212+
</data>
213+
</root>

src/Components/Components/src/Routing/RouteConstraint.cs

Lines changed: 0 additions & 36 deletions
This file was deleted.

0 commit comments

Comments
 (0)