Skip to content

Commit bb41532

Browse files
authored
Remove some Linq usages from Routing (#47004)
* Remove some Linq usages from Routing These Linq usages account for roughly 150KB of the 770KB of NativeAOT app size attributed to Routing code. The largest usages are the ones where we are using ValueTypes (like ValueTuple and KeyValuePair) over internal types. The size savings helps even if the app is using Linq because the generic instantiation will never be brought back into the app.
1 parent b07d086 commit bb41532

File tree

7 files changed

+216
-76
lines changed

7 files changed

+216
-76
lines changed

src/Http/Routing/src/EndpointNameAddressScheme.cs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
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.Linq;
54
using System.Text;
65
using Microsoft.AspNetCore.Http;
76

@@ -52,11 +51,17 @@ private static Dictionary<string, Endpoint[]> Initialize(IReadOnlyList<Endpoint>
5251
entries[endpointName] = new[] { endpoint };
5352
continue;
5453
}
55-
56-
// Ok this is a duplicate, because we have two endpoints with the same name. Bail out, because we
57-
// are just going to throw, we don't need to finish collecting data.
58-
hasDuplicates = true;
59-
break;
54+
else
55+
{
56+
// Ok this is a duplicate, because we have two endpoints with the same name. Collect all the data
57+
// so we can throw an exception. The extra allocations here don't matter since this is an exceptional case.
58+
hasDuplicates = true;
59+
60+
var newEntry = new Endpoint[existing.Length + 1];
61+
Array.Copy(existing, newEntry, existing.Length);
62+
newEntry[existing.Length] = endpoint;
63+
entries[endpointName] = newEntry;
64+
}
6065
}
6166

6267
if (!hasDuplicates)
@@ -66,21 +71,20 @@ private static Dictionary<string, Endpoint[]> Initialize(IReadOnlyList<Endpoint>
6671
}
6772

6873
// OK we need to report some duplicates.
69-
var duplicates = endpoints
70-
.GroupBy(GetEndpointName)
71-
.Where(g => g.Key != null && g.Count() > 1);
72-
7374
var builder = new StringBuilder();
7475
builder.AppendLine(Resources.DuplicateEndpointNameHeader);
7576

76-
foreach (var group in duplicates)
77+
foreach (var group in entries)
7778
{
78-
builder.AppendLine();
79-
builder.AppendLine(Resources.FormatDuplicateEndpointNameEntry(group.Key));
80-
81-
foreach (var endpoint in group)
79+
if (group.Key is not null && group.Value.Length > 1)
8280
{
83-
builder.AppendLine(endpoint.DisplayName);
81+
builder.AppendLine();
82+
builder.AppendLine(Resources.FormatDuplicateEndpointNameEntry(group.Key));
83+
84+
foreach (var endpoint in group.Value)
85+
{
86+
builder.AppendLine(endpoint.DisplayName);
87+
}
8488
}
8589
}
8690

src/Http/Routing/src/Matching/AcceptsMatcherPolicy.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,14 @@ public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
236236
edges.Add(string.Empty, anyEndpoints.ToList());
237237
}
238238

239-
return edges
240-
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
241-
.ToArray();
239+
var result = new PolicyNodeEdge[edges.Count];
240+
var index = 0;
241+
foreach (var kvp in edges)
242+
{
243+
result[index] = new PolicyNodeEdge(kvp.Key, kvp.Value);
244+
index++;
245+
}
246+
return result;
242247
}
243248

244249
private static Endpoint CreateRejectionEndpoint()
@@ -258,10 +263,13 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJ
258263

259264
// Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they
260265
// are then then execute them in linear order.
261-
var ordered = edges
262-
.Select(e => (mediaType: CreateEdgeMediaType(ref e), destination: e.Destination))
263-
.OrderBy(e => GetScore(e.mediaType))
264-
.ToArray();
266+
var ordered = new (ReadOnlyMediaTypeHeaderValue mediaType, int destination)[edges.Count];
267+
for (var i = 0; i < edges.Count; i++)
268+
{
269+
var e = edges[i];
270+
ordered[i] = (mediaType: CreateEdgeMediaType(ref e), destination: e.Destination);
271+
}
272+
Array.Sort(ordered, static (left, right) => GetScore(left.mediaType).CompareTo(GetScore(right.mediaType)));
265273

266274
// If any edge matches all content types, then treat that as the 'exit'. This will
267275
// always happen because we insert a 415 endpoint.

src/Http/Routing/src/Matching/HostMatcherPolicy.cs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@ public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
199199
for (var i = 0; i < endpoints.Count; i++)
200200
{
201201
var endpoint = endpoints[i];
202-
var hosts = endpoint.Metadata.GetMetadata<IHostMetadata>()?.Hosts.Select(CreateEdgeKey).ToArray();
203-
if (hosts == null || hosts.Length == 0)
202+
var hosts = GetEdgeKeys(endpoint);
203+
if (hosts is null || hosts.Length == 0)
204204
{
205205
hosts = new[] { EdgeKey.WildcardEdgeKey };
206206
}
@@ -221,8 +221,8 @@ public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
221221
{
222222
var endpoint = endpoints[i];
223223

224-
var endpointKeys = endpoint.Metadata.GetMetadata<IHostMetadata>()?.Hosts.Select(CreateEdgeKey).ToArray() ?? Array.Empty<EdgeKey>();
225-
if (endpointKeys.Length == 0)
224+
var endpointKeys = GetEdgeKeys(endpoint);
225+
if (endpointKeys is null || endpointKeys.Length == 0)
226226
{
227227
// OK this means that this endpoint matches *all* hosts.
228228
// So, loop and add it to all states.
@@ -259,9 +259,28 @@ public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
259259
}
260260
}
261261

262-
return edges
263-
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
264-
.ToArray();
262+
var result = new PolicyNodeEdge[edges.Count];
263+
var index = 0;
264+
foreach (var kvp in edges)
265+
{
266+
result[index] = new PolicyNodeEdge(kvp.Key, kvp.Value);
267+
index++;
268+
}
269+
return result;
270+
}
271+
272+
private static EdgeKey[]? GetEdgeKeys(Endpoint endpoint)
273+
{
274+
List<EdgeKey>? result = null;
275+
var hostMetadata = endpoint.Metadata.GetMetadata<IHostMetadata>();
276+
if (hostMetadata is not null)
277+
{
278+
foreach (var host in hostMetadata.Hosts)
279+
{
280+
(result ??= new()).Add(CreateEdgeKey(host));
281+
}
282+
}
283+
return result?.ToArray();
265284
}
266285

267286
/// <inheritdoc />
@@ -271,10 +290,13 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJ
271290

272291
// Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they
273292
// are then then execute them in linear order.
274-
var ordered = edges
275-
.Select(e => (host: (EdgeKey)e.State, destination: e.Destination))
276-
.OrderBy(e => GetScore(e.host))
277-
.ToArray();
293+
var ordered = new (EdgeKey host, int destination)[edges.Count];
294+
for (var i = 0; i < edges.Count; i++)
295+
{
296+
PolicyJumpTableEdge e = edges[i];
297+
ordered[i] = (host: (EdgeKey)e.State, destination: e.Destination);
298+
}
299+
Array.Sort(ordered, static (left, right) => GetScore(left.host).CompareTo(GetScore(right.host)));
278300

279301
return new HostPolicyJumpTable(exitDestination, ordered);
280302
}

src/Http/Routing/src/ParameterPolicyActivator.cs

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
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.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Globalization;
6-
using System.Linq;
77
using System.Reflection;
88
using Microsoft.Extensions.DependencyInjection;
99

@@ -93,8 +93,8 @@ private static bool ResolveParameterPolicyTypeAndArgument(
9393
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "We ensure the constructor is preserved when the constraint map is added.")]
9494
private static IParameterPolicy CreateParameterPolicy(IServiceProvider? serviceProvider, Type parameterPolicyType, string? argumentString)
9595
{
96-
ConstructorInfo? activationConstructor = null;
97-
object?[]? parameters = null;
96+
ConstructorInfo? activationConstructor;
97+
object?[]? parameters;
9898
var constructors = parameterPolicyType.GetConstructors();
9999

100100
// If there is only one constructor and it has a single parameter, pass the argument string directly
@@ -113,30 +113,24 @@ private static IParameterPolicy CreateParameterPolicy(IServiceProvider? serviceP
113113
// arguments that can be resolved from DI
114114
//
115115
// For example, ctor(string, IService) will beat ctor(string)
116-
var matchingConstructors = constructors
117-
.Where(ci => GetNonConvertableParameterTypeCount(serviceProvider, ci.GetParameters()) == arguments.Length)
118-
.OrderByDescending(ci => ci.GetParameters().Length)
119-
.ToArray();
116+
var matchingConstructors = GetMatchingConstructors(constructors, serviceProvider, arguments.Length);
120117

121-
if (matchingConstructors.Length == 0)
118+
if (matchingConstructors.Count == 0)
122119
{
123120
throw new RouteCreationException(
124121
Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor(
125122
parameterPolicyType.Name, arguments.Length));
126123
}
127124
else
128125
{
129-
// When there are multiple matching constructors, choose the one with the most service arguments
130-
if (matchingConstructors.Length == 1
131-
|| matchingConstructors[0].GetParameters().Length > matchingConstructors[1].GetParameters().Length)
126+
if (matchingConstructors.Count == 1)
132127
{
133128
activationConstructor = matchingConstructors[0];
134129
}
135130
else
136131
{
137-
throw new RouteCreationException(
138-
Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors(
139-
parameterPolicyType.Name, matchingConstructors[0].GetParameters().Length));
132+
// When there are multiple matching constructors, choose the one with the most service arguments
133+
activationConstructor = GetLongestConstructor(matchingConstructors, parameterPolicyType);
140134
}
141135

142136
parameters = ConvertArguments(serviceProvider, activationConstructor.GetParameters(), arguments);
@@ -146,6 +140,52 @@ private static IParameterPolicy CreateParameterPolicy(IServiceProvider? serviceP
146140
return (IParameterPolicy)activationConstructor.Invoke(parameters);
147141
}
148142

143+
private static List<ConstructorInfo> GetMatchingConstructors(ConstructorInfo[] constructors, IServiceProvider? serviceProvider, int argumentsLength)
144+
{
145+
var result = new List<ConstructorInfo>();
146+
foreach (var constructor in constructors)
147+
{
148+
if (GetNonConvertableParameterTypeCount(serviceProvider, constructor.GetParameters()) == argumentsLength)
149+
{
150+
result.Add(constructor);
151+
}
152+
}
153+
return result;
154+
}
155+
156+
private static ConstructorInfo GetLongestConstructor(List<ConstructorInfo> constructors, Type parameterPolicyType)
157+
{
158+
Debug.Assert(constructors.Count > 0);
159+
160+
var longestLength = -1;
161+
ConstructorInfo? longest = null;
162+
var multipleBestLengthFound = false;
163+
164+
foreach (var constructor in constructors)
165+
{
166+
var length = constructor.GetParameters().Length;
167+
if (length > longestLength)
168+
{
169+
multipleBestLengthFound = false;
170+
longestLength = length;
171+
longest = constructor;
172+
}
173+
else if (longestLength == length)
174+
{
175+
multipleBestLengthFound = true;
176+
}
177+
}
178+
179+
if (multipleBestLengthFound)
180+
{
181+
throw new RouteCreationException(
182+
Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors(
183+
parameterPolicyType.Name, longestLength));
184+
}
185+
186+
return longest!;
187+
}
188+
149189
private static int GetNonConvertableParameterTypeCount(IServiceProvider? serviceProvider, ParameterInfo[] parameters)
150190
{
151191
if (serviceProvider == null)

src/Http/Routing/src/Template/RouteTemplate.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ public RouteTemplate(RoutePattern other)
2828
// RequiredValues will be ignored. RouteTemplate doesn't support them.
2929

3030
TemplateText = other.RawText;
31-
Segments = new List<TemplateSegment>(other.PathSegments.Select(p => new TemplateSegment(p)));
31+
32+
Segments = new List<TemplateSegment>(other.PathSegments.Count);
33+
foreach (var p in other.PathSegments)
34+
{
35+
Segments.Add(new TemplateSegment(p));
36+
}
37+
3238
Parameters = new List<TemplatePart>();
3339
for (var i = 0; i < Segments.Count; i++)
3440
{

src/Http/Routing/src/Template/TemplateBinder.cs

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,7 @@ internal TemplateBinder(
8787
}
8888
_filters = filters.ToArray();
8989

90-
_constraints = parameterPolicies
91-
?.Where(p => p.policy is IRouteConstraint)
92-
.Select(p => (p.parameterName, (IRouteConstraint)p.policy))
93-
.ToArray() ?? Array.Empty<(string, IRouteConstraint)>();
94-
_parameterTransformers = parameterPolicies
95-
?.Where(p => p.policy is IOutboundParameterTransformer)
96-
.Select(p => (p.parameterName, (IOutboundParameterTransformer)p.policy))
97-
.ToArray() ?? Array.Empty<(string, IOutboundParameterTransformer)>();
90+
Initialize(parameterPolicies, out _constraints, out _parameterTransformers);
9891

9992
_slots = AssignSlots(_pattern, _filters);
10093
}
@@ -126,18 +119,38 @@ internal TemplateBinder(
126119
}
127120
_filters = filters.ToArray();
128121

129-
_constraints = parameterPolicies
130-
?.Where(p => p.policy is IRouteConstraint)
131-
.Select(p => (p.parameterName, (IRouteConstraint)p.policy))
132-
.ToArray() ?? Array.Empty<(string, IRouteConstraint)>();
133-
_parameterTransformers = parameterPolicies
134-
?.Where(p => p.policy is IOutboundParameterTransformer)
135-
.Select(p => (p.parameterName, (IOutboundParameterTransformer)p.policy))
136-
.ToArray() ?? Array.Empty<(string, IOutboundParameterTransformer)>();
122+
Initialize(parameterPolicies, out _constraints, out _parameterTransformers);
137123

138124
_slots = AssignSlots(_pattern, _filters);
139125
}
140126

127+
private static void Initialize(
128+
IEnumerable<(string parameterName, IParameterPolicy policy)>? parameterPolicies,
129+
out (string parameterName, IRouteConstraint constraint)[] constraints,
130+
out (string parameterName, IOutboundParameterTransformer transformer)[] parameterTransformers)
131+
{
132+
List<(string parameterName, IRouteConstraint constraint)>? constraintList = null;
133+
List<(string parameterName, IOutboundParameterTransformer transformer)>? parameterTransformerList = null;
134+
135+
if (parameterPolicies is not null)
136+
{
137+
foreach (var p in parameterPolicies)
138+
{
139+
if (p.policy is IRouteConstraint routeConstraint)
140+
{
141+
(constraintList ??= new()).Add((p.parameterName, routeConstraint));
142+
}
143+
if (p.policy is IOutboundParameterTransformer transformer)
144+
{
145+
(parameterTransformerList ??= new()).Add((p.parameterName, transformer));
146+
}
147+
}
148+
}
149+
150+
constraints = constraintList?.ToArray() ?? Array.Empty<(string, IRouteConstraint)>();
151+
parameterTransformers = parameterTransformerList?.ToArray() ?? Array.Empty<(string, IOutboundParameterTransformer)>();
152+
}
153+
141154
/// <summary>
142155
/// Generates the parameter values in the route.
143156
/// </summary>

0 commit comments

Comments
 (0)