Skip to content

Commit 3f5cb0e

Browse files
committed
EndpointParameter.ParsingBlockEmitter property lifted out into EndpointParameterEmitter.EmitParsingBlock to be able to properly emit parsing logic for optional arrays.
1 parent 08b60af commit 3f5cb0e

File tree

3 files changed

+94
-71
lines changed

3 files changed

+94
-71
lines changed

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

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,30 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter,
9191
{
9292
if (endpointParameter.IsArray && endpointParameter.IsParsable)
9393
{
94-
codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)} {endpointParameter.EmitHandlerArgument()} = new {endpointParameter.ElementType.ToDisplayString(EmitterConstants.DisplayFormat)}[{endpointParameter.EmitTempArgument()}.Length];");
94+
var createArray = $"new {endpointParameter.ElementType.ToDisplayString(EmitterConstants.DisplayFormat)}[{endpointParameter.EmitTempArgument()}.Length]";
95+
96+
// we assign a null to result parameter if it's optional array, otherwise we create new array immediately
97+
codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)} {endpointParameter.EmitHandlerArgument()} = {(endpointParameter.IsOptional ? "null" : createArray)};");
98+
9599
codeWriter.WriteLine($"for (var i = 0; i < {endpointParameter.EmitTempArgument()}.Length; i++)");
96100
codeWriter.StartBlock();
97101
codeWriter.WriteLine($"var element = {endpointParameter.EmitTempArgument()}[i];");
98-
endpointParameter.ParsingBlockEmitter(codeWriter, "element", "parsed_element");
102+
103+
//endpointParameter.ParsingBlockEmitter(codeWriter, "element", "parsed_element");
104+
codeWriter.WriteLine($$"""if (!{{endpointParameter.PreferredTryParseInvocation("element", "parsed_element")}})""");
105+
codeWriter.StartBlock();
106+
codeWriter.WriteLine("if (!string.IsNullOrEmpty(element))");
107+
codeWriter.StartBlock();
108+
codeWriter.WriteLine("wasParamCheckFailure = true;");
109+
EmitLogOrThrowException(endpointParameter, codeWriter, "element");
110+
codeWriter.EndBlock();
111+
codeWriter.EndBlock();
112+
113+
// In case we have optional parameter, we emit array assignment
114+
if (endpointParameter.IsOptional)
115+
{
116+
codeWriter.WriteLine($$"""{{endpointParameter.EmitHandlerArgument()}} ??= {{createArray}};""");
117+
}
99118

100119
// In cases where we are dealing with an array of parsable nullables we need to substitute
101120
// empty strings for null values.
@@ -115,13 +134,59 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter,
115134
}
116135
else if (!endpointParameter.IsArray && endpointParameter.IsParsable)
117136
{
118-
endpointParameter.ParsingBlockEmitter(codeWriter, endpointParameter.EmitTempArgument(), endpointParameter.EmitParsedTempArgument());
137+
var temp_argument = endpointParameter.EmitTempArgument();
138+
var output_argument = endpointParameter.EmitParsedTempArgument();
139+
140+
//endpointParameter.ParsingBlockEmitter(codeWriter, endpointParameter.EmitTempArgument(), endpointParameter.EmitParsedTempArgument());
141+
if (endpointParameter.IsOptional)
142+
{
143+
var parameterType = endpointParameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true);
144+
var temp_argument_parsed_non_nullable = $"{temp_argument}_parsed_non_nullable";
145+
146+
codeWriter.WriteLine($"""{parameterType.ToDisplayString(EmitterConstants.DisplayFormat)} {output_argument} = default;""");
147+
codeWriter.WriteLine($"""if ({endpointParameter.PreferredTryParseInvocation(temp_argument, temp_argument_parsed_non_nullable)})""");
148+
codeWriter.StartBlock();
149+
codeWriter.WriteLine($"""{output_argument} = {temp_argument_parsed_non_nullable};""");
150+
codeWriter.EndBlock();
151+
codeWriter.WriteLine($"""else if (string.IsNullOrEmpty({temp_argument}))""");
152+
codeWriter.StartBlock();
153+
codeWriter.WriteLine($"""{output_argument} = {endpointParameter.DefaultValue};""");
154+
codeWriter.EndBlock();
155+
codeWriter.WriteLine("else");
156+
codeWriter.EndBlock();
157+
codeWriter.WriteLine("wasParamCheckFailure = true;");
158+
codeWriter.StartBlock();
159+
}
160+
else
161+
{
162+
codeWriter.WriteLine($$"""if (!{{endpointParameter.PreferredTryParseInvocation(temp_argument, output_argument)}})""");
163+
codeWriter.StartBlock();
164+
codeWriter.WriteLine($"if (!string.IsNullOrEmpty({temp_argument}))");
165+
codeWriter.StartBlock();
166+
codeWriter.WriteLine("wasParamCheckFailure = true;");
167+
EmitLogOrThrowException(endpointParameter, codeWriter, temp_argument);
168+
codeWriter.EndBlock();
169+
codeWriter.EndBlock();
170+
}
171+
119172
codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitParsedTempArgument()}!;");
120173
}
121174
else // Not parsable, not an array.
122175
{
123176
codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitTempArgument()}!;");
124177
}
178+
179+
static void EmitLogOrThrowException(EndpointParameter parameter, CodeWriter writer, string inputArgument)
180+
{
181+
if (parameter.IsArray && parameter.ElementType.NullableAnnotation == NullableAnnotation.Annotated)
182+
{
183+
writer.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(parameter.SymbolName, true)}, {SymbolDisplay.FormatLiteral(parameter.ToMessageString(), true)});");
184+
}
185+
else
186+
{
187+
writer.WriteLine($@"logOrThrowExceptionHelper.ParameterBindingFailed({SymbolDisplay.FormatLiteral(parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(parameter.SymbolName, true)}, {inputArgument});");
188+
}
189+
}
125190
}
126191

127192
internal static void EmitRouteParameterPreparation(this EndpointParameter endpointParameter, CodeWriter codeWriter)

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

Lines changed: 17 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using Microsoft.AspNetCore.Analyzers.Infrastructure;
1111
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
1212
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
13-
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel.Emitters;
1413
using Microsoft.CodeAnalysis;
1514
using Microsoft.CodeAnalysis.CSharp;
1615
using WellKnownType = Microsoft.AspNetCore.App.Analyzers.Infrastructure.WellKnownTypeData.WellKnownType;
@@ -19,7 +18,7 @@ namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerM
1918

2019
internal class EndpointParameter
2120
{
22-
public EndpointParameter(Endpoint endpoint, IParameterSymbol parameter, WellKnownTypes wellKnownTypes): this(endpoint, parameter.Type, parameter.Name, wellKnownTypes)
21+
public EndpointParameter(Endpoint endpoint, IParameterSymbol parameter, WellKnownTypes wellKnownTypes) : this(endpoint, parameter.Type, parameter.Name, wellKnownTypes)
2322
{
2423
Ordinal = parameter.Ordinal;
2524
IsOptional = parameter.IsOptional();
@@ -71,22 +70,22 @@ private void ProcessEndpointParameterSource(Endpoint endpoint, ISymbol symbol, I
7170
{
7271
Source = EndpointParameterSource.Route;
7372
LookupName = GetEscapedParameterName(fromRouteAttribute, symbol.Name);
74-
IsParsable = TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter);
75-
ParsingBlockEmitter = parsingBlockEmitter;
73+
IsParsable = TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation);
74+
PreferredTryParseInvocation = preferredTryParseInvocation;
7675
}
7776
else if (attributes.TryGetAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata), out var fromQueryAttribute))
7877
{
7978
Source = EndpointParameterSource.Query;
8079
LookupName = GetEscapedParameterName(fromQueryAttribute, symbol.Name);
81-
IsParsable = TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter);
82-
ParsingBlockEmitter = parsingBlockEmitter;
80+
IsParsable = TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation);
81+
PreferredTryParseInvocation = preferredTryParseInvocation;
8382
}
8483
else if (attributes.TryGetAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata), out var fromHeaderAttribute))
8584
{
8685
Source = EndpointParameterSource.Header;
8786
LookupName = GetEscapedParameterName(fromHeaderAttribute, symbol.Name);
88-
IsParsable = TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter);
89-
ParsingBlockEmitter = parsingBlockEmitter;
87+
IsParsable = TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation);
88+
PreferredTryParseInvocation = preferredTryParseInvocation;
9089
}
9190
else if (attributes.TryGetAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata), out var fromFormAttribute))
9291
{
@@ -121,8 +120,8 @@ private void ProcessEndpointParameterSource(Endpoint endpoint, ISymbol symbol, I
121120
AssigningCode = !IsArray
122121
? $"(string?)httpContext.Request.Form[{SymbolDisplay.FormatLiteral(LookupName, true)}]"
123122
: $"httpContext.Request.Form[{SymbolDisplay.FormatLiteral(LookupName, true)}].ToArray()";
124-
IsParsable = TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter);
125-
ParsingBlockEmitter = parsingBlockEmitter;
123+
IsParsable = TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation);
124+
PreferredTryParseInvocation = preferredTryParseInvocation;
126125
}
127126
}
128127
else if (TryGetExplicitFromJsonBody(symbol, attributes, wellKnownTypes, out var isOptional))
@@ -237,11 +236,11 @@ Type is not INamedTypeSymbol namedTypeSymbol ||
237236
Source = EndpointParameterSource.Query;
238237
IsStringValues = true;
239238
}
240-
else if (TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter))
239+
else if (TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation))
241240
{
242241
Source = EndpointParameterSource.RouteOrQuery;
243242
IsParsable = true;
244-
ParsingBlockEmitter = parsingBlockEmitter;
243+
PreferredTryParseInvocation = preferredTryParseInvocation;
245244
}
246245
else
247246
{
@@ -293,9 +292,9 @@ private static bool ImplementsIEndpointParameterMetadataProvider(ITypeSymbol typ
293292
// to be resolved by a specific WellKnownType
294293
public string? AssigningCode { get; set; }
295294

296-
[MemberNotNullWhen(true, nameof(ParsingBlockEmitter))]
295+
[MemberNotNullWhen(true, nameof(PreferredTryParseInvocation))]
297296
public bool IsParsable { get; set; }
298-
public Action<CodeWriter, string, string>? ParsingBlockEmitter { get; set; }
297+
public Func<string, string, string>? PreferredTryParseInvocation { get; set; }
299298
public bool IsStringValues { get; set; }
300299

301300
public BindabilityMethod? BindMethod { get; set; }
@@ -307,7 +306,7 @@ private static bool HasBindAsync(ITypeSymbol typeSymbol, WellKnownTypes wellKnow
307306
return ParsabilityHelper.GetBindability(parameterType, wellKnownTypes, out bindMethod, out bindMethodSymbol) == Bindability.Bindable;
308307
}
309308

310-
private static bool TryGetArrayElementType(ITypeSymbol type, [NotNullWhen(true)]out ITypeSymbol elementType)
309+
private static bool TryGetArrayElementType(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol elementType)
311310
{
312311
if (type.TypeKind == TypeKind.Array)
313312
{
@@ -321,7 +320,7 @@ private static bool TryGetArrayElementType(ITypeSymbol type, [NotNullWhen(true)]
321320
}
322321
}
323322

324-
private bool TryGetParsability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out Action<CodeWriter, string, string>? parsingBlockEmitter)
323+
private bool TryGetParsability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out Func<string, string, string>? preferredTryParseInvocation)
325324
{
326325
var parameterType = typeSymbol.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true);
327326

@@ -332,14 +331,14 @@ private bool TryGetParsability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownT
332331
// parsable at all we bail.
333332
if (ParsabilityHelper.GetParsability(parameterType, wellKnownTypes, out var parsabilityMethod) != Parsability.Parsable)
334333
{
335-
parsingBlockEmitter = null;
334+
preferredTryParseInvocation = null;
336335
return false;
337336
}
338337

339338
// If we are parsable we need to emit code based on the enumeration ParsabilityMethod which has a bunch of members
340339
// which spell out the preferred TryParse usage. This switch statement makes slight variations to them based on
341340
// which method was encountered.
342-
Func<string, string, string>? preferredTryParseInvocation = parsabilityMethod switch
341+
preferredTryParseInvocation = parsabilityMethod switch
343342
{
344343
ParsabilityMethod.IParsable => (string inputArgument, string outputArgument) => $$"""GeneratedRouteBuilderExtensionsCore.TryParseExplicit<{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}>({{inputArgument}}!, CultureInfo.InvariantCulture, out var {{outputArgument}})""",
345344
ParsabilityMethod.TryParseWithFormatProvider => (string inputArgument, string outputArgument) => $$"""{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}.TryParse({{inputArgument}}!, CultureInfo.InvariantCulture, out var {{outputArgument}})""",
@@ -371,58 +370,9 @@ private bool TryGetParsability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownT
371370
// ... so for strings (null) we bail.
372371
if (preferredTryParseInvocation == null)
373372
{
374-
parsingBlockEmitter = null;
375373
return false;
376374
}
377375

378-
if (IsOptional)
379-
{
380-
parsingBlockEmitter = (writer, inputArgument, outputArgument) =>
381-
{
382-
writer.WriteLine($"""{typeSymbol.ToDisplayString(EmitterConstants.DisplayFormat)} {outputArgument} = default;""");
383-
writer.WriteLine($$"""if ({{preferredTryParseInvocation(inputArgument, $"{inputArgument}_parsed_non_nullable")}})""");
384-
writer.StartBlock();
385-
writer.WriteLine($$"""{{outputArgument}} = {{$"{inputArgument}_parsed_non_nullable"}};""");
386-
writer.EndBlock();
387-
writer.WriteLine($$"""else if (string.IsNullOrEmpty({{inputArgument}}))""");
388-
writer.StartBlock();
389-
writer.WriteLine($$"""{{outputArgument}} = {{DefaultValue}};""");
390-
writer.EndBlock();
391-
writer.WriteLine("else");
392-
writer.StartBlock();
393-
writer.WriteLine("wasParamCheckFailure = true;");
394-
writer.EndBlock();
395-
};
396-
}
397-
else
398-
{
399-
parsingBlockEmitter = (writer, inputArgument, outputArgument) =>
400-
{
401-
if (IsArray && ElementType.NullableAnnotation == NullableAnnotation.Annotated)
402-
{
403-
writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})""");
404-
writer.StartBlock();
405-
writer.WriteLine($$"""if (!string.IsNullOrEmpty({{inputArgument}}))""");
406-
writer.StartBlock();
407-
writer.WriteLine("wasParamCheckFailure = true;");
408-
writer.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(SymbolName, true)}, {SymbolDisplay.FormatLiteral(this.ToMessageString(), true)});");
409-
writer.EndBlock();
410-
writer.EndBlock();
411-
}
412-
else
413-
{
414-
writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})""");
415-
writer.StartBlock();
416-
writer.WriteLine($"if (!string.IsNullOrEmpty({inputArgument}))");
417-
writer.StartBlock();
418-
writer.WriteLine($@"logOrThrowExceptionHelper.ParameterBindingFailed({SymbolDisplay.FormatLiteral(Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(SymbolName, true)}, {inputArgument});");
419-
writer.WriteLine("wasParamCheckFailure = true;");
420-
writer.EndBlock();
421-
writer.EndBlock();
422-
}
423-
};
424-
}
425-
426376
// Wrap the TryParse method call in an if-block and if it doesn't work set param check failure.
427377
return true;
428378
}

src/Http/samples/MinimalSample/Program.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858

5959
superNested.MapGet("/", (string groupName, string nestedName) =>
6060
{
61-
return $"Hello from {groupName}:{nestedName}!";
61+
return $"Hello from {groupName}:{nestedName}!";
6262
});
6363

6464
object Json() => new { message = "Hello, World!" };
@@ -94,6 +94,14 @@
9494
app.MapPost("/todos", (TodoBindable todo) => todo);
9595
app.MapGet("/todos", () => new Todo[] { new Todo(1, "Walk the dog"), new Todo(2, "Come back home") });
9696

97+
app.MapGet("/array-of-nullables", (int?[] array1) => Results.Ok());
98+
99+
app.MapGet("/array-nullable", (int[]? array2) => Results.Ok());
100+
app.MapGet("/array-nullable-of-nullables", (int?[]? array3) => Results.Ok());
101+
102+
app.MapGet("/array-nullable-optional", IResult (int[]? array4 = null) => Results.Ok());
103+
app.MapGet("/array-nullable-optional-of-nullables", (int?[]? array5 = null) => Results.Ok());
104+
97105
app.Run();
98106

99107
internal record Todo(int Id, string Title);

0 commit comments

Comments
 (0)