Browse Source

Expand support for MapX methods in RDG (#48012)

* Expand support for MapX methods in RDG

* Address feedback from review

* Route handler argument is always last argument to method

* Address feedback from peer review

* Update src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs

Co-authored-by: Eric Erhardt <[email protected]>

* Clean up method name pattern

---------

Co-authored-by: Eric Erhardt <[email protected]>
Safia Abdalla 2 years ago
parent
commit
ff8ea01c6a
39 changed files with 249 additions and 89 deletions
  1. 29 27
      src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
  2. 1 1
      src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs
  3. 2 2
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs
  4. 57 25
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs
  5. 3 0
      src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs
  6. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt
  7. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt
  8. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt
  9. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt
  10. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt
  11. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_ComplexTypeArrayParam.generated.txt
  12. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableStringArrayParam.generated.txt
  13. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam.generated.txt
  14. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitServiceParam_SimpleReturn_Snapshot.generated.txt
  15. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitSource_SimpleReturn_Snapshot.generated.txt
  16. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_ComplexTypeArrayParam.generated.txt
  17. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt
  18. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt
  19. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt
  20. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt
  21. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt
  22. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt
  23. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleStringParam_StringReturn.generated.txt
  24. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_NoParam_StringReturn_WithFilter.generated.txt
  25. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsString_Has_Metadata.generated.txt
  26. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsValidationProblemResult_Has_Metadata.generated.txt
  27. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsVoid_Has_No_Metadata.generated.txt
  28. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleComplexTypeParam_StringReturn.generated.txt
  29. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleEnumParam_StringReturn.generated.txt
  30. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleNullableStringParam_WithEmptyQueryStringValueProvided_StringReturn.generated.txt
  31. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt
  32. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt
  33. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt
  34. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt
  35. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_WithParams_StringReturn.generated.txt
  36. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/RequestDelegateValidateGeneratedFormCode.generated.txt
  37. 123 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.cs
  38. 2 2
      src/Http/Routing/src/Builder/RouteHandlerServices.cs
  39. 1 1
      src/Http/Routing/src/PublicAPI.Unshipped.txt

+ 29 - 27
src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -12,41 +13,24 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
 using Microsoft.CodeAnalysis.Operations;
 using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel.Emitters;
 using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
+using Microsoft.CodeAnalysis.CSharp;
 
 namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator;
 
 [Generator]
 public sealed class RequestDelegateGenerator : IIncrementalGenerator
 {
-    private static readonly string[] _knownMethods =
-    {
-        "MapGet",
-        "MapPost",
-        "MapPut",
-        "MapDelete",
-        "MapPatch",
-    };
-
     public void Initialize(IncrementalGeneratorInitializationContext context)
     {
         var endpointsWithDiagnostics = context.SyntaxProvider.CreateSyntaxProvider(
-            predicate: static (node, _) => node is InvocationExpressionSyntax
-            {
-                Expression: MemberAccessExpressionSyntax
-                {
-                    Name: IdentifierNameSyntax
-                    {
-                        Identifier: { ValueText: var method }
-                    }
-                },
-                ArgumentList: { Arguments: { Count: 2 } args }
-            } && _knownMethods.Contains(method),
+            predicate: static (node, _) => node.TryGetMapMethodName(out var method) && InvocationOperationExtensions.KnownMethods.Contains(method),
             transform: static (context, token) =>
             {
                 var operation = context.SemanticModel.GetOperation(context.Node, token);
                 var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation);
-                if (operation is IInvocationOperation { Arguments: { Length: 3 } parameters } invocationOperation &&
-                    invocationOperation.GetRouteHandlerArgument() is { Parameter.Type: {} delegateType } &&
+                if (operation is IInvocationOperation invocationOperation &&
+                    invocationOperation.TryGetRouteHandlerArgument(out var routeHandlerParameter) &&
+                    routeHandlerParameter is { Parameter.Type: {} delegateType } &&
                     SymbolEqualityComparer.Default.Equals(delegateType, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Delegate)))
                 {
                     return new Endpoint(invocationOperation, wellKnownTypes, context.SemanticModel);
@@ -136,11 +120,20 @@ public sealed class RequestDelegateGenerator : IIncrementalGenerator
                 using var codeWriter = new CodeWriter(stringWriter, baseIndent: 2);
                 foreach (var endpoint in dedupedByDelegate)
                 {
-                    codeWriter.WriteLine($"internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder {endpoint!.HttpMethod}(");
+                    codeWriter.WriteLine($"internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder {endpoint.HttpMethod}(");
                     codeWriter.Indent++;
                     codeWriter.WriteLine("this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,");
-                    codeWriter.WriteLine(@"[global::System.Diagnostics.CodeAnalysis.StringSyntax(""Route"")] string pattern,");
-                    codeWriter.WriteLine($"global::{endpoint!.EmitHandlerDelegateType()} handler,");
+                    // MapFallback overloads that only take a delegate do not need a pattern argument
+                    if (endpoint.HttpMethod != "MapFallback" || endpoint.Operation.Arguments.Length != 2)
+                    {
+                        codeWriter.WriteLine(@"[global::System.Diagnostics.CodeAnalysis.StringSyntax(""Route"")] string pattern,");
+                    }
+                    // MapMethods overloads define an additional `httpMethods` parameter
+                    if (endpoint.HttpMethod == "MapMethods")
+                    {
+                        codeWriter.WriteLine("global::System.Collections.Generic.IEnumerable<string> httpMethods,");
+                    }
+                    codeWriter.WriteLine($"global::{endpoint.EmitHandlerDelegateType()} handler,");
                     codeWriter.WriteLine(@"[global::System.Runtime.CompilerServices.CallerFilePath] string filePath = """",");
                     codeWriter.WriteLine("[global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)");
                     codeWriter.Indent--;
@@ -148,9 +141,18 @@ public sealed class RequestDelegateGenerator : IIncrementalGenerator
                     codeWriter.WriteLine("return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(");
                     codeWriter.Indent++;
                     codeWriter.WriteLine("endpoints,");
-                    codeWriter.WriteLine("pattern,");
+                    // For `MapFallback` overloads that only take a delegate, provide the assumed default
+                    // Otherwise, pass the pattern provided from the MapX invocation
+                    if (endpoint.HttpMethod != "MapFallback" && endpoint.Operation.Arguments.Length != 2)
+                    {
+                        codeWriter.WriteLine("pattern,");
+                    }
+                    else
+                    {
+                        codeWriter.WriteLine($"{SymbolDisplay.FormatLiteral(endpoint.RoutePattern!, true)},");
+                    }
                     codeWriter.WriteLine("handler,");
-                    codeWriter.WriteLine($"{endpoint!.EmitVerb()},");
+                    codeWriter.WriteLine($"{endpoint.EmitVerb()},");
                     codeWriter.WriteLine("filePath,");
                     codeWriter.WriteLine("lineNumber);");
                     codeWriter.Indent--;

+ 1 - 1
src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs

@@ -494,7 +494,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 2 - 2
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs

@@ -28,9 +28,9 @@ internal class Endpoint
             return;
         }
 
-        RoutePattern = routeToken.ValueText;
+        RoutePattern = routeToken;
 
-        if (!operation.TryGetRouteHandlerMethod(semanticModel, out var method) || method == null)
+        if (!operation.TryGetRouteHandlerMethod(semanticModel, out var method))
         {
             Diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.UnableToResolveMethod, Operation.Syntax.GetLocation()));
             return;

+ 57 - 25
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs

@@ -11,52 +11,84 @@ namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerM
 internal static class InvocationOperationExtensions
 {
     private const int RoutePatternArgumentOrdinal = 1;
-    private const int RouteHandlerArgumentOrdinal = 2;
+    public static readonly string[] KnownMethods =
+    {
+        "MapGet",
+        "MapPost",
+        "MapPut",
+        "MapDelete",
+        "MapPatch",
+        "Map",
+        "MapMethods",
+        "MapFallback"
+    };
+
+    public static bool TryGetRouteHandlerMethod(this IInvocationOperation invocation, SemanticModel semanticModel, [NotNullWhen(true)] out IMethodSymbol? method)
+    {
+        method = null;
+        if (invocation.TryGetRouteHandlerArgument(out var argument))
+        {
+            method = ResolveMethodFromOperation(argument, semanticModel);
+            return method is not null;
+        }
+        return false;
+    }
 
-    public static bool TryGetRouteHandlerMethod(this IInvocationOperation invocation, SemanticModel semanticModel, out IMethodSymbol? method)
+    public static bool TryGetRouteHandlerArgument(this IInvocationOperation invocation, [NotNullWhen(true)] out IArgumentOperation? argumentOperation)
     {
+        argumentOperation = null;
+        // Route handler argument is assumed to be the last parameter provided to
+        // the Map methods.
+        var routeHandlerArgumentOrdinal = invocation.Arguments.Length - 1;
         foreach (var argument in invocation.Arguments)
         {
-            if (argument.Parameter?.Ordinal == RouteHandlerArgumentOrdinal)
+            if (argument.Parameter?.Ordinal == routeHandlerArgumentOrdinal)
             {
-                method = ResolveMethodFromOperation(argument, semanticModel);
+                argumentOperation = argument;
                 return true;
             }
         }
-        method = null;
         return false;
     }
 
-    public static IArgumentOperation? GetRouteHandlerArgument(this IInvocationOperation invocation)
+    public static bool TryGetMapMethodName(this SyntaxNode node, out string? methodName)
     {
-        foreach (var argument in invocation.Arguments)
+        methodName = default;
+        // Given an invocation like app.MapGet, app.Map, app.MapFallback, etc. get
+        // the value of the Map method being access on the the WebApplication `app`.
+        if (node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax { Name: { Identifier: { ValueText: var method } } } })
         {
-            if (argument.Parameter?.Ordinal == RouteHandlerArgumentOrdinal)
-            {
-                return argument;
-            }
+            methodName = method;
+            return true;
         }
-        return null;
+        return false;
     }
 
-    public static bool TryGetRouteHandlerPattern(this IInvocationOperation invocation, out SyntaxToken token)
+    public static bool TryGetRouteHandlerPattern(this IInvocationOperation invocation, [NotNullWhen(true)] out string? token)
     {
-        IArgumentOperation? argumentOperation = null;
-        foreach (var argument in invocation.Arguments)
+        token = default;
+        if (invocation.Syntax.TryGetMapMethodName(out var methodName))
         {
-            if (argument.Parameter?.Ordinal == RoutePatternArgumentOrdinal)
+            if (methodName == "MapFallback" && invocation.Arguments.Length == 2)
             {
-                argumentOperation = argument;
+                token = "{*path:nonfile}";
+                return true;
+            }
+            foreach (var argument in invocation.Arguments)
+            {
+                if (argument.Parameter?.Ordinal == RoutePatternArgumentOrdinal)
+                {
+                    if (argument?.Syntax is not ArgumentSyntax routePatternArgumentSyntax ||
+                        routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax)
+                    {
+                        return false;
+                    }
+                    token = routePatternArgumentLiteralSyntax.Token.ValueText;
+                    return true;
+                }
             }
         }
-        if (argumentOperation?.Syntax is not ArgumentSyntax routePatternArgumentSyntax ||
-            routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax)
-        {
-            token = default;
-            return false;
-        }
-        token = routePatternArgumentLiteralSyntax.Token;
-        return true;
+        return false;
     }
 
     private static IMethodSymbol? ResolveMethodFromOperation(IOperation operation, SemanticModel semanticModel) => operation switch

+ 3 - 0
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs

@@ -44,6 +44,9 @@ internal static class StaticRouteHandlerModelEmitter
             "MapPost" => "PostVerb",
             "MapDelete" => "DeleteVerb",
             "MapPatch" => "PatchVerb",
+            "MapMethods" => "httpMethods",
+            "Map" => "null",
+            "MapFallback" => "null",
             _ => throw new ArgumentException($"Received unexpected HTTP method: {endpoint.HttpMethod}")
         };
     }

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt

@@ -1697,7 +1697,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt

@@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt

@@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt

@@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt

@@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_ComplexTypeArrayParam.generated.txt

@@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableStringArrayParam.generated.txt

@@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam.generated.txt

@@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitServiceParam_SimpleReturn_Snapshot.generated.txt

@@ -321,7 +321,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitSource_SimpleReturn_Snapshot.generated.txt

@@ -493,7 +493,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_ComplexTypeArrayParam.generated.txt

@@ -194,7 +194,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt

@@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt

@@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt

@@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt

@@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt

@@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt

@@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleStringParam_StringReturn.generated.txt

@@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_NoParam_StringReturn_WithFilter.generated.txt

@@ -153,7 +153,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsString_Has_Metadata.generated.txt

@@ -153,7 +153,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsValidationProblemResult_Has_Metadata.generated.txt

@@ -152,7 +152,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsVoid_Has_No_Metadata.generated.txt

@@ -152,7 +152,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleComplexTypeParam_StringReturn.generated.txt

@@ -191,7 +191,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleEnumParam_StringReturn.generated.txt

@@ -191,7 +191,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleNullableStringParam_WithEmptyQueryStringValueProvided_StringReturn.generated.txt

@@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt

@@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt

@@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt

@@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt

@@ -350,7 +350,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_WithParams_StringReturn.generated.txt

@@ -307,7 +307,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/RequestDelegateValidateGeneratedFormCode.generated.txt

@@ -255,7 +255,7 @@ namespace Microsoft.AspNetCore.Http.Generated
             this IEndpointRouteBuilder routes,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             string filePath,
             int lineNumber)
         {

+ 123 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.cs

@@ -449,4 +449,127 @@ app.MapGet("/", (HttpContext httpContext, int id) =>
         Assert.Equal(42, httpContext.Items["id"]);
         Assert.Equal(200, httpContext.Response.StatusCode);
     }
+
+    [Fact]
+    public async Task RequestDelegateCreation_SupportMapMethods()
+    {
+        var source = """
+var supportedMethods = new[] { "GET", "POST" };
+app.MapMethods("/", supportedMethods, (HttpContext httpContext, int id) =>
+{
+    httpContext.Items["id"] = id;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
+        {
+            ["id"] = "42",
+        });
+
+        httpContext.Request.Headers.Referer = "https://example.org";
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(42, httpContext.Items["id"]);
+        Assert.Equal(200, httpContext.Response.StatusCode);
+    }
+
+    [Fact]
+    public async Task RequestDelegateCreation_SupportMapMethods_InvalidRequestMethod()
+    {
+        var source = """
+var supportedMethods = new[] { "DELETE", "PATCH" };
+app.MapMethods("/", supportedMethods, (HttpContext httpContext, int id) =>
+{
+    httpContext.Items["id"] = id;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Method = "GET";
+
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Null(httpContext.Items["id"]);
+        Assert.Equal(400, httpContext.Response.StatusCode);
+    }
+
+    [Fact]
+    public async Task RequestDelegateCreation_SupportsMap()
+    {
+        var source = """
+app.Map("/", (HttpContext httpContext, int id) =>
+{
+    httpContext.Items["id"] = id;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
+        {
+            ["id"] = "42",
+        });
+
+        httpContext.Request.Headers.Referer = "https://example.org";
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(42, httpContext.Items["id"]);
+        Assert.Equal(200, httpContext.Response.StatusCode);
+    }
+
+    [Fact]
+    public async Task RequestDelegateCreation_SupportsMapFallback()
+    {
+        var source = """
+app.MapFallback("/", (HttpContext httpContext, int id) =>
+{
+    httpContext.Items["id"] = id;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
+        {
+            ["id"] = "42",
+        });
+
+        httpContext.Request.Headers.Referer = "https://example.org";
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(42, httpContext.Items["id"]);
+        Assert.Equal(200, httpContext.Response.StatusCode);
+    }
+
+    [Fact]
+    public async Task RequestDelegateCreation_SupportsMapFallback_NoRoute()
+    {
+        var source = """
+app.MapFallback((HttpContext httpContext, int id) =>
+{
+    httpContext.Items["id"] = id;
+});
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
+        {
+            ["id"] = "42",
+        });
+
+        httpContext.Request.Headers.Referer = "https://example.org";
+        await endpoint.RequestDelegate(httpContext);
+
+        Assert.Equal(42, httpContext.Items["id"]);
+        Assert.Equal(200, httpContext.Response.StatusCode);
+    }
 }

+ 2 - 2
src/Http/Routing/src/Builder/RouteHandlerServices.cs

@@ -26,7 +26,7 @@ public static class RouteHandlerServices
     /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
     /// <param name="pattern">The route pattern.</param>
     /// <param name="handler">The delegate executed when the endpoint is matched.</param>
-    /// <param name="httpMethods">The set of supported HTTP methods. May not be null.</param>
+    /// <param name="httpMethods">The set of supported HTTP methods.</param>
     /// <param name="populateMetadata">A delegate for populating endpoint metadata.</param>
     /// <param name="createRequestDelegate">A delegate for constructing a RequestDelegate.</param>
     /// <returns></returns>
@@ -34,7 +34,7 @@ public static class RouteHandlerServices
             IEndpointRouteBuilder endpoints,
             string pattern,
             Delegate handler,
-            IEnumerable<string> httpMethods,
+            IEnumerable<string>? httpMethods,
             Func<MethodInfo, RequestDelegateFactoryOptions?, RequestDelegateMetadataResult> populateMetadata,
             Func<Delegate, RequestDelegateFactoryOptions, RequestDelegateMetadataResult?, RequestDelegateResult> createRequestDelegate)
     {

+ 1 - 1
src/Http/Routing/src/PublicAPI.Unshipped.txt

@@ -1,8 +1,8 @@
 #nullable enable
 Microsoft.AspNetCore.Routing.RouteHandlerServices
-static Microsoft.AspNetCore.Routing.RouteHandlerServices.Map(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! handler, System.Collections.Generic.IEnumerable<string!>! httpMethods, System.Func<System.Reflection.MethodInfo!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions?, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult!>! populateMetadata, System.Func<System.Delegate!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions!, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult?, Microsoft.AspNetCore.Http.RequestDelegateResult!>! createRequestDelegate) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
 Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions
 Microsoft.AspNetCore.Routing.RouteShortCircuitEndpointRouteBuilderExtensions
 static Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions.ShortCircuit(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int? statusCode = null) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
+static Microsoft.AspNetCore.Routing.RouteHandlerServices.Map(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! handler, System.Collections.Generic.IEnumerable<string!>? httpMethods, System.Func<System.Reflection.MethodInfo!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions?, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult!>! populateMetadata, System.Func<System.Delegate!, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions!, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult?, Microsoft.AspNetCore.Http.RequestDelegateResult!>! createRequestDelegate) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
 static Microsoft.AspNetCore.Routing.RouteShortCircuitEndpointRouteBuilderExtensions.MapShortCircuit(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! builder, int statusCode, params string![]! routePrefixes) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
 static Microsoft.Extensions.DependencyInjection.RoutingServiceCollectionExtensions.AddRoutingCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!