From 3f5cb0ed97aaebd47260ed7fb7a2bc92049c796d Mon Sep 17 00:00:00 2001 From: ithline Date: Wed, 14 Aug 2024 16:54:56 +0200 Subject: [PATCH 1/5] `EndpointParameter.ParsingBlockEmitter` property lifted out into `EndpointParameterEmitter.EmitParsingBlock` to be able to properly emit parsing logic for optional arrays. --- .../Emitters/EndpointParameterEmitter.cs | 71 +++++++++++++++- .../EndpointParameter.cs | 84 ++++--------------- src/Http/samples/MinimalSample/Program.cs | 10 ++- 3 files changed, 94 insertions(+), 71 deletions(-) diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs index 3d20278757ae..427ff61d52b8 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs @@ -91,11 +91,30 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, { if (endpointParameter.IsArray && endpointParameter.IsParsable) { - codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)} {endpointParameter.EmitHandlerArgument()} = new {endpointParameter.ElementType.ToDisplayString(EmitterConstants.DisplayFormat)}[{endpointParameter.EmitTempArgument()}.Length];"); + var createArray = $"new {endpointParameter.ElementType.ToDisplayString(EmitterConstants.DisplayFormat)}[{endpointParameter.EmitTempArgument()}.Length]"; + + // we assign a null to result parameter if it's optional array, otherwise we create new array immediately + codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)} {endpointParameter.EmitHandlerArgument()} = {(endpointParameter.IsOptional ? "null" : createArray)};"); + codeWriter.WriteLine($"for (var i = 0; i < {endpointParameter.EmitTempArgument()}.Length; i++)"); codeWriter.StartBlock(); codeWriter.WriteLine($"var element = {endpointParameter.EmitTempArgument()}[i];"); - endpointParameter.ParsingBlockEmitter(codeWriter, "element", "parsed_element"); + + //endpointParameter.ParsingBlockEmitter(codeWriter, "element", "parsed_element"); + codeWriter.WriteLine($$"""if (!{{endpointParameter.PreferredTryParseInvocation("element", "parsed_element")}})"""); + codeWriter.StartBlock(); + codeWriter.WriteLine("if (!string.IsNullOrEmpty(element))"); + codeWriter.StartBlock(); + codeWriter.WriteLine("wasParamCheckFailure = true;"); + EmitLogOrThrowException(endpointParameter, codeWriter, "element"); + codeWriter.EndBlock(); + codeWriter.EndBlock(); + + // In case we have optional parameter, we emit array assignment + if (endpointParameter.IsOptional) + { + codeWriter.WriteLine($$"""{{endpointParameter.EmitHandlerArgument()}} ??= {{createArray}};"""); + } // In cases where we are dealing with an array of parsable nullables we need to substitute // empty strings for null values. @@ -115,13 +134,59 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, } else if (!endpointParameter.IsArray && endpointParameter.IsParsable) { - endpointParameter.ParsingBlockEmitter(codeWriter, endpointParameter.EmitTempArgument(), endpointParameter.EmitParsedTempArgument()); + var temp_argument = endpointParameter.EmitTempArgument(); + var output_argument = endpointParameter.EmitParsedTempArgument(); + + //endpointParameter.ParsingBlockEmitter(codeWriter, endpointParameter.EmitTempArgument(), endpointParameter.EmitParsedTempArgument()); + if (endpointParameter.IsOptional) + { + var parameterType = endpointParameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true); + var temp_argument_parsed_non_nullable = $"{temp_argument}_parsed_non_nullable"; + + codeWriter.WriteLine($"""{parameterType.ToDisplayString(EmitterConstants.DisplayFormat)} {output_argument} = default;"""); + codeWriter.WriteLine($"""if ({endpointParameter.PreferredTryParseInvocation(temp_argument, temp_argument_parsed_non_nullable)})"""); + codeWriter.StartBlock(); + codeWriter.WriteLine($"""{output_argument} = {temp_argument_parsed_non_nullable};"""); + codeWriter.EndBlock(); + codeWriter.WriteLine($"""else if (string.IsNullOrEmpty({temp_argument}))"""); + codeWriter.StartBlock(); + codeWriter.WriteLine($"""{output_argument} = {endpointParameter.DefaultValue};"""); + codeWriter.EndBlock(); + codeWriter.WriteLine("else"); + codeWriter.EndBlock(); + codeWriter.WriteLine("wasParamCheckFailure = true;"); + codeWriter.StartBlock(); + } + else + { + codeWriter.WriteLine($$"""if (!{{endpointParameter.PreferredTryParseInvocation(temp_argument, output_argument)}})"""); + codeWriter.StartBlock(); + codeWriter.WriteLine($"if (!string.IsNullOrEmpty({temp_argument}))"); + codeWriter.StartBlock(); + codeWriter.WriteLine("wasParamCheckFailure = true;"); + EmitLogOrThrowException(endpointParameter, codeWriter, temp_argument); + codeWriter.EndBlock(); + codeWriter.EndBlock(); + } + codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitParsedTempArgument()}!;"); } else // Not parsable, not an array. { codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitTempArgument()}!;"); } + + static void EmitLogOrThrowException(EndpointParameter parameter, CodeWriter writer, string inputArgument) + { + if (parameter.IsArray && parameter.ElementType.NullableAnnotation == NullableAnnotation.Annotated) + { + writer.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(parameter.SymbolName, true)}, {SymbolDisplay.FormatLiteral(parameter.ToMessageString(), true)});"); + } + else + { + writer.WriteLine($@"logOrThrowExceptionHelper.ParameterBindingFailed({SymbolDisplay.FormatLiteral(parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(parameter.SymbolName, true)}, {inputArgument});"); + } + } } internal static void EmitRouteParameterPreparation(this EndpointParameter endpointParameter, CodeWriter codeWriter) diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs index 1eca598bc655..8bea8558fea4 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Analyzers.Infrastructure; using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; -using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel.Emitters; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using WellKnownType = Microsoft.AspNetCore.App.Analyzers.Infrastructure.WellKnownTypeData.WellKnownType; @@ -19,7 +18,7 @@ namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerM internal class EndpointParameter { - public EndpointParameter(Endpoint endpoint, IParameterSymbol parameter, WellKnownTypes wellKnownTypes): this(endpoint, parameter.Type, parameter.Name, wellKnownTypes) + public EndpointParameter(Endpoint endpoint, IParameterSymbol parameter, WellKnownTypes wellKnownTypes) : this(endpoint, parameter.Type, parameter.Name, wellKnownTypes) { Ordinal = parameter.Ordinal; IsOptional = parameter.IsOptional(); @@ -71,22 +70,22 @@ private void ProcessEndpointParameterSource(Endpoint endpoint, ISymbol symbol, I { Source = EndpointParameterSource.Route; LookupName = GetEscapedParameterName(fromRouteAttribute, symbol.Name); - IsParsable = TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter); - ParsingBlockEmitter = parsingBlockEmitter; + IsParsable = TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation); + PreferredTryParseInvocation = preferredTryParseInvocation; } else if (attributes.TryGetAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata), out var fromQueryAttribute)) { Source = EndpointParameterSource.Query; LookupName = GetEscapedParameterName(fromQueryAttribute, symbol.Name); - IsParsable = TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter); - ParsingBlockEmitter = parsingBlockEmitter; + IsParsable = TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation); + PreferredTryParseInvocation = preferredTryParseInvocation; } else if (attributes.TryGetAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata), out var fromHeaderAttribute)) { Source = EndpointParameterSource.Header; LookupName = GetEscapedParameterName(fromHeaderAttribute, symbol.Name); - IsParsable = TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter); - ParsingBlockEmitter = parsingBlockEmitter; + IsParsable = TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation); + PreferredTryParseInvocation = preferredTryParseInvocation; } else if (attributes.TryGetAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata), out var fromFormAttribute)) { @@ -121,8 +120,8 @@ private void ProcessEndpointParameterSource(Endpoint endpoint, ISymbol symbol, I AssigningCode = !IsArray ? $"(string?)httpContext.Request.Form[{SymbolDisplay.FormatLiteral(LookupName, true)}]" : $"httpContext.Request.Form[{SymbolDisplay.FormatLiteral(LookupName, true)}].ToArray()"; - IsParsable = TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter); - ParsingBlockEmitter = parsingBlockEmitter; + IsParsable = TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation); + PreferredTryParseInvocation = preferredTryParseInvocation; } } else if (TryGetExplicitFromJsonBody(symbol, attributes, wellKnownTypes, out var isOptional)) @@ -237,11 +236,11 @@ Type is not INamedTypeSymbol namedTypeSymbol || Source = EndpointParameterSource.Query; IsStringValues = true; } - else if (TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter)) + else if (TryGetParsability(Type, wellKnownTypes, out var preferredTryParseInvocation)) { Source = EndpointParameterSource.RouteOrQuery; IsParsable = true; - ParsingBlockEmitter = parsingBlockEmitter; + PreferredTryParseInvocation = preferredTryParseInvocation; } else { @@ -293,9 +292,9 @@ private static bool ImplementsIEndpointParameterMetadataProvider(ITypeSymbol typ // to be resolved by a specific WellKnownType public string? AssigningCode { get; set; } - [MemberNotNullWhen(true, nameof(ParsingBlockEmitter))] + [MemberNotNullWhen(true, nameof(PreferredTryParseInvocation))] public bool IsParsable { get; set; } - public Action? ParsingBlockEmitter { get; set; } + public Func? PreferredTryParseInvocation { get; set; } public bool IsStringValues { get; set; } public BindabilityMethod? BindMethod { get; set; } @@ -307,7 +306,7 @@ private static bool HasBindAsync(ITypeSymbol typeSymbol, WellKnownTypes wellKnow return ParsabilityHelper.GetBindability(parameterType, wellKnownTypes, out bindMethod, out bindMethodSymbol) == Bindability.Bindable; } - private static bool TryGetArrayElementType(ITypeSymbol type, [NotNullWhen(true)]out ITypeSymbol elementType) + private static bool TryGetArrayElementType(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol elementType) { if (type.TypeKind == TypeKind.Array) { @@ -321,7 +320,7 @@ private static bool TryGetArrayElementType(ITypeSymbol type, [NotNullWhen(true)] } } - private bool TryGetParsability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out Action? parsingBlockEmitter) + private bool TryGetParsability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out Func? preferredTryParseInvocation) { var parameterType = typeSymbol.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true); @@ -332,14 +331,14 @@ private bool TryGetParsability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownT // parsable at all we bail. if (ParsabilityHelper.GetParsability(parameterType, wellKnownTypes, out var parsabilityMethod) != Parsability.Parsable) { - parsingBlockEmitter = null; + preferredTryParseInvocation = null; return false; } // If we are parsable we need to emit code based on the enumeration ParsabilityMethod which has a bunch of members // which spell out the preferred TryParse usage. This switch statement makes slight variations to them based on // which method was encountered. - Func? preferredTryParseInvocation = parsabilityMethod switch + preferredTryParseInvocation = parsabilityMethod switch { ParsabilityMethod.IParsable => (string inputArgument, string outputArgument) => $$"""GeneratedRouteBuilderExtensionsCore.TryParseExplicit<{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}>({{inputArgument}}!, CultureInfo.InvariantCulture, out var {{outputArgument}})""", 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 // ... so for strings (null) we bail. if (preferredTryParseInvocation == null) { - parsingBlockEmitter = null; return false; } - if (IsOptional) - { - parsingBlockEmitter = (writer, inputArgument, outputArgument) => - { - writer.WriteLine($"""{typeSymbol.ToDisplayString(EmitterConstants.DisplayFormat)} {outputArgument} = default;"""); - writer.WriteLine($$"""if ({{preferredTryParseInvocation(inputArgument, $"{inputArgument}_parsed_non_nullable")}})"""); - writer.StartBlock(); - writer.WriteLine($$"""{{outputArgument}} = {{$"{inputArgument}_parsed_non_nullable"}};"""); - writer.EndBlock(); - writer.WriteLine($$"""else if (string.IsNullOrEmpty({{inputArgument}}))"""); - writer.StartBlock(); - writer.WriteLine($$"""{{outputArgument}} = {{DefaultValue}};"""); - writer.EndBlock(); - writer.WriteLine("else"); - writer.StartBlock(); - writer.WriteLine("wasParamCheckFailure = true;"); - writer.EndBlock(); - }; - } - else - { - parsingBlockEmitter = (writer, inputArgument, outputArgument) => - { - if (IsArray && ElementType.NullableAnnotation == NullableAnnotation.Annotated) - { - writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})"""); - writer.StartBlock(); - writer.WriteLine($$"""if (!string.IsNullOrEmpty({{inputArgument}}))"""); - writer.StartBlock(); - writer.WriteLine("wasParamCheckFailure = true;"); - writer.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(SymbolName, true)}, {SymbolDisplay.FormatLiteral(this.ToMessageString(), true)});"); - writer.EndBlock(); - writer.EndBlock(); - } - else - { - writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})"""); - writer.StartBlock(); - writer.WriteLine($"if (!string.IsNullOrEmpty({inputArgument}))"); - writer.StartBlock(); - writer.WriteLine($@"logOrThrowExceptionHelper.ParameterBindingFailed({SymbolDisplay.FormatLiteral(Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(SymbolName, true)}, {inputArgument});"); - writer.WriteLine("wasParamCheckFailure = true;"); - writer.EndBlock(); - writer.EndBlock(); - } - }; - } - // Wrap the TryParse method call in an if-block and if it doesn't work set param check failure. return true; } diff --git a/src/Http/samples/MinimalSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs index 8d21ab71c671..0e634fb07c0f 100644 --- a/src/Http/samples/MinimalSample/Program.cs +++ b/src/Http/samples/MinimalSample/Program.cs @@ -58,7 +58,7 @@ superNested.MapGet("/", (string groupName, string nestedName) => { - return $"Hello from {groupName}:{nestedName}!"; + return $"Hello from {groupName}:{nestedName}!"; }); object Json() => new { message = "Hello, World!" }; @@ -94,6 +94,14 @@ app.MapPost("/todos", (TodoBindable todo) => todo); app.MapGet("/todos", () => new Todo[] { new Todo(1, "Walk the dog"), new Todo(2, "Come back home") }); +app.MapGet("/array-of-nullables", (int?[] array1) => Results.Ok()); + +app.MapGet("/array-nullable", (int[]? array2) => Results.Ok()); +app.MapGet("/array-nullable-of-nullables", (int?[]? array3) => Results.Ok()); + +app.MapGet("/array-nullable-optional", IResult (int[]? array4 = null) => Results.Ok()); +app.MapGet("/array-nullable-optional-of-nullables", (int?[]? array5 = null) => Results.Ok()); + app.Run(); internal record Todo(int Id, string Title); From cf3710e20aa11335c23aba337a71ad5a7e41a0e0 Mon Sep 17 00:00:00 2001 From: ithline Date: Wed, 14 Aug 2024 21:03:13 +0200 Subject: [PATCH 2/5] add array tests and fixed emitter for nullable types --- .../Emitters/EndpointParameterEmitter.cs | 22 +++-- .../RequestDelegateCreationTests.Arrays.cs | 87 +++++++++++++++++++ 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs index 427ff61d52b8..eae97c50ee9e 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs @@ -89,6 +89,7 @@ internal static void EmitFormParameterPreparation(this EndpointParameter endpoin internal static void EmitParsingBlock(this EndpointParameter endpointParameter, CodeWriter codeWriter) { + // parsable array if (endpointParameter.IsArray && endpointParameter.IsParsable) { var createArray = $"new {endpointParameter.ElementType.ToDisplayString(EmitterConstants.DisplayFormat)}[{endpointParameter.EmitTempArgument()}.Length]"; @@ -105,7 +106,6 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, codeWriter.StartBlock(); codeWriter.WriteLine("if (!string.IsNullOrEmpty(element))"); codeWriter.StartBlock(); - codeWriter.WriteLine("wasParamCheckFailure = true;"); EmitLogOrThrowException(endpointParameter, codeWriter, "element"); codeWriter.EndBlock(); codeWriter.EndBlock(); @@ -128,22 +128,24 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, } codeWriter.EndBlock(); } - else if (endpointParameter.IsArray && !endpointParameter.IsParsable) + // array fallback + else if (endpointParameter.IsArray) { codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitTempArgument()}!;"); } - else if (!endpointParameter.IsArray && endpointParameter.IsParsable) + // parsable single + else if (endpointParameter.IsParsable) { var temp_argument = endpointParameter.EmitTempArgument(); var output_argument = endpointParameter.EmitParsedTempArgument(); //endpointParameter.ParsingBlockEmitter(codeWriter, endpointParameter.EmitTempArgument(), endpointParameter.EmitParsedTempArgument()); - if (endpointParameter.IsOptional) + if (endpointParameter.IsOptional || endpointParameter.Type.NullableAnnotation == NullableAnnotation.Annotated) { var parameterType = endpointParameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true); var temp_argument_parsed_non_nullable = $"{temp_argument}_parsed_non_nullable"; - codeWriter.WriteLine($"""{parameterType.ToDisplayString(EmitterConstants.DisplayFormat)} {output_argument} = default;"""); + codeWriter.WriteLine($"""{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)} {output_argument} = default;"""); codeWriter.WriteLine($"""if ({endpointParameter.PreferredTryParseInvocation(temp_argument, temp_argument_parsed_non_nullable)})"""); codeWriter.StartBlock(); codeWriter.WriteLine($"""{output_argument} = {temp_argument_parsed_non_nullable};"""); @@ -153,9 +155,9 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, codeWriter.WriteLine($"""{output_argument} = {endpointParameter.DefaultValue};"""); codeWriter.EndBlock(); codeWriter.WriteLine("else"); - codeWriter.EndBlock(); - codeWriter.WriteLine("wasParamCheckFailure = true;"); codeWriter.StartBlock(); + codeWriter.WriteLine("wasParamCheckFailure = true;"); + codeWriter.EndBlock(); } else { @@ -163,7 +165,6 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, codeWriter.StartBlock(); codeWriter.WriteLine($"if (!string.IsNullOrEmpty({temp_argument}))"); codeWriter.StartBlock(); - codeWriter.WriteLine("wasParamCheckFailure = true;"); EmitLogOrThrowException(endpointParameter, codeWriter, temp_argument); codeWriter.EndBlock(); codeWriter.EndBlock(); @@ -171,7 +172,8 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitParsedTempArgument()}!;"); } - else // Not parsable, not an array. + // Not parsable, not an array. + else { codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitTempArgument()}!;"); } @@ -180,11 +182,13 @@ static void EmitLogOrThrowException(EndpointParameter parameter, CodeWriter writ { if (parameter.IsArray && parameter.ElementType.NullableAnnotation == NullableAnnotation.Annotated) { + writer.WriteLine("wasParamCheckFailure = true;"); writer.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(parameter.SymbolName, true)}, {SymbolDisplay.FormatLiteral(parameter.ToMessageString(), true)});"); } else { writer.WriteLine($@"logOrThrowExceptionHelper.ParameterBindingFailed({SymbolDisplay.FormatLiteral(parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(parameter.SymbolName, true)}, {inputArgument});"); + writer.WriteLine("wasParamCheckFailure = true;"); } } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs index d7bf9bdef497..54faa9e51d7e 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs @@ -10,6 +10,7 @@ using System.Text.Json; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.VisualStudio.TestPlatform.Common; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; @@ -325,6 +326,92 @@ public async Task MapAction_ImplicitQuery_NullableStringArrayParam() await VerifyAgainstBaselineUsingFile(compilation); } + [Fact] + public async Task MapAction_ImplicitQuery_OptionalIntArrayParam() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", (int[]? p = null) => p?.Length ?? 0); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + httpContext.Request.QueryString = new QueryString("?p=0&p=1&p=2"); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "3"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ImplicitQuery_OptionalIntArrayParam_EmptyValues() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", (int[]? p = null) => p?.Length ?? 0); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "0"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ExplicitQuery_OptionalIntArrayParam() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", ([FromQuery] int[]? p = null) => p?.Length ?? 0); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + httpContext.Request.QueryString = new QueryString("?p=0&p=1&p=2"); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "3"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ExplicitQuery_OptionalIntArrayParam_EmptyValues() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", ([FromQuery] int[]? p = null) => p?.Length ?? 0); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "0"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + [Fact] public async Task MapPost_WithArrayQueryString_ShouldFail() { From c61296ed6d3db6546bdd525d2565f1f05d169e93 Mon Sep 17 00:00:00 2001 From: ithline Date: Wed, 14 Aug 2024 21:05:02 +0200 Subject: [PATCH 3/5] fix keyed service test in environments that use comma (`,`) as decimal point, because string interpolation made `12.3` as `12,3` failing the compilation. --- .../RequestDelegateCreationTests.KeyServices.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.KeyServices.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.KeyServices.cs index 32c08cd4e22b..c7dcb131c2c3 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.KeyServices.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.KeyServices.cs @@ -1,5 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Http.RequestDelegateGenerator; using Microsoft.Extensions.DependencyInjection; @@ -99,7 +100,7 @@ public async Task SupportsSingleKeyedServiceWithCharKey() public async Task SupportsSingleKeyedServiceWithPrimitiveKeyTypes(object key) { var source = $$""" -app.MapGet("/", (HttpContext context, [FromKeyedServices({{key.ToString()?.ToLowerInvariant()}})] TestService arg) => context.Items["arg"] = arg); +app.MapGet("/", (HttpContext context, [FromKeyedServices({{Convert.ToString(key, CultureInfo.InvariantCulture)?.ToLowerInvariant()}})] TestService arg) => context.Items["arg"] = arg); """; var (_, compilation) = await RunGeneratorAsync(source); var myOriginalService = new TestService(); From 7d313cb4f303bc9725b42ba4a47355ea58c1f9b4 Mon Sep 17 00:00:00 2001 From: ithline Date: Wed, 14 Aug 2024 23:34:12 +0200 Subject: [PATCH 4/5] add more tests for string optional array align array creation more closely with the reflection delegate factory --- .../Emitters/EndpointParameterEmitter.cs | 14 +- .../RequestDelegateCreationTests.Arrays.cs | 196 ++++++++++++++++-- src/Http/samples/MinimalSample/Program.cs | 8 - 3 files changed, 188 insertions(+), 30 deletions(-) diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs index eae97c50ee9e..e68aeb559e94 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs @@ -95,13 +95,13 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, var createArray = $"new {endpointParameter.ElementType.ToDisplayString(EmitterConstants.DisplayFormat)}[{endpointParameter.EmitTempArgument()}.Length]"; // we assign a null to result parameter if it's optional array, otherwise we create new array immediately - codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)} {endpointParameter.EmitHandlerArgument()} = {(endpointParameter.IsOptional ? "null" : createArray)};"); + codeWriter.WriteLine($"{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)} {endpointParameter.EmitHandlerArgument()} = {createArray};"); codeWriter.WriteLine($"for (var i = 0; i < {endpointParameter.EmitTempArgument()}.Length; i++)"); codeWriter.StartBlock(); codeWriter.WriteLine($"var element = {endpointParameter.EmitTempArgument()}[i];"); - //endpointParameter.ParsingBlockEmitter(codeWriter, "element", "parsed_element"); + // emit parsing block for current array element codeWriter.WriteLine($$"""if (!{{endpointParameter.PreferredTryParseInvocation("element", "parsed_element")}})"""); codeWriter.StartBlock(); codeWriter.WriteLine("if (!string.IsNullOrEmpty(element))"); @@ -110,12 +110,6 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, codeWriter.EndBlock(); codeWriter.EndBlock(); - // In case we have optional parameter, we emit array assignment - if (endpointParameter.IsOptional) - { - codeWriter.WriteLine($$"""{{endpointParameter.EmitHandlerArgument()}} ??= {{createArray}};"""); - } - // In cases where we are dealing with an array of parsable nullables we need to substitute // empty strings for null values. if (endpointParameter.ElementType.NullableAnnotation == NullableAnnotation.Annotated) @@ -139,10 +133,9 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, var temp_argument = endpointParameter.EmitTempArgument(); var output_argument = endpointParameter.EmitParsedTempArgument(); - //endpointParameter.ParsingBlockEmitter(codeWriter, endpointParameter.EmitTempArgument(), endpointParameter.EmitParsedTempArgument()); + // emit parsing block for optional OR nullable values if (endpointParameter.IsOptional || endpointParameter.Type.NullableAnnotation == NullableAnnotation.Annotated) { - var parameterType = endpointParameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true); var temp_argument_parsed_non_nullable = $"{temp_argument}_parsed_non_nullable"; codeWriter.WriteLine($"""{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)} {output_argument} = default;"""); @@ -159,6 +152,7 @@ internal static void EmitParsingBlock(this EndpointParameter endpointParameter, codeWriter.WriteLine("wasParamCheckFailure = true;"); codeWriter.EndBlock(); } + // parsing block for non-nullable required parameters else { codeWriter.WriteLine($$"""if (!{{endpointParameter.PreferredTryParseInvocation(temp_argument, output_argument)}})"""); diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs index 54faa9e51d7e..b10465db36b3 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs @@ -327,10 +327,96 @@ public async Task MapAction_ImplicitQuery_NullableStringArrayParam() } [Fact] - public async Task MapAction_ImplicitQuery_OptionalIntArrayParam() + public async Task MapAction_ImplicitQuery_StringArrayParam_Optional() { var (results, compilation) = await RunGeneratorAsync(""" - app.MapGet("/hello", (int[]? p = null) => p?.Length ?? 0); + app.MapGet("/hello", (string[]? p = null) => p); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + httpContext.Request.QueryString = new QueryString("?p=a&p=b&p=c"); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, """["a","b","c"]"""); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPresent() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", (string[]? p = null) => p); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "[]"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ExplicitQuery_StringArrayParam_Optional() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", ([FromQuery] string[]? p = null) => p); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + httpContext.Request.QueryString = new QueryString("?p=a&p=b&p=c"); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, """["a","b","c"]"""); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPresent() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", ([FromQuery] string[]? p = null) => p); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "[]"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ImplicitQuery_IntArrayParam_Optional() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", (int[]? p = null) => p); """); var endpoint = GetEndpointFromCompilation(compilation); @@ -344,15 +430,15 @@ public async Task MapAction_ImplicitQuery_OptionalIntArrayParam() httpContext.Request.QueryString = new QueryString("?p=0&p=1&p=2"); await endpoint.RequestDelegate(httpContext); - await VerifyResponseBodyAsync(httpContext, "3"); + await VerifyResponseBodyAsync(httpContext, "[0,1,2]"); //await VerifyAgainstBaselineUsingFile(compilation); } [Fact] - public async Task MapAction_ImplicitQuery_OptionalIntArrayParam_EmptyValues() + public async Task MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent() { var (results, compilation) = await RunGeneratorAsync(""" - app.MapGet("/hello", (int[]? p = null) => p?.Length ?? 0); + app.MapGet("/hello", (int[]? p = null) => p); """); var endpoint = GetEndpointFromCompilation(compilation); @@ -365,15 +451,15 @@ public async Task MapAction_ImplicitQuery_OptionalIntArrayParam_EmptyValues() var httpContext = CreateHttpContext(); await endpoint.RequestDelegate(httpContext); - await VerifyResponseBodyAsync(httpContext, "0"); + await VerifyResponseBodyAsync(httpContext, "[]"); //await VerifyAgainstBaselineUsingFile(compilation); } [Fact] - public async Task MapAction_ExplicitQuery_OptionalIntArrayParam() + public async Task MapAction_ExplicitQuery_IntArrayParam_Optional() { var (results, compilation) = await RunGeneratorAsync(""" - app.MapGet("/hello", ([FromQuery] int[]? p = null) => p?.Length ?? 0); + app.MapGet("/hello", ([FromQuery] int[]? p = null) => p); """); var endpoint = GetEndpointFromCompilation(compilation); @@ -387,15 +473,15 @@ public async Task MapAction_ExplicitQuery_OptionalIntArrayParam() httpContext.Request.QueryString = new QueryString("?p=0&p=1&p=2"); await endpoint.RequestDelegate(httpContext); - await VerifyResponseBodyAsync(httpContext, "3"); + await VerifyResponseBodyAsync(httpContext, "[0,1,2]"); //await VerifyAgainstBaselineUsingFile(compilation); } [Fact] - public async Task MapAction_ExplicitQuery_OptionalIntArrayParam_EmptyValues() + public async Task MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent() { var (results, compilation) = await RunGeneratorAsync(""" - app.MapGet("/hello", ([FromQuery] int[]? p = null) => p?.Length ?? 0); + app.MapGet("/hello", ([FromQuery] int[]? p = null) => p); """); var endpoint = GetEndpointFromCompilation(compilation); @@ -408,7 +494,93 @@ public async Task MapAction_ExplicitQuery_OptionalIntArrayParam_EmptyValues() var httpContext = CreateHttpContext(); await endpoint.RequestDelegate(httpContext); - await VerifyResponseBodyAsync(httpContext, "0"); + await VerifyResponseBodyAsync(httpContext, "[]"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ImplicitQuery_NullableIntArrayParam_Optional() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", (int?[]? p = null) => p); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + httpContext.Request.QueryString = new QueryString("?p=0&p=1&p=2"); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "[0,1,2]"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", (int?[]? p = null) => p); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "[]"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ExplicitQuery_NullableIntArrayParam_Optional() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", ([FromQuery] int?[]? p = null) => p); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + httpContext.Request.QueryString = new QueryString("?p=0&p=1&p=2"); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "[0,1,2]"); + //await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent() + { + var (results, compilation) = await RunGeneratorAsync(""" + app.MapGet("/hello", ([FromQuery] int?[]? p = null) => p); + """); + + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(results, endpointModel => + { + Assert.Equal("MapGet", endpointModel.HttpMethod); + }); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, "[]"); //await VerifyAgainstBaselineUsingFile(compilation); } diff --git a/src/Http/samples/MinimalSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs index 0e634fb07c0f..b90c3a3cb48c 100644 --- a/src/Http/samples/MinimalSample/Program.cs +++ b/src/Http/samples/MinimalSample/Program.cs @@ -94,14 +94,6 @@ app.MapPost("/todos", (TodoBindable todo) => todo); app.MapGet("/todos", () => new Todo[] { new Todo(1, "Walk the dog"), new Todo(2, "Come back home") }); -app.MapGet("/array-of-nullables", (int?[] array1) => Results.Ok()); - -app.MapGet("/array-nullable", (int[]? array2) => Results.Ok()); -app.MapGet("/array-nullable-of-nullables", (int?[]? array3) => Results.Ok()); - -app.MapGet("/array-nullable-optional", IResult (int[]? array4 = null) => Results.Ok()); -app.MapGet("/array-nullable-optional-of-nullables", (int?[]? array5 = null) => Results.Ok()); - app.Run(); internal record Todo(int Id, string Title); From 898e8402a70c70bb35f8915263d5b78497319b9a Mon Sep 17 00:00:00 2001 From: ithline Date: Thu, 22 Aug 2024 15:22:59 +0200 Subject: [PATCH 5/5] created baselines for optional array binding tests --- ...Query_IntArrayParam_Optional.generated.txt | 442 +++++++++++++++ ...ram_Optional_QueryNotPresent.generated.txt | 442 +++++++++++++++ ...llableIntArrayParam_Optional.generated.txt | 442 +++++++++++++++ ...ram_Optional_QueryNotPresent.generated.txt | 442 +++++++++++++++ ...ry_StringArrayParam_Optional.generated.txt | 413 ++++++++++++++ ...ram_Optional_QueryNotPresent.generated.txt | 413 ++++++++++++++ ...Query_IntArrayParam_Optional.generated.txt | 449 +++++++++++++++ ...ram_Optional_QueryNotPresent.generated.txt | 449 +++++++++++++++ ...llableIntArrayParam_Optional.generated.txt | 449 +++++++++++++++ ...ram_Optional_QueryNotPresent.generated.txt | 449 +++++++++++++++ ...ry_StringArrayParam_Optional.generated.txt | 510 ++++++++++++++++++ ...ram_Optional_QueryNotPresent.generated.txt | 510 ++++++++++++++++++ .../RequestDelegateCreationTests.Arrays.cs | 24 +- 13 files changed, 5422 insertions(+), 12 deletions(-) create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional.generated.txt create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional.generated.txt new file mode 100644 index 000000000000..f09d01273f7e --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional.generated.txt @@ -0,0 +1,442 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.Int32[] (global::System.Int32[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.Int32[])); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + global::System.Int32[]? p_local = new global::System.Int32[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + logOrThrowExceptionHelper.ParameterBindingFailed("int[]?", "p", element); + wasParamCheckFailure = true; + } + } + p_local[i] = parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + global::System.Int32[]? p_local = new global::System.Int32[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + logOrThrowExceptionHelper.ParameterBindingFailed("int[]?", "p", element); + wasParamCheckFailure = true; + } + } + p_local[i] = parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.Int32[] (global::System.Int32[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static bool TryParseExplicit(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable + => T.TryParse(s, provider, out result); + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt new file mode 100644 index 000000000000..f09d01273f7e --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt @@ -0,0 +1,442 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.Int32[] (global::System.Int32[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.Int32[])); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + global::System.Int32[]? p_local = new global::System.Int32[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + logOrThrowExceptionHelper.ParameterBindingFailed("int[]?", "p", element); + wasParamCheckFailure = true; + } + } + p_local[i] = parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + global::System.Int32[]? p_local = new global::System.Int32[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + logOrThrowExceptionHelper.ParameterBindingFailed("int[]?", "p", element); + wasParamCheckFailure = true; + } + } + p_local[i] = parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.Int32[] (global::System.Int32[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static bool TryParseExplicit(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable + => T.TryParse(s, provider, out result); + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional.generated.txt new file mode 100644 index 000000000000..9075d188acda --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional.generated.txt @@ -0,0 +1,442 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32?[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.Int32?[] (global::System.Int32?[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.Int32?[])); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int?[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + global::System.Int32?[]? p_local = new global::System.Int32?[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + wasParamCheckFailure = true; + logOrThrowExceptionHelper.RequiredParameterNotProvided("int?[]?", "p", "query string"); + } + } + p_local[i] = string.IsNullOrEmpty(element) ? null! : parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int?[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + global::System.Int32?[]? p_local = new global::System.Int32?[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + wasParamCheckFailure = true; + logOrThrowExceptionHelper.RequiredParameterNotProvided("int?[]?", "p", "query string"); + } + } + p_local[i] = string.IsNullOrEmpty(element) ? null! : parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.Int32?[] (global::System.Int32?[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static bool TryParseExplicit(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable + => T.TryParse(s, provider, out result); + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt new file mode 100644 index 000000000000..9075d188acda --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt @@ -0,0 +1,442 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32?[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.Int32?[] (global::System.Int32?[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.Int32?[])); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int?[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + global::System.Int32?[]? p_local = new global::System.Int32?[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + wasParamCheckFailure = true; + logOrThrowExceptionHelper.RequiredParameterNotProvided("int?[]?", "p", "query string"); + } + } + p_local[i] = string.IsNullOrEmpty(element) ? null! : parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int?[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + global::System.Int32?[]? p_local = new global::System.Int32?[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + wasParamCheckFailure = true; + logOrThrowExceptionHelper.RequiredParameterNotProvided("int?[]?", "p", "query string"); + } + } + p_local[i] = string.IsNullOrEmpty(element) ? null! : parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.Int32?[] (global::System.Int32?[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static bool TryParseExplicit(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable + => T.TryParse(s, provider, out result); + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional.generated.txt new file mode 100644 index 000000000000..3e4918ffd63d --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional.generated.txt @@ -0,0 +1,413 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.String[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.String[] (global::System.String[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.String[])); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = string[]?, IsOptional = True, IsParsable = False, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + string[] p_local = p_temp!; + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = string[]?, IsOptional = True, IsParsable = False, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + string[] p_local = p_temp!; + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.String[] (global::System.String[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt new file mode 100644 index 000000000000..3e4918ffd63d --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt @@ -0,0 +1,413 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.String[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.String[] (global::System.String[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.String[])); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = string[]?, IsOptional = True, IsParsable = False, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + string[] p_local = p_temp!; + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = string[]?, IsOptional = True, IsParsable = False, IsArray = True, Source = Query) + var p_raw = httpContext.Request.Query["p"]; + var p_temp = p_raw.ToArray(); + string[] p_local = p_temp!; + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.String[] (global::System.String[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional.generated.txt new file mode 100644 index 000000000000..682208c08c8c --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional.generated.txt @@ -0,0 +1,449 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.Int32[] (global::System.Int32[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.Int32[])); + var p_RouteOrQueryResolver = GeneratedRouteBuilderExtensionsCore.ResolveFromRouteOrQuery("p", options.RouteParameterNames); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = RouteOrQuery) + var p_raw = p_RouteOrQueryResolver(httpContext); + var p_temp = p_raw.ToArray(); + global::System.Int32[]? p_local = new global::System.Int32[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + logOrThrowExceptionHelper.ParameterBindingFailed("int[]?", "p", element); + wasParamCheckFailure = true; + } + } + p_local[i] = parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = RouteOrQuery) + var p_raw = p_RouteOrQueryResolver(httpContext); + var p_temp = p_raw.ToArray(); + global::System.Int32[]? p_local = new global::System.Int32[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + logOrThrowExceptionHelper.ParameterBindingFailed("int[]?", "p", element); + wasParamCheckFailure = true; + } + } + p_local[i] = parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.Int32[] (global::System.Int32[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static Func ResolveFromRouteOrQuery(string parameterName, IEnumerable? routeParameterNames) + { + return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true + ? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName]) + : (httpContext) => httpContext.Request.Query[parameterName]; + } + private static bool TryParseExplicit(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable + => T.TryParse(s, provider, out result); + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt new file mode 100644 index 000000000000..682208c08c8c --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt @@ -0,0 +1,449 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.Int32[] (global::System.Int32[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.Int32[])); + var p_RouteOrQueryResolver = GeneratedRouteBuilderExtensionsCore.ResolveFromRouteOrQuery("p", options.RouteParameterNames); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = RouteOrQuery) + var p_raw = p_RouteOrQueryResolver(httpContext); + var p_temp = p_raw.ToArray(); + global::System.Int32[]? p_local = new global::System.Int32[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + logOrThrowExceptionHelper.ParameterBindingFailed("int[]?", "p", element); + wasParamCheckFailure = true; + } + } + p_local[i] = parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = RouteOrQuery) + var p_raw = p_RouteOrQueryResolver(httpContext); + var p_temp = p_raw.ToArray(); + global::System.Int32[]? p_local = new global::System.Int32[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + logOrThrowExceptionHelper.ParameterBindingFailed("int[]?", "p", element); + wasParamCheckFailure = true; + } + } + p_local[i] = parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.Int32[] (global::System.Int32[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static Func ResolveFromRouteOrQuery(string parameterName, IEnumerable? routeParameterNames) + { + return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true + ? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName]) + : (httpContext) => httpContext.Request.Query[parameterName]; + } + private static bool TryParseExplicit(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable + => T.TryParse(s, provider, out result); + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional.generated.txt new file mode 100644 index 000000000000..82e1993b1b45 --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional.generated.txt @@ -0,0 +1,449 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32?[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.Int32?[] (global::System.Int32?[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.Int32?[])); + var p_RouteOrQueryResolver = GeneratedRouteBuilderExtensionsCore.ResolveFromRouteOrQuery("p", options.RouteParameterNames); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int?[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = RouteOrQuery) + var p_raw = p_RouteOrQueryResolver(httpContext); + var p_temp = p_raw.ToArray(); + global::System.Int32?[]? p_local = new global::System.Int32?[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + wasParamCheckFailure = true; + logOrThrowExceptionHelper.RequiredParameterNotProvided("int?[]?", "p", "route or query string"); + } + } + p_local[i] = string.IsNullOrEmpty(element) ? null! : parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int?[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = RouteOrQuery) + var p_raw = p_RouteOrQueryResolver(httpContext); + var p_temp = p_raw.ToArray(); + global::System.Int32?[]? p_local = new global::System.Int32?[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + wasParamCheckFailure = true; + logOrThrowExceptionHelper.RequiredParameterNotProvided("int?[]?", "p", "route or query string"); + } + } + p_local[i] = string.IsNullOrEmpty(element) ? null! : parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.Int32?[] (global::System.Int32?[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static Func ResolveFromRouteOrQuery(string parameterName, IEnumerable? routeParameterNames) + { + return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true + ? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName]) + : (httpContext) => httpContext.Request.Query[parameterName]; + } + private static bool TryParseExplicit(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable + => T.TryParse(s, provider, out result); + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt new file mode 100644 index 000000000000..82e1993b1b45 --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt @@ -0,0 +1,449 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32?[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.Int32?[] (global::System.Int32?[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.Int32?[])); + var p_RouteOrQueryResolver = GeneratedRouteBuilderExtensionsCore.ResolveFromRouteOrQuery("p", options.RouteParameterNames); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int?[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = RouteOrQuery) + var p_raw = p_RouteOrQueryResolver(httpContext); + var p_temp = p_raw.ToArray(); + global::System.Int32?[]? p_local = new global::System.Int32?[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + wasParamCheckFailure = true; + logOrThrowExceptionHelper.RequiredParameterNotProvided("int?[]?", "p", "route or query string"); + } + } + p_local[i] = string.IsNullOrEmpty(element) ? null! : parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return Task.CompletedTask; + } + var result = handler(p_local); + return GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = int?[]?, IsOptional = True, IsParsable = True, IsArray = True, Source = RouteOrQuery) + var p_raw = p_RouteOrQueryResolver(httpContext); + var p_temp = p_raw.ToArray(); + global::System.Int32?[]? p_local = new global::System.Int32?[p_temp.Length]; + for (var i = 0; i < p_temp.Length; i++) + { + var element = p_temp[i]; + if (!GeneratedRouteBuilderExtensionsCore.TryParseExplicit(element!, CultureInfo.InvariantCulture, out var parsed_element)) + { + if (!string.IsNullOrEmpty(element)) + { + wasParamCheckFailure = true; + logOrThrowExceptionHelper.RequiredParameterNotProvided("int?[]?", "p", "route or query string"); + } + } + p_local[i] = string.IsNullOrEmpty(element) ? null! : parsed_element!; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.Int32?[] (global::System.Int32?[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static Func ResolveFromRouteOrQuery(string parameterName, IEnumerable? routeParameterNames) + { + return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true + ? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName]) + : (httpContext) => httpContext.Request.Query[parameterName]; + } + private static bool TryParseExplicit(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable + => T.TryParse(s, provider, out result); + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional.generated.txt new file mode 100644 index 000000000000..5e6adff6d906 --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional.generated.txt @@ -0,0 +1,510 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var serviceProviderIsService = serviceProvider.GetRequiredService(); + var jsonBodyOrServiceTypeTuples = new (bool, Type)[] { + #nullable disable + (true, typeof(global::System.String[])), + #nullable enable + }; + foreach (var (isOptional, type) in jsonBodyOrServiceTypeTuples) + { + if (!serviceProviderIsService.IsService(type)) + { + options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); + break; + } + } + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.String[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.String[] (global::System.String[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.String[])); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + async Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = string[]?, IsOptional = True, IsParsable = False, IsArray = True, Source = JsonBodyOrQuery) + global::System.String[]? p_local = null!; + if (options.DisableInferBodyFromParameters) + { + var p_raw = httpContext.Request.Query["p"]; + p_local = p_raw!; + } + else + { + var p_JsonTypeInfo = (JsonTypeInfo)jsonOptions.SerializerOptions.GetTypeInfo(typeof(global::System.String[])); + var p_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync(httpContext, logOrThrowExceptionHelper, true, "string[]?", "p", p_JsonTypeInfo); + p_local = p_resolveBodyResult.Item2!; + if (!p_resolveBodyResult.Item1) + { + return; + } + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return; + } + var result = handler(p_local); + await GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = string[]?, IsOptional = True, IsParsable = False, IsArray = True, Source = JsonBodyOrQuery) + global::System.String[]? p_local = null!; + if (options.DisableInferBodyFromParameters) + { + var p_raw = httpContext.Request.Query["p"]; + p_local = p_raw!; + } + else + { + var p_JsonTypeInfo = (JsonTypeInfo)jsonOptions.SerializerOptions.GetTypeInfo(typeof(global::System.String[])); + var p_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync(httpContext, logOrThrowExceptionHelper, true, "string[]?", "p", p_JsonTypeInfo); + p_local = p_resolveBodyResult.Item2!; + if (!p_resolveBodyResult.Item1) + { + return; + } + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.String[] (global::System.String[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, JsonTypeInfo jsonTypeInfo, bool isInferred = false) + { + var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; + + if (feature?.CanHaveBody == true) + { + if (!httpContext.Request.HasJsonContentType()) + { + logOrThrowExceptionHelper.UnexpectedJsonContentType(httpContext.Request.ContentType); + httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; + return (false, default); + } + try + { + bodyValue = await httpContext.Request.ReadFromJsonAsync(jsonTypeInfo); + bodyValueSet = bodyValue != null; + } + catch (BadHttpRequestException badHttpRequestException) + { + logOrThrowExceptionHelper.RequestBodyIOException(badHttpRequestException); + httpContext.Response.StatusCode = badHttpRequestException.StatusCode; + return (false, default); + } + catch (IOException ioException) + { + logOrThrowExceptionHelper.RequestBodyIOException(ioException); + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, default); + } + catch (System.Text.Json.JsonException jsonException) + { + logOrThrowExceptionHelper.InvalidJsonRequestBody(parameterTypeName, parameterName, jsonException); + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, default); + } + } + + if (!allowEmpty && !bodyValueSet) + { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); + } + + return (true, bodyValue); + } + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt new file mode 100644 index 000000000000..5e6adff6d906 --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt @@ -0,0 +1,510 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace System.Runtime.CompilerServices +{ + %GENERATEDCODEATTRIBUTE% + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization.Metadata; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Antiforgery; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Json; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Primitives; + using Microsoft.Extensions.Options; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedRouteBuilderExtensionsCore + { + private static readonly JsonOptions FallbackJsonOptions = new(); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + + %INTERCEPTSLOCATIONATTRIBUTE% + internal static RouteHandlerBuilder MapGet0( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern, + Delegate handler) + { + MetadataPopulator populateMetadata = (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var serviceProviderIsService = serviceProvider.GetRequiredService(); + var jsonBodyOrServiceTypeTuples = new (bool, Type)[] { + #nullable disable + (true, typeof(global::System.String[])), + #nullable enable + }; + foreach (var (isOptional, type) in jsonBodyOrServiceTypeTuples) + { + if (!serviceProviderIsService.IsService(type)) + { + options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); + break; + } + } + var parameters = methodInfo.GetParameters(); + options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: true)); + options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.String[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }; + RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = Cast(del, global::System.String[] (global::System.String[]? arg0= default) => throw null!); + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + var jsonOptions = serviceProvider?.GetService>()?.Value ?? FallbackJsonOptions; + var jsonSerializerOptions = jsonOptions.SerializerOptions; + jsonSerializerOptions.MakeReadOnly(); + var objectJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(object)); + var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof(global::System.String[])); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + async Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = string[]?, IsOptional = True, IsParsable = False, IsArray = True, Source = JsonBodyOrQuery) + global::System.String[]? p_local = null!; + if (options.DisableInferBodyFromParameters) + { + var p_raw = httpContext.Request.Query["p"]; + p_local = p_raw!; + } + else + { + var p_JsonTypeInfo = (JsonTypeInfo)jsonOptions.SerializerOptions.GetTypeInfo(typeof(global::System.String[])); + var p_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync(httpContext, logOrThrowExceptionHelper, true, "string[]?", "p", p_JsonTypeInfo); + p_local = p_resolveBodyResult.Item2!; + if (!p_resolveBodyResult.Item1) + { + return; + } + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return; + } + var result = handler(p_local); + await GeneratedRouteBuilderExtensionsCore.WriteJsonResponseAsync(httpContext.Response, result, responseJsonTypeInfo); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + // Endpoint Parameter: p (Type = string[]?, IsOptional = True, IsParsable = False, IsArray = True, Source = JsonBodyOrQuery) + global::System.String[]? p_local = null!; + if (options.DisableInferBodyFromParameters) + { + var p_raw = httpContext.Request.Query["p"]; + p_local = p_raw!; + } + else + { + var p_JsonTypeInfo = (JsonTypeInfo)jsonOptions.SerializerOptions.GetTypeInfo(typeof(global::System.String[])); + var p_resolveBodyResult = await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync(httpContext, logOrThrowExceptionHelper, true, "string[]?", "p", p_JsonTypeInfo); + p_local = p_resolveBodyResult.Item2!; + if (!p_resolveBodyResult.Item1) + { + return; + } + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, p_local)); + if (result is not null) + { + await GeneratedRouteBuilderExtensionsCore.ExecuteReturnAsync(result, httpContext, objectJsonTypeInfo); + } + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }; + var castHandler = Cast(handler, global::System.String[] (global::System.String[]? arg0= default) => throw null!); + return MapCore( + endpoints, + pattern, + handler, + GetVerb, + populateMetadata, + createRequestDelegate, + castHandler.Method); + } + + + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable? httpMethods, + MetadataPopulator populateMetadata, + RequestDelegateFactoryFunc createRequestDelegate, + MethodInfo methodInfo) + { + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate, methodInfo); + } + + private static T Cast(Delegate d, T _) where T : Delegate + { + return (T)d; + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteReturnAsync(object? obj, HttpContext httpContext, JsonTypeInfo jsonTypeInfo) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return WriteJsonResponseAsync(httpContext.Response, obj, jsonTypeInfo); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed ASP.NET apps, ensures the JsonSerializer doesn't use Reflection.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "See above.")] + private static Task WriteJsonResponseAsync(HttpResponse response, T? value, JsonTypeInfo jsonTypeInfo) + { + var runtimeType = value?.GetType(); + + if (jsonTypeInfo.ShouldUseWith(runtimeType)) + { + return HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, jsonTypeInfo, default); + } + + return response.WriteAsJsonAsync(value, jsonTypeInfo.Options); + } + + private static bool HasKnownPolymorphism(this JsonTypeInfo jsonTypeInfo) + => jsonTypeInfo.Type.IsSealed || jsonTypeInfo.Type.IsValueType || jsonTypeInfo.PolymorphismOptions is not null; + + private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(false)] Type? runtimeType) + => runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.HasKnownPolymorphism(); + + private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, JsonTypeInfo jsonTypeInfo, bool isInferred = false) + { + var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; + + if (feature?.CanHaveBody == true) + { + if (!httpContext.Request.HasJsonContentType()) + { + logOrThrowExceptionHelper.UnexpectedJsonContentType(httpContext.Request.ContentType); + httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; + return (false, default); + } + try + { + bodyValue = await httpContext.Request.ReadFromJsonAsync(jsonTypeInfo); + bodyValueSet = bodyValue != null; + } + catch (BadHttpRequestException badHttpRequestException) + { + logOrThrowExceptionHelper.RequestBodyIOException(badHttpRequestException); + httpContext.Response.StatusCode = badHttpRequestException.StatusCode; + return (false, default); + } + catch (IOException ioException) + { + logOrThrowExceptionHelper.RequestBodyIOException(ioException); + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, default); + } + catch (System.Text.Json.JsonException jsonException) + { + logOrThrowExceptionHelper.InvalidJsonRequestBody(parameterTypeName, parameterName, jsonException); + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, default); + } + } + + if (!allowEmpty && !bodyValueSet) + { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); + } + + return (true, bodyValue); + } + + } + + %GENERATEDCODEATTRIBUTE% + file static class GeneratedMetadataConstants + { + public static readonly string[] JsonContentType = new [] { "application/json" }; + public static readonly string[] PlaintextContentType = new [] { "text/plain" }; + public static readonly string[] FormFileContentType = new[] { "multipart/form-data" }; + public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + } + + %GENERATEDCODEATTRIBUTE% + file sealed class ParameterBindingMetadata: IParameterBindingMetadata + { + internal ParameterBindingMetadata( + string name, + ParameterInfo parameterInfo, + bool hasTryParse = false, + bool hasBindAsync = false, + bool isOptional = false) + { + Name = name; + ParameterInfo = parameterInfo; + HasTryParse = hasTryParse; + HasBindAsync = hasBindAsync; + IsOptional = isOptional; + } + + public string Name { get; } + + public bool HasTryParse { get; } + + public bool HasBindAsync { get; } + + public ParameterInfo ParameterInfo { get; } + + public bool IsOptional { get; } + } + + %GENERATEDCODEATTRIBUTE% + file sealed class LogOrThrowExceptionHelper + { + private readonly ILogger? _rdgLogger; + private readonly bool _shouldThrow; + + public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options) + { + var loggerFactory = serviceProvider?.GetRequiredService(); + _rdgLogger = loggerFactory?.CreateLogger("Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator"); + _shouldThrow = options?.ThrowOnBadRequest ?? false; + } + + public void RequestBodyIOException(IOException exception) + { + if (_rdgLogger != null) + { + _requestBodyIOException(_rdgLogger, exception); + } + } + + private static readonly Action _requestBodyIOException = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "RequestBodyIOException"), "Reading the request body failed with an IOException."); + + public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as JSON.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidJsonRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(2, "InvalidJsonRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as JSON."); + + public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to bind parameter \"{0} {1}\" from \"{2}\".", parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null); + } + } + + private static readonly Action _parameterBindingFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(3, "ParameterBindingFailed"), "Failed to bind parameter \"{ParameterType} {ParameterName}\" from \"{SourceValue}\"."); + + public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Required parameter \"{0} {1}\" was not provided from {2}.", parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null); + } + } + + private static readonly Action _requiredParameterNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequiredParameterNotProvided"), "Required parameter \"{ParameterType} {ParameterName}\" was not provided from {Source}."); + + public void ImplicitBodyNotProvided(string parameterName) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Implicit body inferred for parameter \"{0}\" but no body was provided. Did you mean to use a Service instead?", parameterName); + throw new BadHttpRequestException(message); + } + + if (_rdgLogger != null) + { + _implicitBodyNotProvided(_rdgLogger, parameterName, null); + } + } + + private static readonly Action _implicitBodyNotProvided = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, "ImplicitBodyNotProvided"), "Implicit body inferred for parameter \"{ParameterName}\" but no body was provided. Did you mean to use a Service instead?"); + + public void UnexpectedJsonContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported JSON media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedJsonContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnexpectedContentType"), "Expected a supported JSON media type but got \"{ContentType}\"."); + + public void UnexpectedNonFormContentType(string? contentType) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Expected a supported form media type but got \"{0}\".", contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + if (_rdgLogger != null) + { + _unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null); + } + } + + private static readonly Action _unexpectedNonFormContentType = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, "UnexpectedNonFormContentType"), "Expected a supported form media type but got \"{ContentType}\"."); + + public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception) + { + if (_shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, "Failed to read parameter \"{0} {1}\" from the request body as form.", parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); + } + + if (_rdgLogger != null) + { + _invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception); + } + } + + private static readonly Action _invalidFormRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(8, "InvalidFormRequestBody"), "Failed to read parameter \"{ParameterType} {ParameterName}\" from the request body as form."); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs index b10465db36b3..275cffd93822 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs @@ -345,7 +345,7 @@ public async Task MapAction_ImplicitQuery_StringArrayParam_Optional() await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, """["a","b","c"]"""); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -366,7 +366,7 @@ public async Task MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPres await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -388,7 +388,7 @@ public async Task MapAction_ExplicitQuery_StringArrayParam_Optional() await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, """["a","b","c"]"""); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -409,7 +409,7 @@ public async Task MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPres await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -431,7 +431,7 @@ public async Task MapAction_ImplicitQuery_IntArrayParam_Optional() await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[0,1,2]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -452,7 +452,7 @@ public async Task MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -474,7 +474,7 @@ public async Task MapAction_ExplicitQuery_IntArrayParam_Optional() await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[0,1,2]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -495,7 +495,7 @@ public async Task MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -517,7 +517,7 @@ public async Task MapAction_ImplicitQuery_NullableIntArrayParam_Optional() await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[0,1,2]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -538,7 +538,7 @@ public async Task MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNo await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -560,7 +560,7 @@ public async Task MapAction_ExplicitQuery_NullableIntArrayParam_Optional() await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[0,1,2]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact] @@ -581,7 +581,7 @@ public async Task MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNo await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "[]"); - //await VerifyAgainstBaselineUsingFile(compilation); + await VerifyAgainstBaselineUsingFile(compilation); } [Fact]