Skip to content

Commit f7628d4

Browse files
authored
Move route or query resolution to startup (#46972)
* Move route or query resolution to startup * Avoid string interpolation in lookup string * Address feedback from review
1 parent db7f79a commit f7628d4

21 files changed

+203
-4949
lines changed

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

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.CodeAnalysis.CSharp.Syntax;
1212
using Microsoft.CodeAnalysis.Operations;
1313
using Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
14+
using Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
1415

1516
namespace Microsoft.AspNetCore.Http.Generators;
1617

@@ -61,47 +62,56 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6162
.Where(endpoint => endpoint.Diagnostics.Count == 0)
6263
.WithTrackingName(GeneratorSteps.EndpointsWithoutDiagnosicsStep);
6364

64-
var thunks = endpoints.Select((endpoint, _) => $$"""
65-
[{{endpoint.EmitSourceKey()}}] = (
66-
(methodInfo, options) =>
67-
{
68-
Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found.");
69-
options.EndpointBuilder.Metadata.Add(new SourceKey{{endpoint.EmitSourceKey()}});
70-
return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() };
71-
},
72-
(del, options, inferredMetadataResult) =>
73-
{
74-
var handler = ({{endpoint.EmitHandlerDelegateCast()}})del;
75-
EndpointFilterDelegate? filteredInvocation = null;
76-
77-
if (options?.EndpointBuilder?.FilterFactories.Count > 0)
78-
{
79-
filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic =>
80-
{
81-
if (ic.HttpContext.Response.StatusCode == 400)
82-
{
83-
return ValueTask.FromResult<object?>(Results.Empty);
84-
}
85-
{{endpoint.EmitFilteredInvocation()}}
86-
},
87-
options.EndpointBuilder,
88-
handler.Method);
89-
}
90-
91-
{{endpoint.EmitRequestHandler(baseIndent: 5)}}
92-
{{endpoint.EmitFilteredRequestHandler(baseIndent: 5)}}
93-
RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered;
94-
var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection<object>.Empty;
95-
return new RequestDelegateResult(targetDelegate, metadata);
96-
}),
97-
""");
65+
var thunks = endpoints.Select((endpoint, _) =>
66+
{
67+
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
68+
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 3);
69+
codeWriter.InitializeIndent();
70+
codeWriter.WriteLine($"[{endpoint.EmitSourceKey()}] = (");
71+
codeWriter.Indent++;
72+
codeWriter.WriteLine("(methodInfo, options) =>");
73+
codeWriter.StartBlock();
74+
codeWriter.WriteLine(@"Debug.Assert(options?.EndpointBuilder != null, ""EndpointBuilder not found."");");
75+
codeWriter.WriteLine($"options.EndpointBuilder.Metadata.Add(new SourceKey{endpoint.EmitSourceKey()});");
76+
codeWriter.WriteLine("return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() };");
77+
codeWriter.EndBlockWithComma();
78+
codeWriter.WriteLine("(del, options, inferredMetadataResult) =>");
79+
codeWriter.StartBlock();
80+
codeWriter.WriteLine($"var handler = ({endpoint.EmitHandlerDelegateCast()})del;");
81+
codeWriter.WriteLine("EndpointFilterDelegate? filteredInvocation = null;");
82+
endpoint.EmitRouteOrQueryResolver(codeWriter);
83+
codeWriter.WriteLineNoTabs(string.Empty);
84+
codeWriter.WriteLine("if (options?.EndpointBuilder?.FilterFactories.Count > 0)");
85+
codeWriter.StartBlock();
86+
codeWriter.WriteLine("filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic =>");
87+
codeWriter.StartBlock();
88+
codeWriter.WriteLine("if (ic.HttpContext.Response.StatusCode == 400)");
89+
codeWriter.StartBlock();
90+
codeWriter.WriteLine("return ValueTask.FromResult<object?>(Results.Empty);");
91+
codeWriter.EndBlock();
92+
codeWriter.WriteLine(endpoint.EmitFilteredInvocation());
93+
codeWriter.EndBlockWithComma();
94+
codeWriter.WriteLine("options.EndpointBuilder,");
95+
codeWriter.WriteLine("handler.Method);");
96+
codeWriter.EndBlock();
97+
codeWriter.WriteLineNoTabs(string.Empty);
98+
endpoint.EmitRequestHandler(codeWriter);
99+
codeWriter.WriteLineNoTabs(string.Empty);
100+
endpoint.EmitFilteredRequestHandler(codeWriter);
101+
codeWriter.WriteLineNoTabs(string.Empty);
102+
codeWriter.WriteLine("RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered;");
103+
codeWriter.WriteLine("var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection<object>.Empty;");
104+
codeWriter.WriteLine("return new RequestDelegateResult(targetDelegate, metadata);");
105+
codeWriter.Indent--;
106+
codeWriter.Write("}),");
107+
return stringWriter.ToString();
108+
});
98109

99110
var stronglyTypedEndpointDefinitions = endpoints
100111
.Collect()
101112
.Select((endpoints, _) =>
102113
{
103114
var dedupedByDelegate = endpoints.Distinct(EndpointDelegateComparer.Instance);
104-
var code = new StringBuilder();
105115
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
106116
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 2);
107117
foreach (var endpoint in dedupedByDelegate)

src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ private static Task ExecuteObjectResult(object? obj, HttpContext httpContext)
102102
}
103103
}
104104
105+
private static Func<HttpContext, StringValues> ResolveFromRouteOrQuery(string parameterName, IEnumerable<string>? routeParameterNames)
106+
{
107+
return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true
108+
? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName])
109+
: (httpContext) => httpContext.Request.Query[parameterName];
110+
}
111+
105112
private static async ValueTask<(bool, T?)> TryResolveBody<T>(HttpContext httpContext, bool allowEmpty)
106113
{
107114
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointEmitter.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,18 @@ internal static string EmitParameterPreparation(this Endpoint endpoint, int base
4343
return stringWriter.ToString();
4444
}
4545

46+
public static void EmitRouteOrQueryResolver(this Endpoint endpoint, CodeWriter codeWriter)
47+
{
48+
foreach (var parameter in endpoint.Parameters)
49+
{
50+
if (parameter.Source == EndpointParameterSource.RouteOrQuery)
51+
{
52+
var parameterName = parameter.Name;
53+
codeWriter.Write($@"var {parameterName}_RouteOrQueryResolver = ");
54+
codeWriter.WriteLine($@"GeneratedRouteBuilderExtensionsCore.ResolveFromRouteOrQuery(""{parameterName}"", options?.RouteParameterNames);");
55+
}
56+
}
57+
}
58+
4659
public static string EmitArgumentList(this Endpoint endpoint) => string.Join(", ", endpoint.Parameters.Select(p => p.EmitArgument()));
4760
}

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,7 @@ internal static void EmitRouteOrQueryParameterPreparation(this EndpointParameter
8585
codeWriter.WriteLine(endpointParameter.EmitParameterDiagnosticComment());
8686

8787
var parameterName = endpointParameter.Name;
88-
codeWriter.Write($"var {endpointParameter.EmitAssigningCodeResult()} = ");
89-
codeWriter.WriteLine($@"options?.RouteParameterNames?.Contains(""{parameterName}"", StringComparer.OrdinalIgnoreCase) == true");
90-
codeWriter.Indent++;
91-
codeWriter.WriteLine($@"? new StringValues(httpContext.Request.RouteValues[$""{parameterName}""]?.ToString())");
92-
codeWriter.WriteLine($@": httpContext.Request.Query[$""{parameterName}""];");
93-
codeWriter.Indent--;
88+
codeWriter.WriteLine($"var {endpointParameter.EmitAssigningCodeResult()} = {parameterName}_RouteOrQueryResolver(httpContext);");
9489

9590
if (!endpointParameter.IsOptional)
9691
{

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,13 @@ public static string EmitVerb(this Endpoint endpoint)
7676
}
7777

7878
/*
79-
* TODO: Emit invocation to the request handler. The structure
79+
* Emit invocation to the request handler. The structure
8080
* involved here consists of a call to bind parameters, check
8181
* their validity (optionality), invoke the underlying handler with
8282
* the arguments bound from HTTP context, and write out the response.
8383
*/
84-
public static string EmitRequestHandler(this Endpoint endpoint, int baseIndent = 0)
84+
public static void EmitRequestHandler(this Endpoint endpoint, CodeWriter codeWriter)
8585
{
86-
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
87-
using var codeWriter = new CodeWriter(stringWriter, baseIndent);
88-
8986
codeWriter.WriteLine(endpoint.IsAwaitable ? "async Task RequestHandler(HttpContext httpContext)" : "Task RequestHandler(HttpContext httpContext)");
9087
codeWriter.StartBlock(); // Start handler method block
9188
codeWriter.WriteLine("var wasParamCheckFailure = false;");
@@ -115,8 +112,6 @@ public static string EmitRequestHandler(this Endpoint endpoint, int baseIndent =
115112
codeWriter.WriteLine($"handler({endpoint.EmitArgumentList()});");
116113
codeWriter.WriteLine(endpoint.Response.IsVoid ? "return Task.CompletedTask;" : endpoint.EmitResponseWritingCall());
117114
codeWriter.EndBlock(); // End handler method block
118-
119-
return stringWriter.ToString();
120115
}
121116

122117
private static string EmitResponseWritingCall(this Endpoint endpoint)
@@ -156,15 +151,12 @@ private static string EmitResponseWritingCall(this Endpoint endpoint)
156151
* can be used to reduce the boxing that happens at runtime when constructing
157152
* the context object.
158153
*/
159-
public static string EmitFilteredRequestHandler(this Endpoint endpoint, int baseIndent = 0)
154+
public static void EmitFilteredRequestHandler(this Endpoint endpoint, CodeWriter codeWriter)
160155
{
161156
var argumentList = endpoint.Parameters.Length == 0 ? string.Empty : $", {endpoint.EmitArgumentList()}";
162157
var invocationConstructor = endpoint.Parameters.Length == 0 ? "DefaultEndpointFilterInvocationContext" : "EndpointFilterInvocationContext";
163158
var invocationGenericArgs = endpoint.Parameters.Length == 0 ? string.Empty : $"<{endpoint.EmitFilterInvocationContextTypeArgs()}>";
164159

165-
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
166-
using var codeWriter = new CodeWriter(stringWriter, baseIndent);
167-
168160
codeWriter.WriteLine("async Task RequestHandlerFiltered(HttpContext httpContext)");
169161
codeWriter.StartBlock(); // Start handler method block
170162
codeWriter.WriteLine("var wasParamCheckFailure = false;");
@@ -181,8 +173,6 @@ public static string EmitFilteredRequestHandler(this Endpoint endpoint, int base
181173
codeWriter.WriteLine($"var result = await filteredInvocation(new {invocationConstructor}{invocationGenericArgs}(httpContext{argumentList}));");
182174
codeWriter.WriteLine("await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);");
183175
codeWriter.EndBlock(); // End handler method block
184-
185-
return stringWriter.ToString();
186176
}
187177

188178
public static string EmitFilteredInvocation(this Endpoint endpoint)

src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Http.Generated
8686
private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new()
8787
{
8888
[(@"TestMapActions.cs", 23)] = (
89-
(methodInfo, options) =>
89+
(methodInfo, options) =>
9090
{
9191
Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found.");
9292
options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 23));
@@ -154,7 +154,7 @@ namespace Microsoft.AspNetCore.Http.Generated
154154
return new RequestDelegateResult(targetDelegate, metadata);
155155
}),
156156
[(@"TestMapActions.cs", 25)] = (
157-
(methodInfo, options) =>
157+
(methodInfo, options) =>
158158
{
159159
Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found.");
160160
options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 25));
@@ -269,6 +269,13 @@ namespace Microsoft.AspNetCore.Http.Generated
269269
}
270270
}
271271

272+
private static Func<HttpContext, StringValues> ResolveFromRouteOrQuery(string parameterName, IEnumerable<string>? routeParameterNames)
273+
{
274+
return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true
275+
? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName])
276+
: (httpContext) => httpContext.Request.Query[parameterName];
277+
}
278+
272279
private static async ValueTask<(bool, T?)> TryResolveBody<T>(HttpContext httpContext, bool allowEmpty)
273280
{
274281
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();

src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitServiceParam_SimpleReturn_Snapshot.generated.txt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Http.Generated
116116
private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new()
117117
{
118118
[(@"TestMapActions.cs", 23)] = (
119-
(methodInfo, options) =>
119+
(methodInfo, options) =>
120120
{
121121
Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found.");
122122
options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 23));
@@ -176,7 +176,7 @@ namespace Microsoft.AspNetCore.Http.Generated
176176
return new RequestDelegateResult(targetDelegate, metadata);
177177
}),
178178
[(@"TestMapActions.cs", 24)] = (
179-
(methodInfo, options) =>
179+
(methodInfo, options) =>
180180
{
181181
Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found.");
182182
options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 24));
@@ -236,7 +236,7 @@ namespace Microsoft.AspNetCore.Http.Generated
236236
return new RequestDelegateResult(targetDelegate, metadata);
237237
}),
238238
[(@"TestMapActions.cs", 25)] = (
239-
(methodInfo, options) =>
239+
(methodInfo, options) =>
240240
{
241241
Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found.");
242242
options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 25));
@@ -347,6 +347,13 @@ namespace Microsoft.AspNetCore.Http.Generated
347347
}
348348
}
349349

350+
private static Func<HttpContext, StringValues> ResolveFromRouteOrQuery(string parameterName, IEnumerable<string>? routeParameterNames)
351+
{
352+
return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true
353+
? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName])
354+
: (httpContext) => httpContext.Request.Query[parameterName];
355+
}
356+
350357
private static async ValueTask<(bool, T?)> TryResolveBody<T>(HttpContext httpContext, bool allowEmpty)
351358
{
352359
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();

0 commit comments

Comments
 (0)