diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs index 3a49c95a1..ebf9c0ce7 100644 --- a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs @@ -94,9 +94,9 @@ private bool TryGetBindings(IMethodSymbol method, out IList>? methodOutputBindings) + if (!TryGetMethodOutputBinding(method, out bool hasMethodOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? methodOutputBindings) || !TryGetParameterInputAndTriggerBindings(method, out bool supportsRetryOptions, out hasHttpTrigger, out IList>? parameterInputAndTriggerBindings) - || !TryGetReturnTypeBindings(method, hasHttpTrigger, hasOutputBinding, out IList>? returnTypeBindings)) + || !TryGetReturnTypeBindings(method, hasHttpTrigger, hasMethodOutputBinding, out IList>? returnTypeBindings)) { bindings = null; return false; @@ -129,12 +129,12 @@ private bool TryGetBindings(IMethodSymbol method, out IList /// Checks for and returns any OutputBinding attributes associated with the method. /// - private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? bindingsList) + private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasMethodOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? bindingsList) { var attributes = method!.GetAttributes(); // methodSymbol is not null here because it's checked in IsValidAzureFunction which is called before bindings are collected/created AttributeData? outputBindingAttribute = null; - hasOutputBinding = false; + hasMethodOutputBinding = false; retryOptions = null; foreach (var attribute in attributes) @@ -149,8 +149,8 @@ private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasOutputBi if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass?.BaseType, _knownFunctionMetadataTypes.OutputBindingAttribute)) { - // There can only be one output binding associated with a function. If there is more than one, we return a diagnostic error here. - if (hasOutputBinding) + // There can only be one method output binding associated with a function. If there is more than one, we return a diagnostic error here. + if (hasMethodOutputBinding) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, Location.None, new object[] { "Method", method.Name })); bindingsList = null; @@ -158,7 +158,7 @@ private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasOutputBi } outputBindingAttribute = attribute; - hasOutputBinding = true; + hasMethodOutputBinding = true; } } @@ -400,7 +400,7 @@ private bool DoesConverterSupportTargetType(List converterAdverti /// /// Checks for and returns any bindings found in the Return Type of the method /// - private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, bool hasOutputBinding, out IList>? bindingsList) + private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, bool hasMethodOutputBinding, out IList>? bindingsList) { ITypeSymbol? returnTypeSymbol = method.ReturnType; bindingsList = new List>(); @@ -440,7 +440,7 @@ private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, } else { - if (!TryGetReturnTypePropertyBindings(returnTypeSymbol, hasHttpTrigger, hasOutputBinding, out bindingsList)) + if (!TryGetReturnTypePropertyBindings(returnTypeSymbol, hasHttpTrigger, hasMethodOutputBinding, out bindingsList)) { bindingsList = null; return false; @@ -451,10 +451,11 @@ private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, return true; } - private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool hasHttpTrigger, bool hasOutputBinding, out IList>? bindingsList) + private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool hasHttpTrigger, bool hasMethodOutputBinding, out IList>? bindingsList) { var members = returnTypeSymbol.GetMembers(); var foundHttpOutput = false; + var returnTypeHasOutputBindings = false; bindingsList = new List>(); // initialize this without size, because it will be difficult to predict how many bindings we can find here in the user code. foreach (var prop in returnTypeSymbol.GetMembers().Where(a => a is IPropertySymbol)) @@ -503,23 +504,16 @@ private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool bindingsList.Add(bindingDict!); - hasOutputBinding = true; + returnTypeHasOutputBindings = true; foundPropertyOutputAttr = true; } } } } - if (hasHttpTrigger && !foundHttpOutput) + if (hasHttpTrigger && !foundHttpOutput && !hasMethodOutputBinding && !returnTypeHasOutputBindings) { - if (!hasOutputBinding) - { - bindingsList.Add(GetHttpReturnBinding(Constants.FunctionMetadataBindingProps.ReturnBindingName)); - } - else - { - bindingsList.Add(GetHttpReturnBinding(Constants.FunctionMetadataBindingProps.HttpResponseBindingName)); - } + bindingsList.Add(GetHttpReturnBinding(Constants.FunctionMetadataBindingProps.ReturnBindingName)); } return true; diff --git a/sdk/release_notes.md b/sdk/release_notes.md index dd5cf7a3f..e9fea88c0 100644 --- a/sdk/release_notes.md +++ b/sdk/release_notes.md @@ -7,3 +7,4 @@ ### Microsoft.Azure.Functions.Worker.Sdk 1.16.3 (meta package) - Update worker.config generation to accurate worker executable name (#1053) +- Bug fix for scenarios with `$return` output binding and `HttpTrigger` breaking output-binding rules (#2098) \ No newline at end of file diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs index 755c40cdb..9e3f79e0a 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs @@ -48,7 +48,7 @@ public IntegratedTriggersAndBindingsTests() } [Fact] - public async Task FunctionWhereOutputBindingIsInTheReturnType() + public async Task FunctionsWhereOutputBindingIsInTheReturnType() { // test generating function metadata for a simple HttpTrigger string inputCode = """ @@ -67,6 +67,13 @@ public static MyOutputType Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", { throw new NotImplementedException(); } + + [Function("OutputTypeNoHttpProp")] + public static MyOutputTypeNoHttpProp Test([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + FunctionContext context) + { + throw new NotImplementedException(); + } } public class MyOutputType @@ -76,6 +83,12 @@ public class MyOutputType public HttpResponseData HttpResponse { get; set; } } + + public class MyOutputTypeNoHttpProp + { + [QueueOutput("functionstesting2", Connection = "AzureWebJobsStorage")] + public string Name { get; set; } + } } """; @@ -119,6 +132,19 @@ public Task> GetFunctionMetadataAsync(string d ScriptFile = "TestProject.dll" }; metadataList.Add(Function0); + var Function1RawBindings = new List(); + Function1RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function1RawBindings.Add(@"{""name"":""Name"",""type"":""queue"",""direction"":""Out"",""queueName"":""functionstesting2"",""connection"":""AzureWebJobsStorage""}"); + + var Function1 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "OutputTypeNoHttpProp", + EntryPoint = "FunctionApp.HttpTriggerWithMultipleOutputBindings.Test", + RawBindings = Function1RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function1); return Task.FromResult(metadataList.ToImmutableArray()); } diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs index 378813a8d..85b45f88d 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs @@ -1,10 +1,8 @@ // 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 Microsoft.Azure.Functions.Worker.Sdk.Generators; -using Microsoft.CodeAnalysis.Testing; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -277,6 +275,103 @@ await TestHelpers.RunTestAsync( expectedGeneratedFileName, expectedOutput); } + + [Fact] + public async void TestQueueOutputWithHttpTrigger() + { + string inputCode = """ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Text.Json.Serialization; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Http; + + namespace FunctionApp + { + public class HttpTriggerQueueOutput + { + [Function("HttpWithQueueOutput")] + [QueueOutput("myqueue", Connection = "Con")] + public string Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] 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 TestProject + { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider + { + /// + public Task> GetFunctionMetadataAsync(string directory) + { + var metadataList = new List(); + var Function0RawBindings = new List(); + Function0RawBindings.Add(@"{""name"":""$return"",""type"":""queue"",""direction"":""Out"",""queueName"":""myqueue"",""connection"":""Con""}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Function"",""methods"":[""get"",""post""]}"); + + var Function0 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "HttpWithQueueOutput", + EntryPoint = "FunctionApp.HttpTriggerQueueOutput.Run", + RawBindings = Function0RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function0); + + 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; + } + } + } + """; + + await TestHelpers.RunTestAsync( + _referencedExtensionAssemblies, + inputCode, + expectedGeneratedFileName, + expectedOutput); + } } } }