From 228018ba3930b08a2f3c7cb7c099025af20f598d Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Wed, 10 Jul 2024 07:24:25 -0700 Subject: [PATCH] Updating metadata generation to create the $return binding for all http trigger functions. (#2579) --- ...unctionMetadataProviderGenerator.Parser.cs | 10 +- sdk/release_notes.md | 1 + .../AutoConfigureStartupTypeTests.cs | 1 + .../HttpTriggerTests.cs | 128 ++++++++++++++++ .../IntegratedTriggersAndBindingsTests.cs | 141 ++++++++++++++++++ 5 files changed, 277 insertions(+), 4 deletions(-) diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs index ebeb0b7d..7acafdca 100644 --- a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs @@ -413,7 +413,9 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators } if (!SymbolEqualityComparer.Default.Equals(returnTypeSymbol, _knownTypes.VoidType) && - !SymbolEqualityComparer.Default.Equals(returnTypeSymbol.OriginalDefinition, _knownTypes.TaskType)) + !SymbolEqualityComparer.Default.Equals(returnTypeSymbol.OriginalDefinition, _knownTypes.TaskType) || + // For HTTP triggers, include the return binding even if the return type is void or Task. + hasHttpTrigger) { // If there is a Task return type, inspect T, the inner type. if (SymbolEqualityComparer.Default.Equals(returnTypeSymbol.OriginalDefinition, _knownTypes.TaskOfTType)) @@ -509,7 +511,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators foundHttpOutput = true; bindingsList.Add(GetHttpReturnBinding(prop.Name)); } - else + else if (bindingAttributes.Any()) { if (!TryCreateBindingDictionary(bindingAttributes.FirstOrDefault(), prop.Name, prop.Locations.FirstOrDefault(), out IDictionary? bindings)) { @@ -537,7 +539,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators var attributes = prop.GetAttributes(); foreach (var attribute in attributes) { - if (attribute.AttributeClass is not null && + if (attribute.AttributeClass is not null && attribute.AttributeClass.IsOrDerivedFrom(_knownFunctionMetadataTypes.HttpResultAttribute)) { return true; @@ -625,7 +627,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators { if (IsArrayOrNotNull(namedArgument.Value)) { - if (string.Equals(namedArgument.Key, Constants.FunctionMetadataBindingProps.IsBatchedKey) + if (string.Equals(namedArgument.Key, Constants.FunctionMetadataBindingProps.IsBatchedKey) && !attrProperties.ContainsKey("cardinality") && namedArgument.Value.Value != null) { var argValue = (bool)namedArgument.Value.Value; // isBatched only takes in booleans and the generator will parse it as a bool so we can type cast this to use in the next line diff --git a/sdk/release_notes.md b/sdk/release_notes.md index e1ef8fae..2a656ea5 100644 --- a/sdk/release_notes.md +++ b/sdk/release_notes.md @@ -11,3 +11,4 @@ ### Microsoft.Azure.Functions.Worker.Sdk.Generators 1.3.1 - ExtensionStartupRunnerGenerator generating code which conflicts with customer code (namespace) (#2542) +- Enhanced function metadata generation to include `$return` binding for HTTP trigger functions. (#1619) diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs index a59321d0..4c52e8be 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs @@ -100,6 +100,7 @@ namespace TestProject var metadataList = new List(); var Function0RawBindings = new List(); Function0RawBindings.Add(@""{{""""name"""":""""req"""",""""type"""":""""httpTrigger"""",""""direction"""":""""In"""",""""authLevel"""":""""Admin"""",""""methods"""":[""""get"""",""""post""""],""""route"""":""""/api2""""}}""); + Function0RawBindings.Add(@""{{""""name"""":""""$return"""",""""type"""":""""http"""",""""direction"""":""""Out""""}}""); var Function0 = new DefaultFunctionMetadata {{ diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs index 26d1a39f..b95db325 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs @@ -196,6 +196,7 @@ namespace Microsoft.Azure.Functions.SdkGeneratorTests var metadataList = new List(); var Function0RawBindings = new List(); Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Admin"",""methods"":[""get"",""post""],""route"":""/api2""}"); + Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata { @@ -352,6 +353,133 @@ namespace Microsoft.Azure.Functions.SdkGeneratorTests buildPropertiesDictionary: buildPropertiesDict, languageVersion: languageVersion); } + + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void NonStaticVoidOrTaskReturnType(LanguageVersion languageVersion) + { + string inputCode = """ + using System; + using System.Collections.Generic; + using Microsoft.Azure.Functions.Worker.Http; + using Microsoft.Azure.Functions.Worker; + using System.Threading; + using System.Threading.Tasks; + + namespace Foo + { + public sealed class HttpTriggers + { + [Function("Function1")] + public void FunctionWithVoidReturnType([HttpTrigger("get")] HttpRequestData req) + { + throw new NotImplementedException(); + } + [Function("Function2")] + public Task FunctionWithTaskReturnType([HttpTrigger("get")] HttpRequestData req) + { + throw new NotImplementedException(); + } + } + } + """; + + string expectedGeneratedFileName = $"GeneratedFunctionMetadataProvider.g.cs"; + string expectedOutput = """" + // + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Text.Json; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + + namespace MyCompany.MyProject.MyApp + { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider + { + /// + public Task> GetFunctionMetadataAsync(string directory) + { + var metadataList = new List(); + var Function0RawBindings = new List(); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""methods"":[""get""]}"); + Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function0 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "Function1", + EntryPoint = "Foo.HttpTriggers.FunctionWithVoidReturnType", + RawBindings = Function0RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function0); + var Function1RawBindings = new List(); + Function1RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""methods"":[""get""]}"); + Function1RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function1 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "Function2", + EntryPoint = "Foo.HttpTriggers.FunctionWithTaskReturnType", + RawBindings = Function1RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function1); + + return Task.FromResult(metadataList.ToImmutableArray()); + } + } + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// + public static class WorkerHostBuilderFunctionMetadataProviderExtension + { + /// + /// Adds the GeneratedFunctionMetadataProvider to the service collection. + /// During initialization, the worker will return generated function metadata instead of relying on the Azure Functions host for function indexing. + /// + public static IHostBuilder ConfigureGeneratedFunctionMetadataProvider(this IHostBuilder builder) + { + builder.ConfigureServices(s => + { + s.AddSingleton(); + }); + return builder; + } + } + } + """"; + // override the namespace value for generated types using msbuild property. + var buildPropertiesDict = new Dictionary() + { + { Constants.BuildProperties.GeneratedCodeNamespace, "MyCompany.MyProject.MyApp"} + }; + + await TestHelpers.RunTestAsync( + _referencedExtensionAssemblies, + inputCode, + expectedGeneratedFileName, + expectedOutput, + buildPropertiesDictionary: buildPropertiesDict, + languageVersion: languageVersion); + } } } } diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs index c512c755..44ffaab6 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -884,6 +885,146 @@ namespace Microsoft.Azure.Functions.SdkGeneratorTests expectedGeneratedFileName, expectedOutput); } + + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void HttpTriggerVoidOrTaskReturnType(LanguageVersion languageVersion) + { + string inputCode = """ + using System; + using System.Collections.Generic; + using Microsoft.Azure.Functions.Worker; + using Microsoft.AspNetCore.Http; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + + namespace Foo + { + public sealed class HttpTriggers + { + [Function("Function1")] + public Task Foo([HttpTrigger("get")] HttpRequest r) => throw new NotImplementedException(); + + [Function("Function2")] + public void Bar([HttpTrigger("get")] HttpRequest req) => throw new NotImplementedException(); + + [Obsolete("This method is obsolete. Use Foo instead.")] + [Function("Function3")] + public Task Baz([HttpTrigger("get")] HttpRequest r) => throw new NotImplementedException(); + } + } + """; + + string expectedGeneratedFileName = $"GeneratedFunctionMetadataProvider.g.cs"; + string expectedOutput = """" + // + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Text.Json; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + + namespace MyCompany.MyProject.MyApp + { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider + { + /// + public Task> GetFunctionMetadataAsync(string directory) + { + var metadataList = new List(); + var Function0RawBindings = new List(); + Function0RawBindings.Add(@"{""name"":""r"",""type"":""httpTrigger"",""direction"":""In"",""methods"":[""get""]}"); + Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function0 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "Function1", + EntryPoint = "Foo.HttpTriggers.Foo", + RawBindings = Function0RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function0); + var Function1RawBindings = new List(); + Function1RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""methods"":[""get""]}"); + Function1RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function1 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "Function2", + EntryPoint = "Foo.HttpTriggers.Bar", + RawBindings = Function1RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function1); + var Function2RawBindings = new List(); + Function2RawBindings.Add(@"{""name"":""r"",""type"":""httpTrigger"",""direction"":""In"",""methods"":[""get""]}"); + Function2RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function2 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "Function3", + EntryPoint = "Foo.HttpTriggers.Baz", + RawBindings = Function2RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function2); + + return Task.FromResult(metadataList.ToImmutableArray()); + } + } + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// + public static class WorkerHostBuilderFunctionMetadataProviderExtension + { + /// + /// Adds the GeneratedFunctionMetadataProvider to the service collection. + /// During initialization, the worker will return generated function metadata instead of relying on the Azure Functions host for function indexing. + /// + public static IHostBuilder ConfigureGeneratedFunctionMetadataProvider(this IHostBuilder builder) + { + builder.ConfigureServices(s => + { + s.AddSingleton(); + }); + return builder; + } + } + } + """"; + // override the namespace value for generated types using msbuild property. + var buildPropertiesDict = new Dictionary() + { + { Constants.BuildProperties.GeneratedCodeNamespace, "MyCompany.MyProject.MyApp"} + }; + + await TestHelpers.RunTestAsync( + _referencedExtensionAssemblies, + inputCode, + expectedGeneratedFileName, + expectedOutput, + buildPropertiesDictionary: buildPropertiesDict, + languageVersion: languageVersion); + } } } }