Skip to content

Commit c21b67c

Browse files
authored
Add initial parameter support to RequestDelegateGenerator (#46407)
1 parent e3d17f5 commit c21b67c

17 files changed

+923
-326
lines changed

src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,16 @@ internal static class DiagnosticDescriptors
2424
DiagnosticSeverity.Warning,
2525
isEnabledByDefault: true
2626
);
27+
28+
// This is temporary. The plan is to be able to resolve all parameters to a known EndpointParameterSource.
29+
public static DiagnosticDescriptor GetUnableToResolveParameterDescriptor(string parameterName)
30+
{
31+
return new(
32+
"RDG073",
33+
new LocalizableResourceString(nameof(Resources.UnableToResolveParameter_Title), Resources.ResourceManager, typeof(Resources)),
34+
new LocalizableResourceString(nameof(Resources.FormatUnableToResolveParameter_Message), Resources.ResourceManager, typeof(Resources), parameterName),
35+
"Usage",
36+
DiagnosticSeverity.Hidden,
37+
isEnabledByDefault: true);
38+
}
2739
}

src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5353
{
5454
context.ReportDiagnostic(Diagnostic.Create(diagnostic, endpoint.Operation.Syntax.GetLocation(), filePath));
5555
}
56-
foreach (var diagnostic in endpoint.Response.Diagnostics)
57-
{
58-
context.ReportDiagnostic(Diagnostic.Create(diagnostic, endpoint.Operation.Syntax.GetLocation(), filePath));
59-
}
60-
foreach (var diagnostic in endpoint.Route.Diagnostics)
61-
{
62-
context.ReportDiagnostic(Diagnostic.Create(diagnostic, endpoint.Operation.Syntax.GetLocation(), filePath));
63-
}
6456
});
6557

6658
var endpoints = endpointsWithDiagnostics
67-
.Where(endpoint => endpoint.Diagnostics.Count == 0 &&
68-
endpoint.Response.Diagnostics.Count == 0 &&
69-
endpoint.Route.Diagnostics.Count == 0)
59+
.Where(endpoint => endpoint.Diagnostics.Count == 0)
7060
.WithTrackingName(GeneratorSteps.EndpointsWithoutDiagnosicsStep);
7161

7262
var thunks = endpoints.Select((endpoint, _) => $$"""
@@ -97,7 +87,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
9787
}
9888
9989
{{endpoint.EmitRequestHandler()}}
100-
{{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}}
90+
{{endpoint.EmitFilteredRequestHandler()}}
10191
10292
RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered;
10393
var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection<object>.Empty;

src/Http/Http.Extensions/gen/Resources.resx

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
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
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
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
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
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
3737
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
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
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
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -129,4 +129,10 @@
129129
<data name="UnableToResolveMethod_Message" xml:space="preserve">
130130
<value>Unable to statically resolve endpoint handler method. Only method groups, lambda expressions or readonly fields/variables are allowed. Compile-time endpoint generation will skip this endpoint.</value>
131131
</data>
132-
</root>
132+
<data name="UnableToResolveParameter_Message" xml:space="preserve">
133+
<value>Unable to statically resolve parameter '{0}' for endpoint. Compile-time endpoint generation will skip this endpoint.</value>
134+
</data>
135+
<data name="UnableToResolveParameter_Title" xml:space="preserve">
136+
<value>Unable to resolve parameter</value>
137+
</data>
138+
</root>
Lines changed: 97 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +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.
3+
34
using System;
45
using System.Collections.Generic;
56
using System.Linq;
@@ -12,61 +13,118 @@ namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
1213

1314
internal class Endpoint
1415
{
15-
public string HttpMethod { get; }
16-
public EndpointRoute Route { get; }
17-
public EndpointResponse Response { get; }
18-
public List<DiagnosticDescriptor> Diagnostics { get; } = new List<DiagnosticDescriptor>();
19-
public (string, int) Location { get; }
20-
public IInvocationOperation Operation { get; }
21-
22-
private WellKnownTypes WellKnownTypes { get; }
16+
private string? _argumentListCache;
2317

2418
public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
2519
{
2620
Operation = operation;
27-
WellKnownTypes = wellKnownTypes;
28-
Location = GetLocation();
29-
HttpMethod = GetHttpMethod();
30-
Response = new EndpointResponse(Operation, wellKnownTypes);
31-
Route = new EndpointRoute(Operation);
32-
}
21+
Location = GetLocation(operation);
22+
HttpMethod = GetHttpMethod(operation);
3323

34-
private (string, int) GetLocation()
35-
{
36-
var filePath = Operation.Syntax.SyntaxTree.FilePath;
37-
var span = Operation.Syntax.SyntaxTree.GetLineSpan(Operation.Syntax.Span);
38-
var lineNumber = span.EndLinePosition.Line + 1;
39-
return (filePath, lineNumber);
40-
}
24+
if (!operation.TryGetRouteHandlerPattern(out var routeToken))
25+
{
26+
Diagnostics.Add(DiagnosticDescriptors.UnableToResolveRoutePattern);
27+
return;
28+
}
4129

42-
private string GetHttpMethod()
43-
{
44-
var syntax = (InvocationExpressionSyntax)Operation.Syntax;
45-
var expression = (MemberAccessExpressionSyntax)syntax.Expression;
46-
var name = (IdentifierNameSyntax)expression.Name;
47-
var identifier = name.Identifier;
48-
return identifier.ValueText;
30+
RoutePattern = routeToken.ValueText;
31+
32+
if (!operation.TryGetRouteHandlerMethod(out var method))
33+
{
34+
Diagnostics.Add(DiagnosticDescriptors.UnableToResolveMethod);
35+
return;
36+
}
37+
38+
Response = new EndpointResponse(method, wellKnownTypes);
39+
40+
if (method.Parameters.Length == 0)
41+
{
42+
return;
43+
}
44+
45+
var parameters = new EndpointParameter[method.Parameters.Length];
46+
47+
for (var i = 0; i < method.Parameters.Length; i++)
48+
{
49+
var parameter = new EndpointParameter(method.Parameters[i], wellKnownTypes);
50+
51+
if (parameter.Source == EndpointParameterSource.Unknown)
52+
{
53+
Diagnostics.Add(DiagnosticDescriptors.GetUnableToResolveParameterDescriptor(parameter.Name));
54+
return;
55+
}
56+
57+
parameters[i] = parameter;
58+
}
59+
60+
Parameters = parameters;
4961
}
5062

51-
public override bool Equals(object o)
63+
public string HttpMethod { get; }
64+
public string? RoutePattern { get; }
65+
public EndpointResponse? Response { get; }
66+
public EndpointParameter[] Parameters { get; } = Array.Empty<EndpointParameter>();
67+
public string EmitArgumentList() => _argumentListCache ??= string.Join(", ", Parameters.Select(p => p.EmitArgument()));
68+
69+
public List<DiagnosticDescriptor> Diagnostics { get; } = new List<DiagnosticDescriptor>();
70+
71+
public (string File, int LineNumber) Location { get; }
72+
public IInvocationOperation Operation { get; }
73+
74+
public override bool Equals(object o) =>
75+
o is Endpoint other && Location == other.Location && SignatureEquals(this, other);
76+
77+
public override int GetHashCode() =>
78+
HashCode.Combine(Location, GetSignatureHashCode(this));
79+
80+
public static bool SignatureEquals(Endpoint a, Endpoint b)
5281
{
53-
if (o is null)
82+
if (!a.Response.WrappedResponseType.Equals(b.Response.WrappedResponseType, StringComparison.Ordinal) ||
83+
!a.HttpMethod.Equals(b.HttpMethod, StringComparison.Ordinal) ||
84+
a.Parameters.Length != b.Parameters.Length)
5485
{
5586
return false;
5687
}
5788

58-
if (o is Endpoint endpoint)
89+
for (var i = 0; i < a.Parameters.Length; i++)
5990
{
60-
return endpoint.HttpMethod.Equals(HttpMethod, StringComparison.OrdinalIgnoreCase) &&
61-
endpoint.Location.Item1.Equals(Location.Item1, StringComparison.OrdinalIgnoreCase) &&
62-
endpoint.Location.Item2.Equals(Location.Item2) &&
63-
endpoint.Response.Equals(Response) &&
64-
endpoint.Diagnostics.SequenceEqual(Diagnostics);
91+
if (a.Parameters[i].Equals(b.Parameters[i]))
92+
{
93+
return false;
94+
}
6595
}
6696

67-
return false;
97+
return true;
6898
}
6999

70-
public override int GetHashCode() =>
71-
HashCode.Combine(HttpMethod, Route, Location, Response, Diagnostics);
100+
public static int GetSignatureHashCode(Endpoint endpoint)
101+
{
102+
var hashCode = new HashCode();
103+
hashCode.Add(endpoint.Response.WrappedResponseType);
104+
hashCode.Add(endpoint.HttpMethod);
105+
106+
foreach (var parameter in endpoint.Parameters)
107+
{
108+
hashCode.Add(parameter);
109+
}
110+
111+
return hashCode.ToHashCode();
112+
}
113+
114+
private static (string, int) GetLocation(IInvocationOperation operation)
115+
{
116+
var filePath = operation.Syntax.SyntaxTree.FilePath;
117+
var span = operation.Syntax.SyntaxTree.GetLineSpan(operation.Syntax.Span);
118+
var lineNumber = span.StartLinePosition.Line + 1;
119+
return (filePath, lineNumber);
120+
}
121+
122+
private static string GetHttpMethod(IInvocationOperation operation)
123+
{
124+
var syntax = (InvocationExpressionSyntax)operation.Syntax;
125+
var expression = (MemberAccessExpressionSyntax)syntax.Expression;
126+
var name = (IdentifierNameSyntax)expression.Name;
127+
var identifier = name.Identifier;
128+
return identifier.ValueText;
129+
}
72130
}
Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,14 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
3-
using System;
4-
using System.Collections;
3+
54
using System.Collections.Generic;
5+
66
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
77

8-
internal sealed class EndpointDelegateComparer : IEqualityComparer<Endpoint>, IComparer<Endpoint>
8+
internal sealed class EndpointDelegateComparer : IEqualityComparer<Endpoint>
99
{
1010
public static readonly EndpointDelegateComparer Instance = new EndpointDelegateComparer();
1111

12-
public bool Equals(Endpoint a, Endpoint b) => Compare(a, b) == 0;
13-
14-
public int GetHashCode(Endpoint endpoint) => HashCode.Combine(
15-
endpoint.Response.WrappedResponseType,
16-
endpoint.Response.IsVoid,
17-
endpoint.Response.IsAwaitable,
18-
endpoint.HttpMethod);
19-
20-
public int Compare(Endpoint a, Endpoint b)
21-
{
22-
if (a.Response.IsAwaitable == b.Response.IsAwaitable &&
23-
a.Response.IsVoid == b.Response.IsVoid &&
24-
a.Response.WrappedResponseType.Equals(b.Response.WrappedResponseType, StringComparison.Ordinal) &&
25-
a.HttpMethod.Equals(b.HttpMethod, StringComparison.Ordinal))
26-
{
27-
return 0;
28-
}
29-
return -1;
30-
}
12+
public bool Equals(Endpoint a, Endpoint b) => Endpoint.SignatureEquals(a, b);
13+
public int GetHashCode(Endpoint endpoint) => Endpoint.GetSignatureHashCode(endpoint);
3114
}

0 commit comments

Comments
 (0)