Skip to content

Commit 42905b0

Browse files
authored
Follow up on IParsable changes and update tests (#47102)
* Follow up on IParsable changes and update tests * Resolve bug with both JSON body and service * Unwrap awaitable response types in filter invocation * Add emitter context to capture helper requirements * Add baselines assets to Helix * Fix lookup path on Helix * Fix type used in MapAction_WarnsForUnsupportedFormTypes
1 parent 6f0e422 commit 42905b0

28 files changed

+540
-1261
lines changed

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

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
8989
codeWriter.WriteLineNoTabs(string.Empty);
9090
codeWriter.WriteLine("if (options?.EndpointBuilder?.FilterFactories.Count > 0)");
9191
codeWriter.StartBlock();
92-
codeWriter.WriteLine("filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic =>");
92+
if (endpoint.Response.IsAwaitable)
93+
{
94+
codeWriter.WriteLine("filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(async ic =>");
95+
}
96+
else
97+
{
98+
codeWriter.WriteLine("filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic =>");
99+
}
93100
codeWriter.StartBlock();
94101
codeWriter.WriteLine("if (ic.HttpContext.Response.StatusCode == 400)");
95102
codeWriter.StartBlock();
96103
codeWriter.WriteLine("return ValueTask.FromResult<object?>(Results.Empty);");
97104
codeWriter.EndBlock();
98-
codeWriter.WriteLine(endpoint.EmitFilteredInvocation());
105+
endpoint.EmitFilteredInvocation(codeWriter);
99106
codeWriter.EndBlockWithComma();
100107
codeWriter.WriteLine("options.EndpointBuilder,");
101108
codeWriter.WriteLine("handler.Method);");
@@ -146,11 +153,58 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
146153
return stringWriter.ToString();
147154
});
148155

149-
var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions);
156+
var endpointHelpers = endpoints
157+
.Collect()
158+
.Select((endpoints, _) =>
159+
{
160+
var hasJsonBodyOrService = endpoints.Any(endpoint => endpoint.EmitterContext.HasJsonBodyOrService);
161+
var hasJsonBody = endpoints.Any(endpoint => endpoint.EmitterContext.HasJsonBody);
162+
var hasRouteOrQuery = endpoints.Any(endpoint => endpoint.EmitterContext.HasRouteOrQuery);
163+
var hasBindAsync = endpoints.Any(endpoint => endpoint.EmitterContext.HasBindAsync);
164+
var hasParsable = endpoints.Any(endpoint => endpoint.EmitterContext.HasParsable);
165+
var hasJsonResponse = endpoints.Any(endpoint => endpoint.EmitterContext.HasJsonResponse);
166+
167+
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
168+
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 0);
169+
170+
if (hasRouteOrQuery)
171+
{
172+
codeWriter.WriteLine(RequestDelegateGeneratorSources.ResolveFromRouteOrQueryMethod);
173+
}
174+
175+
if (hasJsonResponse)
176+
{
177+
codeWriter.WriteLine(RequestDelegateGeneratorSources.WriteToResponseAsyncMethod);
178+
}
179+
180+
if (hasJsonBody || hasJsonBodyOrService)
181+
{
182+
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveBodyAsyncMethod);
183+
}
184+
185+
if (hasBindAsync)
186+
{
187+
codeWriter.WriteLine(RequestDelegateGeneratorSources.BindAsyncMethod);
188+
}
189+
190+
if (hasJsonBodyOrService)
191+
{
192+
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveJsonBodyOrServiceAsyncMethod);
193+
}
194+
195+
if (hasParsable)
196+
{
197+
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryParseExplicitMethod);
198+
}
199+
200+
return stringWriter.ToString();
201+
});
202+
203+
var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions).Combine(endpointHelpers);
150204

151205
context.RegisterSourceOutput(thunksAndEndpoints, (context, sources) =>
152206
{
153-
var (thunks, endpointsCode) = sources;
207+
var ((thunks, endpointsCode), helpers) = sources;
154208

155209
if (thunks.IsDefaultOrEmpty || string.IsNullOrEmpty(endpointsCode))
156210
{
@@ -166,7 +220,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
166220
var code = RequestDelegateGeneratorSources.GetGeneratedRouteBuilderExtensionsSource(
167221
genericThunks: string.Empty,
168222
thunks: thunksCode.ToString(),
169-
endpoints: endpointsCode);
223+
endpoints: endpointsCode,
224+
helperMethods: helpers ?? string.Empty);
170225

171226
context.AddSource("GeneratedRouteBuilderExtensions.g.cs", code);
172227
});

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

Lines changed: 92 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,97 @@ internal static class RequestDelegateGeneratorSources
1919

2020
public static string GeneratedCodeAttribute => $@"[System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]";
2121

22-
public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints) => $$"""
22+
public static string TryResolveBodyAsyncMethod => """
23+
private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
24+
{
25+
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
26+
27+
if (feature?.CanHaveBody == true)
28+
{
29+
if (!httpContext.Request.HasJsonContentType())
30+
{
31+
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
32+
return (false, default);
33+
}
34+
try
35+
{
36+
var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
37+
if (!allowEmpty && bodyValue == null)
38+
{
39+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
40+
return (false, bodyValue);
41+
}
42+
return (true, bodyValue);
43+
}
44+
catch (IOException)
45+
{
46+
return (false, default);
47+
}
48+
catch (System.Text.Json.JsonException)
49+
{
50+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
51+
return (false, default);
52+
}
53+
}
54+
else if (!allowEmpty)
55+
{
56+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
57+
}
58+
59+
return (false, default);
60+
}
61+
""";
62+
63+
public static string TryParseExplicitMethod => """
64+
private static bool TryParseExplicit<T>(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable<T>
65+
=> T.TryParse(s, provider, out result);
66+
""";
67+
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+
82+
public static string BindAsyncMethod => """
83+
private static ValueTask<T?> BindAsync<T>(HttpContext context, ParameterInfo parameter)
84+
where T : class, IBindableFromHttpContext<T>
85+
{
86+
return T.BindAsync(context, parameter);
87+
}
88+
""";
89+
90+
public static string ResolveFromRouteOrQueryMethod => """
91+
private static Func<HttpContext, StringValues> ResolveFromRouteOrQuery(string parameterName, IEnumerable<string>? routeParameterNames)
92+
{
93+
return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true
94+
? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName])
95+
: (httpContext) => httpContext.Request.Query[parameterName];
96+
}
97+
""";
98+
99+
public static string WriteToResponseAsyncMethod => """
100+
private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo, JsonSerializerOptions options)
101+
{
102+
var runtimeType = value?.GetType();
103+
if (runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.PolymorphismOptions is not null)
104+
{
105+
return httpContext.Response.WriteAsJsonAsync(value!, jsonTypeInfo);
106+
}
107+
108+
return httpContext.Response.WriteAsJsonAsync(value!, options.GetTypeInfo(runtimeType));
109+
}
110+
""";
111+
112+
public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints, string helperMethods) => $$"""
23113
{{SourceHeader}}
24114
25115
namespace Microsoft.AspNetCore.Builder
@@ -107,87 +197,7 @@ private static Task ExecuteObjectResult(object? obj, HttpContext httpContext)
107197
}
108198
}
109199
110-
private static Func<HttpContext, StringValues> ResolveFromRouteOrQuery(string parameterName, IEnumerable<string>? routeParameterNames)
111-
{
112-
return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true
113-
? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName])
114-
: (httpContext) => httpContext.Request.Query[parameterName];
115-
}
116-
117-
private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo, JsonSerializerOptions options)
118-
{
119-
var runtimeType = value?.GetType();
120-
if (runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.PolymorphismOptions is not null)
121-
{
122-
return httpContext.Response.WriteAsJsonAsync(value!, jsonTypeInfo);
123-
}
124-
125-
return httpContext.Response.WriteAsJsonAsync(value!, options.GetTypeInfo(runtimeType));
126-
}
127-
128-
private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
129-
{
130-
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
131-
132-
if (feature?.CanHaveBody == true)
133-
{
134-
if (!httpContext.Request.HasJsonContentType())
135-
{
136-
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
137-
return (false, default);
138-
}
139-
try
140-
{
141-
var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
142-
if (!allowEmpty && bodyValue == null)
143-
{
144-
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
145-
return (false, bodyValue);
146-
}
147-
return (true, bodyValue);
148-
}
149-
catch (IOException)
150-
{
151-
return (false, default);
152-
}
153-
catch (System.Text.Json.JsonException)
154-
{
155-
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
156-
return (false, default);
157-
}
158-
}
159-
else if (!allowEmpty)
160-
{
161-
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
162-
}
163-
164-
return (false, default);
165-
}
166-
167-
private static ValueTask<T?> BindAsync<T>(HttpContext context, ParameterInfo parameter)
168-
where T : class, IBindableFromHttpContext<T>
169-
{
170-
return T.BindAsync(context, parameter);
171-
}
172-
173-
private static ValueTask<(bool, T?)> TryResolveJsonBodyOrServiceAsync<T>(HttpContext httpContext, bool isOptional, IServiceProviderIsService? serviceProviderIsService = null)
174-
{
175-
if (serviceProviderIsService is not null)
176-
{
177-
if (serviceProviderIsService.IsService(typeof(T)))
178-
{
179-
return new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
180-
}
181-
}
182-
return TryResolveBodyAsync<T>(httpContext, isOptional);
183-
}
184-
}
185-
186-
{{GeneratedCodeAttribute}}
187-
file static class ParsableHelper<T> where T : IParsable<T>
188-
{
189-
public static T Parse(string s, IFormatProvider? provider) => T.Parse(s, provider);
190-
public static bool TryParse(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) => T.TryParse(s, provider, out result);
200+
{{helperMethods}}
191201
}
192202
}
193203
""";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
4+
5+
internal sealed class EmitterContext
6+
{
7+
public bool HasJsonBodyOrService { get; set; }
8+
public bool HasJsonBody { get; set; }
9+
public bool HasRouteOrQuery { get; set; }
10+
public bool HasBindAsync { get; set; }
11+
public bool HasParsable { get; set; }
12+
public bool HasJsonResponse { get; set; }
13+
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +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.Text;
5-
64
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
75

86
internal static class EndpointJsonResponseEmitter

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,14 @@ internal static void EmitJsonBodyParameterPreparationString(this EndpointParamet
108108
// Invoke TryResolveBody method to parse JSON and set
109109
// status codes on exceptions.
110110
var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(httpContext, {(endpointParameter.IsOptional ? "true" : "false")})";
111-
codeWriter.WriteLine($"var (isSuccessful, {endpointParameter.EmitHandlerArgument()}) = {assigningCode};");
111+
var resolveBodyResult = $"{endpointParameter.Name}_resolveBodyResult";
112+
codeWriter.WriteLine($"var {resolveBodyResult} = {assigningCode};");
113+
codeWriter.WriteLine($"var {endpointParameter.EmitHandlerArgument()} = {resolveBodyResult}.Item2;");
112114

113115
// If binding from the JSON body fails, we exit early. Don't
114116
// set the status code here because assume it has been set by the
115117
// TryResolveBody method.
116-
codeWriter.WriteLine("if (!isSuccessful)");
118+
codeWriter.WriteLine($"if (!{resolveBodyResult}.Item1)");
117119
codeWriter.StartBlock();
118120
codeWriter.WriteLine("return;");
119121
codeWriter.EndBlock();
@@ -128,11 +130,13 @@ internal static void EmitJsonBodyOrServiceParameterPreparationString(this Endpoi
128130
// type from DI if it exists. Otherwise, resolve the parameter
129131
// as a body parameter.
130132
var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveJsonBodyOrServiceAsync<{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(httpContext, {(endpointParameter.IsOptional ? "true" : "false")}, serviceProviderIsService)";
131-
codeWriter.WriteLine($"var (isSuccessful, {endpointParameter.EmitHandlerArgument()}) = {assigningCode};");
133+
var resolveJsonBodyOrServiceResult = $"{endpointParameter.Name}_resolveJsonBodyOrServiceResult";
134+
codeWriter.WriteLine($"var {resolveJsonBodyOrServiceResult} = {assigningCode};");
135+
codeWriter.WriteLine($"var {endpointParameter.EmitHandlerArgument()} = {resolveJsonBodyOrServiceResult}.Item2;");
132136

133137
// If binding from the JSON body fails, TryResolveJsonBodyOrService
134138
// will return `false` and we will need to exit early.
135-
codeWriter.WriteLine("if (!isSuccessful)");
139+
codeWriter.WriteLine($"if (!{resolveJsonBodyOrServiceResult}.Item1)");
136140
codeWriter.StartBlock();
137141
codeWriter.WriteLine("return;");
138142
codeWriter.EndBlock();

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using Microsoft.AspNetCore.Analyzers.Infrastructure;
78
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
9+
using Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
810
using Microsoft.CodeAnalysis;
911
using Microsoft.CodeAnalysis.CSharp.Syntax;
1012
using Microsoft.CodeAnalysis.Operations;
@@ -18,6 +20,7 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
1820
Operation = operation;
1921
Location = GetLocation(operation);
2022
HttpMethod = GetHttpMethod(operation);
23+
EmitterContext = new EmitterContext();
2124

2225
if (!operation.TryGetRouteHandlerPattern(out var routeToken))
2326
{
@@ -34,6 +37,7 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
3437
}
3538

3639
Response = new EndpointResponse(method, wellKnownTypes);
40+
EmitterContext.HasJsonResponse = !(Response.ResponseType.IsSealed || Response.ResponseType.IsValueType);
3741
IsAwaitable = Response.IsAwaitable;
3842

3943
if (method.Parameters.Length == 0)
@@ -75,12 +79,19 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
7579
}
7680

7781
Parameters = parameters;
82+
83+
EmitterContext.HasJsonBodyOrService = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBodyOrService);
84+
EmitterContext.HasJsonBody = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBody);
85+
EmitterContext.HasRouteOrQuery = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.RouteOrQuery);
86+
EmitterContext.HasBindAsync = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.BindAsync);
87+
EmitterContext.HasParsable = Parameters.Any(parameter => parameter.IsParsable);
7888
}
7989

8090
public string HttpMethod { get; }
8191
public bool IsAwaitable { get; }
8292
public bool NeedsParameterArray { get; }
8393
public string? RoutePattern { get; }
94+
public EmitterContext EmitterContext { get; }
8495
public EndpointResponse? Response { get; }
8596
public EndpointParameter[] Parameters { get; } = Array.Empty<EndpointParameter>();
8697
public List<Diagnostic> Diagnostics { get; } = new List<Diagnostic>();

0 commit comments

Comments
 (0)