diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs index 3d20278757ae..e68aeb559e94 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs @@ -89,13 +89,26 @@ internal static void EmitFormParameterPreparation(this EndpointParameter endpoin internal static void EmitParsingBlock(this EndpointParameter endpointParameter, CodeWriter codeWriter) { + // parsable array 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()} = {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))"); + codeWriter.StartBlock(); + EmitLogOrThrowException(endpointParameter, codeWriter, "element"); + codeWriter.EndBlock(); + codeWriter.EndBlock(); // In cases where we are dealing with an array of parsable nullables we need to substitute // empty strings for null values. @@ -109,19 +122,69 @@ 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) { - endpointParameter.ParsingBlockEmitter(codeWriter, endpointParameter.EmitTempArgument(), endpointParameter.EmitParsedTempArgument()); + var temp_argument = endpointParameter.EmitTempArgument(); + var output_argument = endpointParameter.EmitParsedTempArgument(); + + // emit parsing block for optional OR nullable values + if (endpointParameter.IsOptional || endpointParameter.Type.NullableAnnotation == NullableAnnotation.Annotated) + { + var temp_argument_parsed_non_nullable = $"{temp_argument}_parsed_non_nullable"; + + 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};"""); + codeWriter.EndBlock(); + codeWriter.WriteLine($"""else if (string.IsNullOrEmpty({temp_argument}))"""); + codeWriter.StartBlock(); + codeWriter.WriteLine($"""{output_argument} = {endpointParameter.DefaultValue};"""); + codeWriter.EndBlock(); + codeWriter.WriteLine("else"); + codeWriter.StartBlock(); + codeWriter.WriteLine("wasParamCheckFailure = true;"); + codeWriter.EndBlock(); + } + // parsing block for non-nullable required parameters + else + { + codeWriter.WriteLine($$"""if (!{{endpointParameter.PreferredTryParseInvocation(temp_argument, output_argument)}})"""); + codeWriter.StartBlock(); + codeWriter.WriteLine($"if (!string.IsNullOrEmpty({temp_argument}))"); + codeWriter.StartBlock(); + 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. + // Not parsable, not an array. + else { 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("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;"); + } + } } 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/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 d7bf9bdef497..275cffd93822 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,264 @@ public async Task MapAction_ImplicitQuery_NullableStringArrayParam() await VerifyAgainstBaselineUsingFile(compilation); } + [Fact] + public async Task MapAction_ImplicitQuery_StringArrayParam_Optional() + { + 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(); + 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); + + 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_IntArrayParam_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_IntArrayParam_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_IntArrayParam_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); + } + + [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); + } + [Fact] public async Task MapPost_WithArrayQueryString_ShouldFail() { 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(); diff --git a/src/Http/samples/MinimalSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs index 8d21ab71c671..b90c3a3cb48c 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!" };