Skip to content

Commit 354efd4

Browse files
RDG array handling. (#47086)
* RDG array handling. * Expand diagnostic comment. * React the test base refactoring, and address build failure. * Add test cases for missing/empty query parameters on arrays. * Only use query string when inferring parameters from body is disallowed. * Rebase on main. * Use existing test cases for arrays. * Fix CS8625.: * Update src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs Co-authored-by: Safia Abdalla <[email protected]> * PR feedback from Safia. --------- Co-authored-by: Safia Abdalla <[email protected]>
1 parent 496e249 commit 354efd4

File tree

34 files changed

+3568
-172
lines changed

34 files changed

+3568
-172
lines changed

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

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,18 @@ internal static void EmitQueryOrHeaderParameterPreparation(this EndpointParamete
2626
// otherwise we need to detect whether no value is provided and set the handler argument to null to
2727
// preserve consistency with RDF behavior. We don't want to emit the conditional block to avoid
2828
// compiler errors around null handling.
29-
if (endpointParameter.IsOptional)
29+
if (endpointParameter.IsArray)
30+
{
31+
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()}.ToArray();");
32+
}
33+
else if (endpointParameter.IsOptional)
3034
{
3135
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()}.Count > 0 ? (string?){endpointParameter.EmitAssigningCodeResult()} : null;");
3236
}
37+
else if (endpointParameter.IsStringValues)
38+
{
39+
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()};");
40+
}
3341
else
3442
{
3543
codeWriter.WriteLine($"if (StringValues.IsNullOrEmpty({endpointParameter.EmitAssigningCodeResult()}))");
@@ -44,12 +52,36 @@ internal static void EmitQueryOrHeaderParameterPreparation(this EndpointParamete
4452

4553
internal static void EmitParsingBlock(this EndpointParameter endpointParameter, CodeWriter codeWriter)
4654
{
47-
if (endpointParameter.IsParsable)
55+
if (endpointParameter.IsArray && endpointParameter.IsParsable)
56+
{
57+
codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)} {endpointParameter.EmitHandlerArgument()} = new {endpointParameter.ElementType.ToDisplayString(EmitterConstants.DisplayFormat)}[{endpointParameter.EmitTempArgument()}.Length];");
58+
codeWriter.WriteLine($"for (var i = 0; i < {endpointParameter.EmitTempArgument()}.Length; i++)");
59+
codeWriter.StartBlock();
60+
codeWriter.WriteLine($"var element = {endpointParameter.EmitTempArgument()}[i];");
61+
endpointParameter.ParsingBlockEmitter(codeWriter, "element", "parsed_element");
62+
63+
// In cases where we are dealing with an array of parsable nullables we need to substitute
64+
// empty strings for null values.
65+
if (endpointParameter.ElementType.NullableAnnotation == NullableAnnotation.Annotated)
66+
{
67+
codeWriter.WriteLine($"{endpointParameter.EmitHandlerArgument()}[i] = string.IsNullOrEmpty(element) ? null! : parsed_element!;");
68+
}
69+
else
70+
{
71+
codeWriter.WriteLine($"{endpointParameter.EmitHandlerArgument()}[i] = parsed_element!;");
72+
}
73+
codeWriter.EndBlock();
74+
}
75+
else if (endpointParameter.IsArray && !endpointParameter.IsParsable)
76+
{
77+
codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitTempArgument()}!;");
78+
}
79+
else if (!endpointParameter.IsArray && endpointParameter.IsParsable)
4880
{
4981
endpointParameter.ParsingBlockEmitter(codeWriter, endpointParameter.EmitTempArgument(), endpointParameter.EmitParsedTempArgument());
5082
codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitParsedTempArgument()}!;");
5183
}
52-
else
84+
else // Not parsable, not an array.
5385
{
5486
codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitTempArgument()}!;");
5587
}
@@ -88,15 +120,23 @@ internal static void EmitRouteOrQueryParameterPreparation(this EndpointParameter
88120
var parameterName = endpointParameter.Name;
89121
codeWriter.WriteLine($"var {endpointParameter.EmitAssigningCodeResult()} = {parameterName}_RouteOrQueryResolver(httpContext);");
90122

91-
if (!endpointParameter.IsOptional)
123+
if (endpointParameter.IsArray)
124+
{
125+
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()}.ToArray();");
126+
}
127+
else if (endpointParameter.IsOptional)
128+
{
129+
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = {endpointParameter.EmitAssigningCodeResult()}.Count > 0 ? (string?){endpointParameter.EmitAssigningCodeResult()} : null;");
130+
}
131+
else
92132
{
93133
codeWriter.WriteLine($"if ({endpointParameter.EmitAssigningCodeResult()} is StringValues {{ Count: 0 }})");
94134
codeWriter.StartBlock();
95135
codeWriter.WriteLine("wasParamCheckFailure = true;");
96136
codeWriter.EndBlock();
137+
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = (string?){endpointParameter.EmitAssigningCodeResult()};");
97138
}
98139

99-
codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = (string?){endpointParameter.EmitAssigningCodeResult()};");
100140
endpointParameter.EmitParsingBlock(codeWriter);
101141
}
102142

@@ -144,7 +184,7 @@ internal static void EmitJsonBodyOrServiceParameterPreparationString(this Endpoi
144184

145185
internal static void EmitBindAsyncPreparation(this EndpointParameter endpointParameter, CodeWriter codeWriter)
146186
{
147-
var unwrappedType = endpointParameter.Type.UnwrapTypeSymbol();
187+
var unwrappedType = endpointParameter.Type.UnwrapTypeSymbol(unwrapNullable: true);
148188
var unwrappedTypeString = unwrappedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
149189

150190
switch (endpointParameter.BindMethod)
@@ -197,7 +237,7 @@ internal static void EmitServiceParameterPreparation(this EndpointParameter endp
197237
codeWriter.WriteLine($"var {endpointParameter.EmitHandlerArgument()} = {assigningCode};");
198238
}
199239

200-
private static string EmitParameterDiagnosticComment(this EndpointParameter endpointParameter) => $"// Endpoint Parameter: {endpointParameter.Name} (Type = {endpointParameter.Type}, IsOptional = {endpointParameter.IsOptional}, IsParsable = {endpointParameter.IsParsable}, Source = {endpointParameter.Source})";
240+
private static string EmitParameterDiagnosticComment(this EndpointParameter endpointParameter) => $"// Endpoint Parameter: {endpointParameter.Name} (Type = {endpointParameter.Type}, IsOptional = {endpointParameter.IsOptional}, IsParsable = {endpointParameter.IsParsable}, IsArray = {endpointParameter.IsArray}, Source = {endpointParameter.Source})";
201241
private static string EmitHandlerArgument(this EndpointParameter endpointParameter) => $"{endpointParameter.Name}_local";
202242
private static string EmitTempArgument(this EndpointParameter endpointParameter) => $"{endpointParameter.Name}_temp";
203243

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes, S
5555

5656
for (var i = 0; i < method.Parameters.Length; i++)
5757
{
58-
var parameter = new EndpointParameter(method.Parameters[i], wellKnownTypes);
58+
var parameter = new EndpointParameter(this, method.Parameters[i], wellKnownTypes);
5959

6060
switch (parameter.Source)
6161
{

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

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
1313

1414
internal class EndpointParameter
1515
{
16-
public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTypes)
16+
public EndpointParameter(Endpoint endpoint, IParameterSymbol parameter, WellKnownTypes wellKnownTypes)
1717
{
1818
Type = parameter.Type;
1919
Name = parameter.Name;
2020
Ordinal = parameter.Ordinal;
2121
Source = EndpointParameterSource.Unknown;
2222
IsOptional = parameter.IsOptional();
23+
IsArray = TryGetArrayElementType(parameter, out var elementType);
24+
ElementType = elementType;
2325

2426
if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata), out var fromRouteAttribute))
2527
{
@@ -79,8 +81,8 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
7981
AssigningCode = specialTypeAssigningCode;
8082
}
8183
else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFile)) ||
82-
SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)) ||
83-
SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)))
84+
SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)) ||
85+
SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)))
8486
{
8587
Source = EndpointParameterSource.Unknown;
8688
}
@@ -93,6 +95,15 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
9395
{
9496
Source = EndpointParameterSource.RouteOrQuery;
9597
}
98+
else if (ShouldDisableInferredBodyParameters(endpoint.HttpMethod) && IsArray && elementType.SpecialType == SpecialType.System_String)
99+
{
100+
Source = EndpointParameterSource.Query;
101+
}
102+
else if (ShouldDisableInferredBodyParameters(endpoint.HttpMethod) && SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_Extensions_Primitives_StringValues)))
103+
{
104+
Source = EndpointParameterSource.Query;
105+
IsStringValues = true;
106+
}
96107
else if (TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter))
97108
{
98109
Source = EndpointParameterSource.RouteOrQuery;
@@ -105,10 +116,24 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
105116
}
106117
}
107118

119+
private static bool ShouldDisableInferredBodyParameters(string httpMethod)
120+
{
121+
switch (httpMethod)
122+
{
123+
case "MapPut" or "MapPatch" or "MapPost":
124+
return false;
125+
default:
126+
return true;
127+
}
128+
}
129+
108130
public ITypeSymbol Type { get; }
131+
public ITypeSymbol ElementType { get; }
132+
109133
public string Name { get; }
110134
public int Ordinal { get; }
111135
public bool IsOptional { get; }
136+
public bool IsArray { get; set; }
112137

113138
public EndpointParameterSource Source { get; }
114139

@@ -119,18 +144,33 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
119144
[MemberNotNullWhen(true, nameof(ParsingBlockEmitter))]
120145
public bool IsParsable { get; }
121146
public Action<CodeWriter, string, string>? ParsingBlockEmitter { get; }
147+
public bool IsStringValues { get; }
122148

123149
public BindabilityMethod? BindMethod { get; }
124150

125151
private static bool HasBindAsync(IParameterSymbol parameter, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out BindabilityMethod? bindMethod)
126152
{
127-
var parameterType = parameter.Type.UnwrapTypeSymbol();
153+
var parameterType = parameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true);
128154
return ParsabilityHelper.GetBindability(parameterType, wellKnownTypes, out bindMethod) == Bindability.Bindable;
129155
}
130156

157+
private static bool TryGetArrayElementType(IParameterSymbol parameter, [NotNullWhen(true)]out ITypeSymbol elementType)
158+
{
159+
if (parameter.Type.TypeKind == TypeKind.Array)
160+
{
161+
elementType = parameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: false);
162+
return true;
163+
}
164+
else
165+
{
166+
elementType = null!;
167+
return false;
168+
}
169+
}
170+
131171
private bool TryGetParsability(IParameterSymbol parameter, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out Action<CodeWriter, string, string>? parsingBlockEmitter)
132172
{
133-
var parameterType = parameter.Type.UnwrapTypeSymbol();
173+
var parameterType = parameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true);
134174

135175
// ParsabilityHelper returns a single enumeration with a Parsable/NonParsable enumeration result. We use this already
136176
// in the analyzers to determine whether we need to warn on whether a type needs to implement TryParse/IParsable<T>. To
@@ -205,10 +245,23 @@ private bool TryGetParsability(IParameterSymbol parameter, WellKnownTypes wellKn
205245
{
206246
parsingBlockEmitter = (writer, inputArgument, outputArgument) =>
207247
{
208-
writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})""");
209-
writer.StartBlock();
210-
writer.WriteLine("wasParamCheckFailure = true;");
211-
writer.EndBlock();
248+
if (IsArray && ElementType.NullableAnnotation == NullableAnnotation.Annotated)
249+
{
250+
writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})""");
251+
writer.StartBlock();
252+
writer.WriteLine($$"""if (!string.IsNullOrEmpty({{inputArgument}}))""");
253+
writer.StartBlock();
254+
writer.WriteLine("wasParamCheckFailure = true;");
255+
writer.EndBlock();
256+
writer.EndBlock();
257+
}
258+
else
259+
{
260+
writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})""");
261+
writer.StartBlock();
262+
writer.WriteLine("wasParamCheckFailure = true;");
263+
writer.EndBlock();
264+
}
212265
};
213266
}
214267

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,11 @@ public static string EmitFilteredArgumentList(this Endpoint endpoint)
205205

206206
for (var i = 0; i < endpoint.Parameters.Length; i++)
207207
{
208-
sb.Append($"ic.GetArgument<{endpoint.Parameters[i].Type.ToDisplayString(EmitterConstants.DisplayFormat)}>({i})");
208+
// The null suppression operator on the GetArgument(...) call here is required because we'll occassionally be
209+
// dealing with nullable types here. We could try to do fancy things to branch the logic here depending on
210+
// the nullability, but at the end of the day we are going to call GetArguments(...) - at runtime the nullability
211+
// suppression operator doesn't come into play - so its not worth worrying about.
212+
sb.Append($"ic.GetArgument<{endpoint.Parameters[i].Type.ToDisplayString(EmitterConstants.DisplayFormat)}>({i})!");
209213

210214
if (i < endpoint.Parameters.Length - 1)
211215
{

0 commit comments

Comments
 (0)