Skip to content

Commit 531f5cf

Browse files
authored
Add support for binding from form body to RDG (#47768)
* Migrate form-related tests to shared infrastructure * Add support for form-binding in RDG * Move form logging tests to shared infrastructure * Add snapshot test for generated form code * Pass readFormEmitted state by ref
1 parent 1a2a5cf commit 531f5cf

15 files changed

+1737
-1066
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
165165
{
166166
var hasJsonBodyOrService = endpoints.Any(endpoint => endpoint!.EmitterContext.HasJsonBodyOrService);
167167
var hasJsonBody = endpoints.Any(endpoint => endpoint!.EmitterContext.HasJsonBody);
168+
var hasFormBody = endpoints.Any(endpoint => endpoint!.EmitterContext.HasFormBody);
168169
var hasRouteOrQuery = endpoints.Any(endpoint => endpoint!.EmitterContext.HasRouteOrQuery);
169170
var hasBindAsync = endpoints.Any(endpoint => endpoint!.EmitterContext.HasBindAsync);
170171
var hasParsable = endpoints.Any(endpoint => endpoint!.EmitterContext.HasParsable);
@@ -188,6 +189,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
188189
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveBodyAsyncMethod);
189190
}
190191

192+
if (hasFormBody)
193+
{
194+
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveFormAsyncMethod);
195+
}
196+
191197
if (hasBindAsync)
192198
{
193199
codeWriter.WriteLine(RequestDelegateGeneratorSources.BindAsyncMethod);

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

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal static class RequestDelegateGeneratorSources
2020

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

23-
public static string TryResolveBodyAsyncMethod => $$"""
23+
public static string TryResolveBodyAsyncMethod => """
2424
private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false)
2525
{
2626
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
@@ -79,6 +79,53 @@ internal static class RequestDelegateGeneratorSources
7979
}
8080
""";
8181

82+
public static string TryResolveFormAsyncMethod => """
83+
private static async Task<(bool, object?)> TryResolveFormAsync(
84+
HttpContext httpContext,
85+
LogOrThrowExceptionHelper logOrThrowExceptionHelper,
86+
string parameterTypeName,
87+
string parameterName)
88+
{
89+
object? formValue = null;
90+
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();
91+
92+
if (feature?.CanHaveBody == true)
93+
{
94+
if (!httpContext.Request.HasFormContentType)
95+
{
96+
logOrThrowExceptionHelper.UnexpectedNonFormContentType(httpContext.Request.ContentType);
97+
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
98+
return (false, null);
99+
}
100+
101+
try
102+
{
103+
formValue = await httpContext.Request.ReadFormAsync();
104+
}
105+
catch (BadHttpRequestException ex)
106+
{
107+
logOrThrowExceptionHelper.RequestBodyIOException(ex);
108+
httpContext.Response.StatusCode = ex.StatusCode;
109+
return (false, null);
110+
}
111+
catch (IOException ex)
112+
{
113+
logOrThrowExceptionHelper.RequestBodyIOException(ex);
114+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
115+
return (false, null);
116+
}
117+
catch (InvalidDataException ex)
118+
{
119+
logOrThrowExceptionHelper.InvalidFormRequestBody(parameterTypeName, parameterName, ex);
120+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
121+
return (false, null);
122+
}
123+
}
124+
125+
return (true, formValue);
126+
}
127+
""";
128+
82129
public static string TryParseExplicitMethod => """
83130
private static bool TryParseExplicit<T>(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable<T>
84131
=> T.TryParse(s, provider, out result);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ internal sealed class EmitterContext
66
{
77
public bool HasJsonBodyOrService { get; set; }
88
public bool HasJsonBody { get; set; }
9+
public bool HasFormBody { get; set; }
910
public bool HasRouteOrQuery { get; set; }
1011
public bool HasBindAsync { get; set; }
1112
public bool HasParsable { get; set; }

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal static class EmitterExtensions
1313
EndpointParameterSource.Header => "header",
1414
EndpointParameterSource.Query => "query string",
1515
EndpointParameterSource.RouteOrQuery => "route or query string",
16+
EndpointParameterSource.FormBody => "form",
1617
EndpointParameterSource.BindAsync => endpointParameter.BindMethod == BindabilityMethod.BindAsync
1718
? $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext)"
1819
: $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext, ParameterInfo)",

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ internal static string EmitParameterPreparation(this Endpoint endpoint, int base
1414
{
1515
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
1616
using var parameterPreparationBuilder = new CodeWriter(stringWriter, baseIndent);
17+
var readFormEmitted = false;
1718

1819
foreach (var parameter in endpoint.Parameters)
1920
{
@@ -38,6 +39,9 @@ internal static string EmitParameterPreparation(this Endpoint endpoint, int base
3839
case EndpointParameterSource.JsonBody:
3940
parameter.EmitJsonBodyParameterPreparationString(parameterPreparationBuilder);
4041
break;
42+
case EndpointParameterSource.FormBody:
43+
parameter.EmitFormParameterPreparation(parameterPreparationBuilder, ref readFormEmitted);
44+
break;
4145
case EndpointParameterSource.JsonBodyOrService:
4246
parameter.EmitJsonBodyOrServiceParameterPreparationString(parameterPreparationBuilder);
4347
break;

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,41 @@ internal static void EmitQueryOrHeaderParameterPreparation(this EndpointParamete
5353
endpointParameter.EmitParsingBlock(codeWriter);
5454
}
5555

56+
internal static void EmitFormParameterPreparation(this EndpointParameter endpointParameter, CodeWriter codeWriter, ref bool readFormEmitted)
57+
{
58+
codeWriter.WriteLine(endpointParameter.EmitParameterDiagnosticComment());
59+
60+
// Invoke TryResolveFormAsync once per handler so that we can
61+
// avoid the blocking code-path that occurs when `httpContext.Request.Form`
62+
// is invoked.
63+
if (!readFormEmitted)
64+
{
65+
var shortParameterTypeName = endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat);
66+
var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveFormAsync(httpContext, logOrThrowExceptionHelper, {SymbolDisplay.FormatLiteral(shortParameterTypeName, true)}, {SymbolDisplay.FormatLiteral(endpointParameter.SymbolName, true)})";
67+
var resolveFormResult = $"{endpointParameter.SymbolName}_resolveFormResult";
68+
codeWriter.WriteLine($"var {resolveFormResult} = {assigningCode};");
69+
70+
// Exit early if binding from the form has failed.
71+
codeWriter.WriteLine($"if (!{resolveFormResult}.Item1)");
72+
codeWriter.StartBlock();
73+
codeWriter.WriteLine("return;");
74+
codeWriter.EndBlock();
75+
readFormEmitted = true;
76+
}
77+
78+
codeWriter.WriteLine($"var {endpointParameter.EmitAssigningCodeResult()} = {endpointParameter.AssigningCode};");
79+
if (!endpointParameter.IsOptional)
80+
{
81+
codeWriter.WriteLine($"if ({endpointParameter.EmitAssigningCodeResult()} == null)");
82+
codeWriter.StartBlock();
83+
codeWriter.WriteLine("wasParamCheckFailure = true;");
84+
codeWriter.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(endpointParameter.SymbolName, true)}, {SymbolDisplay.FormatLiteral(endpointParameter.ToMessageString(), true)});");
85+
codeWriter.EndBlock();
86+
}
87+
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()};");
88+
endpointParameter.EmitParsingBlock(codeWriter);
89+
}
90+
5691
internal static void EmitParsingBlock(this EndpointParameter endpointParameter, CodeWriter codeWriter)
5792
{
5893
if (endpointParameter.IsArray && endpointParameter.IsParsable)
@@ -255,7 +290,7 @@ internal static void EmitServiceParameterPreparation(this EndpointParameter endp
255290

256291
public static string EmitArgument(this EndpointParameter endpointParameter) => endpointParameter.Source switch
257292
{
258-
EndpointParameterSource.JsonBody or EndpointParameterSource.Route or EndpointParameterSource.RouteOrQuery or EndpointParameterSource.JsonBodyOrService => endpointParameter.IsOptional ? endpointParameter.EmitHandlerArgument() : $"{endpointParameter.EmitHandlerArgument()}!",
293+
EndpointParameterSource.JsonBody or EndpointParameterSource.Route or EndpointParameterSource.RouteOrQuery or EndpointParameterSource.JsonBodyOrService or EndpointParameterSource.FormBody => endpointParameter.IsOptional ? endpointParameter.EmitHandlerArgument() : $"{endpointParameter.EmitHandlerArgument()}!",
259294
EndpointParameterSource.Unknown => throw new Exception("Unreachable!"),
260295
_ => endpointParameter.EmitHandlerArgument()
261296
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes, S
7272
break;
7373
case EndpointParameterSource.JsonBody:
7474
case EndpointParameterSource.JsonBodyOrService:
75+
case EndpointParameterSource.FormBody:
7576
IsAwaitable = true;
7677
break;
7778
case EndpointParameterSource.Unknown:
@@ -89,6 +90,7 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes, S
8990

9091
EmitterContext.HasJsonBodyOrService = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBodyOrService);
9192
EmitterContext.HasJsonBody = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBody);
93+
EmitterContext.HasFormBody = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.FormBody);
9294
EmitterContext.HasRouteOrQuery = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.RouteOrQuery);
9395
EmitterContext.HasBindAsync = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.BindAsync);
9496
EmitterContext.HasParsable = Parameters.Any(parameter => parameter.IsParsable);

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

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,35 @@ public EndpointParameter(Endpoint endpoint, IParameterSymbol parameter, WellKnow
4949
IsParsable = TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter);
5050
ParsingBlockEmitter = parsingBlockEmitter;
5151
}
52-
else if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata), out _))
52+
else if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata), out var fromFormAttribute))
5353
{
54-
Source = EndpointParameterSource.Unknown;
54+
Source = EndpointParameterSource.FormBody;
55+
LookupName = GetEscapedParameterName(fromFormAttribute, parameter.Name);
56+
if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)))
57+
{
58+
AssigningCode = "httpContext.Request.Form.Files";
59+
}
60+
else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFile)))
61+
{
62+
AssigningCode = $"httpContext.Request.Form.Files[{SymbolDisplay.FormatLiteral(LookupName, true)}]";
63+
}
64+
else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)))
65+
{
66+
AssigningCode = "httpContext.Request.Form";
67+
}
68+
else
69+
{
70+
AssigningCode = $"(string?)httpContext.Request.Form[{SymbolDisplay.FormatLiteral(LookupName, true)}]";
71+
IsParsable = TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter);
72+
ParsingBlockEmitter = parsingBlockEmitter;
73+
}
5574
}
5675
else if (TryGetExplicitFromJsonBody(parameter, wellKnownTypes, out var isOptional))
5776
{
5877
if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.System_IO_Stream)))
5978
{
6079
Source = EndpointParameterSource.SpecialType;
6180
AssigningCode = "httpContext.Request.Body";
62-
6381
}
6482
else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.System_IO_Pipelines_PipeReader)))
6583
{
@@ -85,11 +103,23 @@ public EndpointParameter(Endpoint endpoint, IParameterSymbol parameter, WellKnow
85103
Source = EndpointParameterSource.SpecialType;
86104
AssigningCode = specialTypeAssigningCode;
87105
}
88-
else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFile)) ||
89-
SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)) ||
90-
SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)))
106+
else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)))
91107
{
92-
Source = EndpointParameterSource.Unknown;
108+
Source = EndpointParameterSource.FormBody;
109+
LookupName = parameter.Name;
110+
AssigningCode = "httpContext.Request.Form.Files";
111+
}
112+
else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFile)))
113+
{
114+
Source = EndpointParameterSource.FormBody;
115+
LookupName = parameter.Name;
116+
AssigningCode = $"httpContext.Request.Form.Files[{SymbolDisplay.FormatLiteral(LookupName, true)}]";
117+
}
118+
else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)))
119+
{
120+
Source = EndpointParameterSource.FormBody;
121+
LookupName = parameter.Name;
122+
AssigningCode = "httpContext.Request.Form";
93123
}
94124
else if (HasBindAsync(parameter, wellKnownTypes, out var bindMethod))
95125
{

0 commit comments

Comments
 (0)