Skip to content

Commit 2a7dc90

Browse files
authored
Only invoke 'IsService' check once in resolution (#47097)
* Only invoke 'IsService' check once in resolution * Update method name * Add baseline test
1 parent 00f7880 commit 2a7dc90

File tree

6 files changed

+310
-22
lines changed

6 files changed

+310
-22
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
8686
codeWriter.WriteLine($"var handler = ({endpoint!.EmitHandlerDelegateCast()})del;");
8787
codeWriter.WriteLine("EndpointFilterDelegate? filteredInvocation = null;");
8888
endpoint!.EmitRouteOrQueryResolver(codeWriter);
89-
endpoint!.EmitJsonBodyOrServicePreparation(codeWriter);
89+
endpoint!.EmitJsonBodyOrServiceResolver(codeWriter);
9090
endpoint!.Response?.EmitJsonPreparation(codeWriter);
9191
if (endpoint.NeedsParameterArray)
9292
{
@@ -190,7 +190,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
190190

191191
if (hasJsonBodyOrService)
192192
{
193-
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveJsonBodyOrServiceAsyncMethod);
193+
codeWriter.WriteLine(RequestDelegateGeneratorSources.ResolveJsonBodyOrServiceMethod);
194194
}
195195

196196
if (hasParsable)

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,6 @@ private static bool TryParseExplicit<T>(string? s, IFormatProvider? provider, [M
6565
=> T.TryParse(s, provider, out result);
6666
""";
6767

68-
public static string TryResolveJsonBodyOrServiceAsyncMethod => """
69-
private static ValueTask<(bool, T?)> TryResolveJsonBodyOrServiceAsync<T>(HttpContext httpContext, bool isOptional, IServiceProviderIsService? serviceProviderIsService = null)
70-
{
71-
if (serviceProviderIsService is not null)
72-
{
73-
if (serviceProviderIsService.IsService(typeof(T)))
74-
{
75-
return new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
76-
}
77-
}
78-
return TryResolveBodyAsync<T>(httpContext, isOptional);
79-
}
80-
""";
81-
8268
public static string BindAsyncMethod => """
8369
private static ValueTask<T?> BindAsync<T>(HttpContext context, ParameterInfo parameter)
8470
where T : class, IBindableFromHttpContext<T>
@@ -109,6 +95,20 @@ private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, J
10995
}
11096
""";
11197

98+
public static string ResolveJsonBodyOrServiceMethod => """
99+
private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(IServiceProviderIsService? serviceProviderIsService = null)
100+
{
101+
if (serviceProviderIsService is not null)
102+
{
103+
if (serviceProviderIsService.IsService(typeof(T)))
104+
{
105+
return static (httpContext, isOptional) => new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
106+
}
107+
}
108+
return static (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, isOptional);
109+
}
110+
""";
111+
112112
public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints, string helperMethods) => $$"""
113113
{{SourceHeader}}
114114

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,20 @@ public static void EmitRouteOrQueryResolver(this Endpoint endpoint, CodeWriter c
6161
}
6262
}
6363

64-
public static void EmitJsonBodyOrServicePreparation(this Endpoint endpoint, CodeWriter codeWriter)
64+
public static void EmitJsonBodyOrServiceResolver(this Endpoint endpoint, CodeWriter codeWriter)
6565
{
66+
var serviceProviderEmitted = false;
6667
foreach (var parameter in endpoint.Parameters)
6768
{
6869
if (parameter.Source == EndpointParameterSource.JsonBodyOrService)
6970
{
70-
codeWriter.WriteLine("var serviceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>();");
71-
return;
71+
if (!serviceProviderEmitted)
72+
{
73+
codeWriter.WriteLine("var serviceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>();");
74+
serviceProviderEmitted = true;
75+
}
76+
codeWriter.Write($@"var {parameter.Name}_JsonBodyOrServiceResolver = ");
77+
codeWriter.WriteLine($"ResolveJsonBodyOrService<{parameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(serviceProviderIsService);");
7278
}
7379
}
7480
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Reflection.Metadata;
56
using Microsoft.AspNetCore.Analyzers.Infrastructure;
67
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
78
using Microsoft.CodeAnalysis;
@@ -166,15 +167,15 @@ internal static void EmitJsonBodyOrServiceParameterPreparationString(this Endpoi
166167
// Preamble for diagnostics purposes.
167168
codeWriter.WriteLine(endpointParameter.EmitParameterDiagnosticComment());
168169

169-
// Invoke TryResolveJsonBodyOrService method to resolve the
170+
// Invoke ResolveJsonBodyOrService method to resolve the
170171
// type from DI if it exists. Otherwise, resolve the parameter
171172
// as a body parameter.
172-
var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveJsonBodyOrServiceAsync<{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(httpContext, {(endpointParameter.IsOptional ? "true" : "false")}, serviceProviderIsService)";
173+
var assigningCode = $"await {endpointParameter.Name}_JsonBodyOrServiceResolver(httpContext, {(endpointParameter.IsOptional ? "true" : "false")})";
173174
var resolveJsonBodyOrServiceResult = $"{endpointParameter.Name}_resolveJsonBodyOrServiceResult";
174175
codeWriter.WriteLine($"var {resolveJsonBodyOrServiceResult} = {assigningCode};");
175176
codeWriter.WriteLine($"var {endpointParameter.EmitHandlerArgument()} = {resolveJsonBodyOrServiceResult}.Item2;");
176177

177-
// If binding from the JSON body fails, TryResolveJsonBodyOrService
178+
// If binding from the JSON body fails, ResolveJsonBodyOrService
178179
// will return `false` and we will need to exit early.
179180
codeWriter.WriteLine($"if (!{resolveJsonBodyOrServiceResult}.Item1)");
180181
codeWriter.StartBlock();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by a tool.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
#nullable enable
10+
11+
namespace Microsoft.AspNetCore.Builder
12+
{
13+
%GENERATEDCODEATTRIBUTE%
14+
internal class SourceKey
15+
{
16+
public string Path { get; init; }
17+
public int Line { get; init; }
18+
19+
public SourceKey(string path, int line)
20+
{
21+
Path = path;
22+
Line = line;
23+
}
24+
}
25+
26+
// This class needs to be internal so that the compiled application
27+
// has access to the strongly-typed endpoint definitions that are
28+
// generated by the compiler so that they will be favored by
29+
// overload resolution and opt the runtime in to the code generated
30+
// implementation produced here.
31+
%GENERATEDCODEATTRIBUTE%
32+
internal static class GenerateRouteBuilderEndpoints
33+
{
34+
private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get };
35+
private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post };
36+
private static readonly string[] PutVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Put };
37+
private static readonly string[] DeleteVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Delete };
38+
private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch };
39+
40+
internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapPost(
41+
this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,
42+
[global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern,
43+
global::System.Func<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo, global::Microsoft.AspNetCore.Http.Generators.Tests.TestService, global::System.String> handler,
44+
[global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "",
45+
[global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)
46+
{
47+
return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(
48+
endpoints,
49+
pattern,
50+
handler,
51+
PostVerb,
52+
filePath,
53+
lineNumber);
54+
}
55+
56+
}
57+
}
58+
59+
namespace Microsoft.AspNetCore.Http.Generated
60+
{
61+
using System;
62+
using System.Collections;
63+
using System.Collections.Generic;
64+
using System.Collections.ObjectModel;
65+
using System.Diagnostics;
66+
using System.Diagnostics.CodeAnalysis;
67+
using System.Globalization;
68+
using System.Linq;
69+
using System.Reflection;
70+
using System.Text.Json;
71+
using System.Text.Json.Serialization.Metadata;
72+
using System.Threading.Tasks;
73+
using System.IO;
74+
using Microsoft.AspNetCore.Routing;
75+
using Microsoft.AspNetCore.Routing.Patterns;
76+
using Microsoft.AspNetCore.Builder;
77+
using Microsoft.AspNetCore.Http;
78+
using Microsoft.AspNetCore.Http.Json;
79+
using Microsoft.AspNetCore.Http.Metadata;
80+
using Microsoft.Extensions.DependencyInjection;
81+
using Microsoft.Extensions.FileProviders;
82+
using Microsoft.Extensions.Primitives;
83+
using Microsoft.Extensions.Options;
84+
85+
using MetadataPopulator = System.Func<System.Reflection.MethodInfo, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions?, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult>;
86+
using RequestDelegateFactoryFunc = System.Func<System.Delegate, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult?, Microsoft.AspNetCore.Http.RequestDelegateResult>;
87+
88+
file static class GeneratedRouteBuilderExtensionsCore
89+
{
90+
91+
private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new()
92+
{
93+
[(@"TestMapActions.cs", 24)] = (
94+
(methodInfo, options) =>
95+
{
96+
Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found.");
97+
options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 24));
98+
return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() };
99+
},
100+
(del, options, inferredMetadataResult) =>
101+
{
102+
var handler = (Func<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo, global::Microsoft.AspNetCore.Http.Generators.Tests.TestService, global::System.String>)del;
103+
EndpointFilterDelegate? filteredInvocation = null;
104+
var serviceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>();
105+
var todo_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>(serviceProviderIsService);
106+
var svc_JsonBodyOrServiceResolver = ResolveJsonBodyOrService<global::Microsoft.AspNetCore.Http.Generators.Tests.TestService>(serviceProviderIsService);
107+
108+
if (options?.EndpointBuilder?.FilterFactories.Count > 0)
109+
{
110+
filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic =>
111+
{
112+
if (ic.HttpContext.Response.StatusCode == 400)
113+
{
114+
return ValueTask.FromResult<object?>(Results.Empty);
115+
}
116+
return ValueTask.FromResult<object?>(handler(ic.GetArgument<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo>(0), ic.GetArgument<global::Microsoft.AspNetCore.Http.Generators.Tests.TestService>(1)));
117+
},
118+
options.EndpointBuilder,
119+
handler.Method);
120+
}
121+
122+
async Task RequestHandler(HttpContext httpContext)
123+
{
124+
var wasParamCheckFailure = false;
125+
// Endpoint Parameter: todo (Type = Microsoft.AspNetCore.Http.Generators.Tests.Todo, IsOptional = False, IsParsable = False, Source = JsonBodyOrService)
126+
var todo_resolveJsonBodyOrServiceResult = await todo_JsonBodyOrServiceResolver(httpContext, false);
127+
var todo_local = todo_resolveJsonBodyOrServiceResult.Item2;
128+
if (!todo_resolveJsonBodyOrServiceResult.Item1)
129+
{
130+
return;
131+
}
132+
// Endpoint Parameter: svc (Type = Microsoft.AspNetCore.Http.Generators.Tests.TestService, IsOptional = False, IsParsable = False, Source = JsonBodyOrService)
133+
var svc_resolveJsonBodyOrServiceResult = await svc_JsonBodyOrServiceResolver(httpContext, false);
134+
var svc_local = svc_resolveJsonBodyOrServiceResult.Item2;
135+
if (!svc_resolveJsonBodyOrServiceResult.Item1)
136+
{
137+
return;
138+
}
139+
140+
if (wasParamCheckFailure)
141+
{
142+
httpContext.Response.StatusCode = 400;
143+
return;
144+
}
145+
httpContext.Response.ContentType ??= "text/plain";
146+
var result = handler(todo_local!, svc_local!);
147+
await httpContext.Response.WriteAsync(result);
148+
}
149+
150+
async Task RequestHandlerFiltered(HttpContext httpContext)
151+
{
152+
var wasParamCheckFailure = false;
153+
// Endpoint Parameter: todo (Type = Microsoft.AspNetCore.Http.Generators.Tests.Todo, IsOptional = False, IsParsable = False, Source = JsonBodyOrService)
154+
var todo_resolveJsonBodyOrServiceResult = await todo_JsonBodyOrServiceResolver(httpContext, false);
155+
var todo_local = todo_resolveJsonBodyOrServiceResult.Item2;
156+
if (!todo_resolveJsonBodyOrServiceResult.Item1)
157+
{
158+
return;
159+
}
160+
// Endpoint Parameter: svc (Type = Microsoft.AspNetCore.Http.Generators.Tests.TestService, IsOptional = False, IsParsable = False, Source = JsonBodyOrService)
161+
var svc_resolveJsonBodyOrServiceResult = await svc_JsonBodyOrServiceResolver(httpContext, false);
162+
var svc_local = svc_resolveJsonBodyOrServiceResult.Item2;
163+
if (!svc_resolveJsonBodyOrServiceResult.Item1)
164+
{
165+
return;
166+
}
167+
168+
if (wasParamCheckFailure)
169+
{
170+
httpContext.Response.StatusCode = 400;
171+
}
172+
var result = await filteredInvocation(EndpointFilterInvocationContext.Create<global::Microsoft.AspNetCore.Http.Generators.Tests.Todo, global::Microsoft.AspNetCore.Http.Generators.Tests.TestService>(httpContext, todo_local!, svc_local!));
173+
await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);
174+
}
175+
176+
RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered;
177+
var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection<object>.Empty;
178+
return new RequestDelegateResult(targetDelegate, metadata);
179+
}),
180+
181+
};
182+
183+
internal static RouteHandlerBuilder MapCore(
184+
this IEndpointRouteBuilder routes,
185+
string pattern,
186+
Delegate handler,
187+
IEnumerable<string> httpMethods,
188+
string filePath,
189+
int lineNumber)
190+
{
191+
var (populateMetadata, createRequestDelegate) = map[(filePath, lineNumber)];
192+
return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate);
193+
}
194+
195+
private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi)
196+
{
197+
var routeHandlerFilters = builder.FilterFactories;
198+
var context0 = new EndpointFilterFactoryContext
199+
{
200+
MethodInfo = mi,
201+
ApplicationServices = builder.ApplicationServices,
202+
};
203+
var initialFilteredInvocation = filteredInvocation;
204+
for (var i = routeHandlerFilters.Count - 1; i >= 0; i--)
205+
{
206+
var filterFactory = routeHandlerFilters[i];
207+
filteredInvocation = filterFactory(context0, filteredInvocation);
208+
}
209+
return filteredInvocation;
210+
}
211+
212+
private static Task ExecuteObjectResult(object? obj, HttpContext httpContext)
213+
{
214+
if (obj is IResult r)
215+
{
216+
return r.ExecuteAsync(httpContext);
217+
}
218+
else if (obj is string s)
219+
{
220+
return httpContext.Response.WriteAsync(s);
221+
}
222+
else
223+
{
224+
return httpContext.Response.WriteAsJsonAsync(obj);
225+
}
226+
}
227+
228+
private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
229+
{
230+
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
231+
232+
if (feature?.CanHaveBody == true)
233+
{
234+
if (!httpContext.Request.HasJsonContentType())
235+
{
236+
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
237+
return (false, default);
238+
}
239+
try
240+
{
241+
var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
242+
if (!allowEmpty && bodyValue == null)
243+
{
244+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
245+
return (false, bodyValue);
246+
}
247+
return (true, bodyValue);
248+
}
249+
catch (IOException)
250+
{
251+
return (false, default);
252+
}
253+
catch (System.Text.Json.JsonException)
254+
{
255+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
256+
return (false, default);
257+
}
258+
}
259+
else if (!allowEmpty)
260+
{
261+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
262+
}
263+
264+
return (false, default);
265+
}
266+
private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(IServiceProviderIsService? serviceProviderIsService = null)
267+
{
268+
if (serviceProviderIsService is not null)
269+
{
270+
if (serviceProviderIsService.IsService(typeof(T)))
271+
{
272+
return static (httpContext, isOptional) => new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
273+
}
274+
}
275+
return static (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, isOptional);
276+
}
277+
278+
}
279+
}

0 commit comments

Comments
 (0)