diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj
index 5e6cbf836ff8..83c09bcf59a2 100644
--- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj
+++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj
@@ -25,6 +25,8 @@
+
+
diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj
index 918ef3c78c1a..b791dcb5efd8 100644
--- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj
+++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj
@@ -21,7 +21,9 @@
+
+
diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
index 77fca5f07d52..6f4432d9af41 100644
--- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
+++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
@@ -120,7 +120,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
lineNumber);
}
""");
- }
+ }
return code.ToString();
});
diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointEmitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointEmitter.cs
new file mode 100644
index 000000000000..f0a2c48b28d5
--- /dev/null
+++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointEmitter.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
+internal static class EndpointEmitter
+{
+ internal static string EmitParameterPreparation(this Endpoint endpoint)
+ {
+ var parameterPreparationBuilder = new StringBuilder();
+
+ for (var parameterIndex = 0; parameterIndex < endpoint.Parameters.Length; parameterIndex++)
+ {
+ var parameter = endpoint.Parameters[parameterIndex];
+
+ var parameterPreparationCode = parameter switch
+ {
+ {
+ Source: EndpointParameterSource.SpecialType
+ } => parameter.EmitSpecialParameterPreparation(),
+ {
+ Source: EndpointParameterSource.Query,
+ } => parameter.EmitQueryParameterPreparation(),
+ _ => throw new Exception("Unreachable!")
+ };
+
+ // To avoid having two newlines after the block of parameter handling code.
+ if (parameterIndex < endpoint.Parameters.Length - 1)
+ {
+ parameterPreparationBuilder.AppendLine(parameterPreparationCode);
+ }
+ else
+ {
+ parameterPreparationBuilder.Append(parameterPreparationCode);
+ }
+ }
+
+ return parameterPreparationBuilder.ToString();
+ }
+}
diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs
new file mode 100644
index 000000000000..23a7f4611ff5
--- /dev/null
+++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
+internal static class EndpointParameterEmitter
+{
+ internal static string EmitSpecialParameterPreparation(this EndpointParameter endpointParameter)
+ {
+ return $"""
+ var {endpointParameter.Name}_local = {endpointParameter.AssigningCode};
+""";
+ }
+
+ internal static string EmitQueryParameterPreparation(this EndpointParameter endpointParameter)
+ {
+ var builder = new StringBuilder();
+
+ // Preamble for diagnostics purposes.
+ builder.AppendLine($$"""
+ // Endpoint Parameter: {{endpointParameter.Name}} (Type = {{endpointParameter.Type}}, IsOptional = {{endpointParameter.IsOptional}}, Source = {{endpointParameter.Source}})
+""");
+
+ // Grab raw input from HttpContext.
+ builder.AppendLine($$"""
+ var {{endpointParameter.Name}}_raw = {{endpointParameter.AssigningCode}};
+""");
+
+ // If we are not optional, then at this point we can just assign the string value to the handler argument,
+ // otherwise we need to detect whether no value is provided and set the handler argument to null to
+ // preserve consistency with RDF behavior. We don't want to emit the conditional block to avoid
+ // compiler errors around null handling.
+ if (endpointParameter.IsOptional)
+ {
+ builder.AppendLine($$"""
+ var {{endpointParameter.HandlerArgument}} = {{endpointParameter.Name}}_raw.Count > 0 ? {{endpointParameter.Name}}_raw.ToString() : null;
+""");
+ }
+ else
+ {
+ builder.AppendLine($$"""
+ if (StringValues.IsNullOrEmpty({{endpointParameter.Name}}_raw))
+ {
+ httpContext.Response.StatusCode = 400;
+ return Task.CompletedTask;
+ }
+ var {{endpointParameter.HandlerArgument}} = {{endpointParameter.Name}}_raw.ToString();
+""");
+ }
+
+ return builder.ToString();
+ }
+}
diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs
index c698758755bc..e863dfedf78b 100644
--- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs
+++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs
@@ -88,7 +88,7 @@ public static bool SignatureEquals(Endpoint a, Endpoint b)
for (var i = 0; i < a.Parameters.Length; i++)
{
- if (a.Parameters[i].Equals(b.Parameters[i]))
+ if (!a.Parameters[i].Equals(b.Parameters[i]))
{
return false;
}
diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs
index 72a6f1f0a948..0c820a0ee4ec 100644
--- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs
+++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs
@@ -3,8 +3,10 @@
using System;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
+using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
using Microsoft.CodeAnalysis;
using WellKnownType = Microsoft.AspNetCore.App.Analyzers.Infrastructure.WellKnownTypeData.WellKnownType;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
@@ -15,11 +17,25 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
Type = parameter.Type;
Name = parameter.Name;
Source = EndpointParameterSource.Unknown;
+ HandlerArgument = $"{parameter.Name}_local";
- if (GetSpecialTypeCallingCode(Type, wellKnownTypes) is string callingCode)
+ var fromQueryMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata);
+
+ if (GetSpecialTypeAssigningCode(Type, wellKnownTypes) is string assigningCode)
{
Source = EndpointParameterSource.SpecialType;
- CallingCode = callingCode;
+ AssigningCode = assigningCode;
+ }
+ else if (parameter.HasAttributeImplementingInterface(fromQueryMetadataInterfaceType))
+ {
+ Source = EndpointParameterSource.Query;
+ AssigningCode = $"httpContext.Request.Query[\"{parameter.Name}\"]";
+ IsOptional = parameter.Type is INamedTypeSymbol parameterType && parameterType.NullableAnnotation == NullableAnnotation.Annotated;
+ }
+ else
+ {
+ // TODO: Inferencing rules go here - but for now:
+ Source = EndpointParameterSource.Unknown;
}
}
@@ -28,23 +44,17 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
// TODO: If the parameter has [FromRoute("AnotherName")] or similar, prefer that.
public string Name { get; }
- public string? CallingCode { get; }
+ public string? AssigningCode { get; }
+ public string HandlerArgument { get; }
+ public bool IsOptional { get; }
public string EmitArgument()
{
- switch (Source)
- {
- case EndpointParameterSource.SpecialType:
- return CallingCode!;
- default:
- // Eventually there should be know unknown parameter sources, but in the meantime we don't expect them to get this far.
- // The netstandard2.0 target means there is no UnreachableException.
- throw new Exception("Unreachable!");
- }
+ return HandlerArgument;
}
// TODO: Handle special form types like IFormFileCollection that need special body-reading logic.
- private static string? GetSpecialTypeCallingCode(ITypeSymbol type, WellKnownTypes wellKnownTypes)
+ private static string? GetSpecialTypeAssigningCode(ITypeSymbol type, WellKnownTypes wellKnownTypes)
{
if (SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_HttpContext)))
{
diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs
index 4ca73fb4c1d4..6d7369e51afd 100644
--- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs
+++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs
@@ -2,9 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Net.Cache;
using System.Text;
+using Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
@@ -61,14 +65,18 @@ public static string EmitRequestHandler(this Endpoint endpoint)
var resultAssignment = endpoint.Response.IsVoid ? string.Empty : "var result = ";
var awaitHandler = endpoint.Response.IsAwaitable ? "await " : string.Empty;
var setContentType = endpoint.Response.IsVoid ? string.Empty : $@"httpContext.Response.ContentType ??= ""{endpoint.Response.ContentType}"";";
- return $$"""
+
+ var requestHandlerSource = $$"""
{{handlerSignature}}
{
+{{endpoint.EmitParameterPreparation()}}
{{setContentType}}
{{resultAssignment}}{{awaitHandler}}handler({{endpoint.EmitArgumentList()}});
{{(endpoint.Response.IsVoid ? "return Task.CompletedTask;" : endpoint.EmitResponseWritingCall())}}
}
""";
+
+ return requestHandlerSource;
}
private static string EmitResponseWritingCall(this Endpoint endpoint)
@@ -108,14 +116,12 @@ private static string EmitResponseWritingCall(this Endpoint endpoint)
* can be used to reduce the boxing that happens at runtime when constructing
* the context object.
*/
- public static string EmitFilteredRequestHandler(this Endpoint endpoint)
+ public static string EmitFilteredRequestHandler(this Endpoint _)
{
- var argumentList = endpoint.Parameters.Length == 0 ? string.Empty : $", {endpoint.EmitArgumentList()}";
-
return $$"""
async Task RequestHandlerFiltered(HttpContext httpContext)
{
- var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext{{argumentList}}));
+ var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext));
await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);
}
""";
diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs
deleted file mode 100644
index 9b26b8c76e37..000000000000
--- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.AspNetCore.App.Analyzers.Infrastructure;
-
-internal static class WellKnownTypeData
-{
- public enum WellKnownType
- {
- Microsoft_AspNetCore_Http_HttpContext,
- Microsoft_AspNetCore_Http_HttpRequest,
- Microsoft_AspNetCore_Http_HttpResponse,
- Microsoft_AspNetCore_Http_IFormCollection,
- Microsoft_AspNetCore_Http_IFormFileCollection,
- Microsoft_AspNetCore_Http_IFormFile,
- Microsoft_AspNetCore_Http_IResult,
- System_IO_Pipelines_PipeReader,
- System_IO_Stream,
- System_Security_Claims_ClaimsPrincipal,
- System_Threading_CancellationToken,
- System_Threading_Tasks_Task,
- System_Threading_Tasks_Task_T,
- System_Threading_Tasks_ValueTask,
- System_Threading_Tasks_ValueTask_T,
- }
-
- public static readonly string[] WellKnownTypeNames = new[]
- {
- "Microsoft.AspNetCore.Http.HttpContext",
- "Microsoft.AspNetCore.Http.HttpRequest",
- "Microsoft.AspNetCore.Http.HttpResponse",
- "Microsoft.AspNetCore.Http.IFormCollection",
- "Microsoft.AspNetCore.Http.IFormFileCollection",
- "Microsoft.AspNetCore.Http.IFormFile",
- "Microsoft.AspNetCore.Http.IResult",
- "System.IO.Pipelines.PipeReader",
- "System.IO.Stream",
- "System.Security.Claims.ClaimsPrincipal",
- "System.Threading.CancellationToken",
- "System.Threading.Tasks.Task",
- "System.Threading.Tasks.Task`1",
- "System.Threading.Tasks.ValueTask",
- "System.Threading.Tasks.ValueTask`1",
- };
-}
diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj
index 09a381ba791e..b6a0988e5a9f 100644
--- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj
+++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj
@@ -16,6 +16,7 @@
+
diff --git a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs
index 43435af6e8c5..4cbb1e8ff8f2 100644
--- a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs
+++ b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs
@@ -11,6 +11,7 @@
using Microsoft.AspNetCore.Http.Json;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis;
+using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
namespace Microsoft.AspNetCore.Http.Extensions.Tests;
diff --git a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs
index bb0d16c9b039..7b4e3ab437f2 100644
--- a/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs
+++ b/src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs
@@ -8,6 +8,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Moq;
+using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
namespace Microsoft.AspNetCore.Http.Extensions.Tests;
diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt
new file mode 100644
index 000000000000..b9a14be5a3b0
--- /dev/null
+++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt
@@ -0,0 +1,179 @@
+//------------------------------------------------------------------------------
+//
+// 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 Microsoft.AspNetCore.Builder
+{
+ %GENERATEDCODEATTRIBUTE%
+ internal class SourceKey
+ {
+ public string Path { get; init; }
+ public int Line { get; init; }
+
+ public SourceKey(string path, int line)
+ {
+ Path = path;
+ Line = line;
+ }
+ }
+
+ // This class needs to be internal so that the compiled application
+ // has access to the strongly-typed endpoint definitions that are
+ // generated by the compiler so that they will be favored by
+ // overload resolution and opt the runtime in to the code generated
+ // implementation produced here.
+ %GENERATEDCODEATTRIBUTE%
+ internal static class GenerateRouteBuilderEndpoints
+ {
+ private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get };
+ private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post };
+ private static readonly string[] PutVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Put };
+ private static readonly string[] DeleteVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Delete };
+ private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch };
+
+ internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet(
+ this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,
+ [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern,
+ global::System.Func handler,
+ [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "",
+ [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)
+ {
+ return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(
+ endpoints,
+ pattern,
+ handler,
+ GetVerb,
+ filePath,
+ lineNumber);
+ }
+
+ }
+}
+
+namespace Microsoft.AspNetCore.Http.Generated
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Reflection;
+ using System.Threading.Tasks;
+ using System.IO;
+ using Microsoft.AspNetCore.Routing;
+ using Microsoft.AspNetCore.Routing.Patterns;
+ using Microsoft.AspNetCore.Builder;
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Http.Metadata;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.FileProviders;
+ using Microsoft.Extensions.Primitives;
+
+ using MetadataPopulator = System.Func;
+ using RequestDelegateFactoryFunc = System.Func;
+
+ file static class GeneratedRouteBuilderExtensionsCore
+ {
+
+ private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new()
+ {
+ [(@"TestMapActions.cs", 15)] = (
+ (methodInfo, options) =>
+ {
+ Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found.");
+ options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15));
+ return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() };
+ },
+ (del, options, inferredMetadataResult) =>
+ {
+ var handler = (System.Func)del;
+ EndpointFilterDelegate? filteredInvocation = null;
+
+ if (options?.EndpointBuilder?.FilterFactories.Count > 0)
+ {
+ filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic =>
+ {
+ if (ic.HttpContext.Response.StatusCode == 400)
+ {
+ return ValueTask.FromResult