Skip to content

Commit f98959f

Browse files
author
msftbot[bot]
authored
Merge pull request #27995 from dotnet-maestro-bot/merge/release/5.0-to-master
[automated] Merge branch 'release/5.0' => 'master'
2 parents eda62e7 + 43d3389 commit f98959f

File tree

52 files changed

+2777
-292
lines changed

Some content is hidden

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

52 files changed

+2777
-292
lines changed

.azure/pipelines/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,7 @@ stages:
850850
- CodeSign_Xplat_Linux_arm
851851
- CodeSign_Xplat_Linux_arm64
852852
- CodeSign_Xplat_Linux_musl_x64
853+
- CodeSign_Xplat_Linux_musl_arm
853854
- CodeSign_Xplat_Linux_musl_arm64
854855
# In addition to the dependencies above, ensure the build was successful overall.
855856
- Source_Build

src/Components/Components/src/PublicAPI.Shipped.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#nullable enable
1+
#nullable enable
22
Microsoft.AspNetCore.Components.BindConverter
33
Microsoft.AspNetCore.Components.BindElementAttribute
44
Microsoft.AspNetCore.Components.BindElementAttribute.BindElementAttribute(string! element, string? suffix, string! valueAttribute, string! changeAttribute) -> void
@@ -427,3 +427,5 @@ virtual Microsoft.AspNetCore.Components.RouteView.Render(Microsoft.AspNetCore.Co
427427
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.MarkupContent.get -> string
428428
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.TextContent.get -> string
429429
~override Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ToString() -> string
430+
Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool
431+
Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components.Routing
5+
{
6+
/// <summary>
7+
/// Provides an abstraction over <see cref="RouteTable"/> and <see cref="LegacyRouteMatching.LegacyRouteTable"/>.
8+
/// This is only an internal implementation detail of <see cref="Router"/> and can be removed once
9+
/// the legacy route matching logic is removed.
10+
/// </summary>
11+
internal interface IRouteTable
12+
{
13+
void Route(RouteContext routeContext);
14+
}
15+
}

src/Components/Components/src/Routing/OptionalTypeRouteConstraint.cs renamed to src/Components/Components/src/Routing/LegacyRouteMatching/LegacyOptionalTypeRouteConstraint.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
namespace Microsoft.AspNetCore.Components.Routing
4+
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
55
{
66
/// <summary>
77
/// A route constraint that allows the value to be null or parseable as the specified
88
/// type.
99
/// </summary>
1010
/// <typeparam name="T">The type to which the value must be parseable.</typeparam>
11-
internal class OptionalTypeRouteConstraint<T> : RouteConstraint
11+
internal class LegacyOptionalTypeRouteConstraint<T> : LegacyRouteConstraint
1212
{
13-
public delegate bool TryParseDelegate(string str, out T result);
13+
public delegate bool LegacyTryParseDelegate(string str, out T result);
1414

15-
private readonly TryParseDelegate _parser;
15+
private readonly LegacyTryParseDelegate _parser;
1616

17-
public OptionalTypeRouteConstraint(TryParseDelegate parser)
17+
public LegacyOptionalTypeRouteConstraint(LegacyTryParseDelegate parser)
1818
{
1919
_parser = parser;
2020
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Globalization;
7+
8+
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
9+
{
10+
internal abstract class LegacyRouteConstraint
11+
{
12+
// note: the things that prevent this cache from growing unbounded is that
13+
// we're the only caller to this code path, and the fact that there are only
14+
// 8 possible instances that we create.
15+
//
16+
// The values passed in here for parsing are always static text defined in route attributes.
17+
private static readonly ConcurrentDictionary<string, LegacyRouteConstraint> _cachedConstraints
18+
= new ConcurrentDictionary<string, LegacyRouteConstraint>();
19+
20+
public abstract bool Match(string pathSegment, out object? convertedValue);
21+
22+
public static LegacyRouteConstraint Parse(string template, string segment, string constraint)
23+
{
24+
if (string.IsNullOrEmpty(constraint))
25+
{
26+
throw new ArgumentException($"Malformed segment '{segment}' in route '{template}' contains an empty constraint.");
27+
}
28+
29+
if (_cachedConstraints.TryGetValue(constraint, out var cachedInstance))
30+
{
31+
return cachedInstance;
32+
}
33+
else
34+
{
35+
var newInstance = CreateRouteConstraint(constraint);
36+
if (newInstance != null)
37+
{
38+
// We've done to the work to create the constraint now, but it's possible
39+
// we're competing with another thread. GetOrAdd can ensure only a single
40+
// instance is returned so that any extra ones can be GC'ed.
41+
return _cachedConstraints.GetOrAdd(constraint, newInstance);
42+
}
43+
else
44+
{
45+
throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'.");
46+
}
47+
}
48+
}
49+
50+
/// <summary>
51+
/// Creates a structured RouteConstraint object given a string that contains
52+
/// the route constraint. A constraint is the place after the colon in a
53+
/// parameter definition, for example `{age:int?}`.
54+
///
55+
/// If the constraint denotes an optional, this method will return an
56+
/// <see cref="LegacyOptionalTypeRouteConstraint{T}" /> which handles the appropriate checks.
57+
/// </summary>
58+
/// <param name="constraint">String representation of the constraint</param>
59+
/// <returns>Type-specific RouteConstraint object</returns>
60+
private static LegacyRouteConstraint? CreateRouteConstraint(string constraint)
61+
{
62+
switch (constraint)
63+
{
64+
case "bool":
65+
return new LegacyTypeRouteConstraint<bool>(bool.TryParse);
66+
case "bool?":
67+
return new LegacyOptionalTypeRouteConstraint<bool>(bool.TryParse);
68+
case "datetime":
69+
return new LegacyTypeRouteConstraint<DateTime>((string str, out DateTime result)
70+
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
71+
case "datetime?":
72+
return new LegacyOptionalTypeRouteConstraint<DateTime>((string str, out DateTime result)
73+
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
74+
case "decimal":
75+
return new LegacyTypeRouteConstraint<decimal>((string str, out decimal result)
76+
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
77+
case "decimal?":
78+
return new LegacyOptionalTypeRouteConstraint<decimal>((string str, out decimal result)
79+
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
80+
case "double":
81+
return new LegacyTypeRouteConstraint<double>((string str, out double result)
82+
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
83+
case "double?":
84+
return new LegacyOptionalTypeRouteConstraint<double>((string str, out double result)
85+
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
86+
case "float":
87+
return new LegacyTypeRouteConstraint<float>((string str, out float result)
88+
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
89+
case "float?":
90+
return new LegacyOptionalTypeRouteConstraint<float>((string str, out float result)
91+
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
92+
case "guid":
93+
return new LegacyTypeRouteConstraint<Guid>(Guid.TryParse);
94+
case "guid?":
95+
return new LegacyOptionalTypeRouteConstraint<Guid>(Guid.TryParse);
96+
case "int":
97+
return new LegacyTypeRouteConstraint<int>((string str, out int result)
98+
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
99+
case "int?":
100+
return new LegacyOptionalTypeRouteConstraint<int>((string str, out int result)
101+
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
102+
case "long":
103+
return new LegacyTypeRouteConstraint<long>((string str, out long result)
104+
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
105+
case "long?":
106+
return new LegacyOptionalTypeRouteConstraint<long>((string str, out long result)
107+
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
108+
default:
109+
return null;
110+
}
111+
}
112+
}
113+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#nullable disable warnings
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Diagnostics;
9+
10+
// Avoid referencing the whole Microsoft.AspNetCore.Components.Routing namespace to
11+
// avoid the risk of accidentally relying on the non-legacy types in the legacy fork
12+
using RouteContext = Microsoft.AspNetCore.Components.Routing.RouteContext;
13+
14+
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
15+
{
16+
[DebuggerDisplay("Handler = {Handler}, Template = {Template}")]
17+
internal class LegacyRouteEntry
18+
{
19+
public LegacyRouteEntry(LegacyRouteTemplate template, Type handler, string[] unusedRouteParameterNames)
20+
{
21+
Template = template;
22+
UnusedRouteParameterNames = unusedRouteParameterNames;
23+
Handler = handler;
24+
}
25+
26+
public LegacyRouteTemplate Template { get; }
27+
28+
public string[] UnusedRouteParameterNames { get; }
29+
30+
public Type Handler { get; }
31+
32+
internal void Match(RouteContext context)
33+
{
34+
string? catchAllValue = null;
35+
36+
// If this template contains a catch-all parameter, we can concatenate the pathSegments
37+
// at and beyond the catch-all segment's position. For example:
38+
// Template: /foo/bar/{*catchAll}
39+
// PathSegments: /foo/bar/one/two/three
40+
if (Template.ContainsCatchAllSegment && context.Segments.Length >= Template.Segments.Length)
41+
{
42+
catchAllValue = string.Join('/', context.Segments[Range.StartAt(Template.Segments.Length - 1)]);
43+
}
44+
// If there are no optional segments on the route and the length of the route
45+
// and the template do not match, then there is no chance of this matching and
46+
// we can bail early.
47+
else if (Template.OptionalSegmentsCount == 0 && Template.Segments.Length != context.Segments.Length)
48+
{
49+
return;
50+
}
51+
52+
// Parameters will be lazily initialized.
53+
Dictionary<string, object> parameters = null;
54+
var numMatchingSegments = 0;
55+
for (var i = 0; i < Template.Segments.Length; i++)
56+
{
57+
var segment = Template.Segments[i];
58+
59+
if (segment.IsCatchAll)
60+
{
61+
numMatchingSegments += 1;
62+
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
63+
parameters[segment.Value] = catchAllValue;
64+
break;
65+
}
66+
67+
// If the template contains more segments than the path, then
68+
// we may need to break out of this for-loop. This can happen
69+
// in one of two cases:
70+
//
71+
// (1) If we are comparing a literal route with a literal template
72+
// and the route is shorter than the template.
73+
// (2) If we are comparing a template where the last value is an optional
74+
// parameter that the route does not provide.
75+
if (i >= context.Segments.Length)
76+
{
77+
// If we are under condition (1) above then we can stop evaluating
78+
// matches on the rest of this template.
79+
if (!segment.IsParameter && !segment.IsOptional)
80+
{
81+
break;
82+
}
83+
}
84+
85+
string pathSegment = null;
86+
if (i < context.Segments.Length)
87+
{
88+
pathSegment = context.Segments[i];
89+
}
90+
91+
if (!segment.Match(pathSegment, out var matchedParameterValue))
92+
{
93+
return;
94+
}
95+
else
96+
{
97+
numMatchingSegments++;
98+
if (segment.IsParameter)
99+
{
100+
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
101+
parameters[segment.Value] = matchedParameterValue;
102+
}
103+
}
104+
}
105+
106+
// In addition to extracting parameter values from the URL, each route entry
107+
// also knows which other parameters should be supplied with null values. These
108+
// are parameters supplied by other route entries matching the same handler.
109+
if (!Template.ContainsCatchAllSegment && UnusedRouteParameterNames.Length > 0)
110+
{
111+
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
112+
for (var i = 0; i < UnusedRouteParameterNames.Length; i++)
113+
{
114+
parameters[UnusedRouteParameterNames[i]] = null;
115+
}
116+
}
117+
118+
// We track the number of segments in the template that matched
119+
// against this particular route then only select the route that
120+
// matches the most number of segments on the route that was passed.
121+
// This check is an exactness check that favors the more precise of
122+
// two templates in the event that the following route table exists.
123+
// Route 1: /{anythingGoes}
124+
// Route 2: /users/{id:int}
125+
// And the provided route is `/users/1`. We want to choose Route 2
126+
// over Route 1.
127+
// Furthermore, literal routes are preferred over parameterized routes.
128+
// If the two routes below are registered in the route table.
129+
// Route 1: /users/1
130+
// Route 2: /users/{id:int}
131+
// And the provided route is `/users/1`. We want to choose Route 1 over
132+
// Route 2.
133+
var allRouteSegmentsMatch = numMatchingSegments >= context.Segments.Length;
134+
// Checking that all route segments have been matches does not suffice if we are
135+
// comparing literal templates with literal routes. For example, the template
136+
// `/this/is/a/template` and the route `/this/`. In that case, we want to ensure
137+
// that all non-optional segments have matched as well.
138+
var allNonOptionalSegmentsMatch = numMatchingSegments >= (Template.Segments.Length - Template.OptionalSegmentsCount);
139+
if (Template.ContainsCatchAllSegment || (allRouteSegmentsMatch && allNonOptionalSegmentsMatch))
140+
{
141+
context.Parameters = parameters;
142+
context.Handler = Handler;
143+
}
144+
}
145+
}
146+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
// Avoid referencing the whole Microsoft.AspNetCore.Components.Routing namespace to
5+
// avoid the risk of accidentally relying on the non-legacy types in the legacy fork
6+
using RouteContext = Microsoft.AspNetCore.Components.Routing.RouteContext;
7+
8+
namespace Microsoft.AspNetCore.Components.LegacyRouteMatching
9+
{
10+
internal class LegacyRouteTable : Routing.IRouteTable
11+
{
12+
public LegacyRouteTable(LegacyRouteEntry[] routes)
13+
{
14+
Routes = routes;
15+
}
16+
17+
public LegacyRouteEntry[] Routes { get; }
18+
19+
public void Route(RouteContext routeContext)
20+
{
21+
for (var i = 0; i < Routes.Length; i++)
22+
{
23+
Routes[i].Match(routeContext);
24+
if (routeContext.Handler != null)
25+
{
26+
return;
27+
}
28+
}
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)