From 2a5e6182c9d96d946a671b7e8cea224034c97a57 Mon Sep 17 00:00:00 2001 From: Shyju Krishnankutty Date: Wed, 9 Feb 2022 15:20:21 -0800 Subject: [PATCH] Include all bindings when generating function metadata (#542) * Adding all bindings to function.json * Fixed casing of direction attribute * Update assembly version * Removed Array allocation * sFixed to address PR comments * Bumped minor version of package. --- common.props | 4 +- .../MethodInfoExtensions.cs | 50 +++- .../FunctionJsonConverterTests.cs | 264 +++++++++++++++++- 3 files changed, 295 insertions(+), 23 deletions(-) diff --git a/common.props b/common.props index d5bc363..76dd135 100644 --- a/common.props +++ b/common.props @@ -3,8 +3,8 @@ true 4 - 0 - 1 + 1 + 0 diff --git a/src/Microsoft.NET.Sdk.Functions.Generator/MethodInfoExtensions.cs b/src/Microsoft.NET.Sdk.Functions.Generator/MethodInfoExtensions.cs index 98b5804..88e9cc5 100644 --- a/src/Microsoft.NET.Sdk.Functions.Generator/MethodInfoExtensions.cs +++ b/src/Microsoft.NET.Sdk.Functions.Generator/MethodInfoExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Mono.Cecil; @@ -59,15 +60,20 @@ public static JObject ManualTriggerBinding(this MethodDefinition method) /// object that represents the passed in . public static FunctionJsonSchema ToFunctionJson(this MethodDefinition method, string assemblyPath) { + // For every SDK parameter, convert it to a FunctionJson bindings. + // Every parameter can potentially contain more than 1 attribute that will be converted into a binding object. + var bindingsFromParameters = method.HasNoAutomaticTriggerAttribute() ? new[] { method.ManualTriggerBinding() } : method.Parameters + .Select(p => p.ToFunctionJsonBindings()) + .SelectMany(i => i); + + // Get binding if a return attribute is used. + // Ex: [return: Queue("myqueue-items-a", Connection = "MyStorageConnStr")] + var returnBindings = GetOutputBindingsFromReturnAttribute(method); + var allBindings = bindingsFromParameters.Concat(returnBindings).ToArray(); + return new FunctionJsonSchema { - // For every SDK parameter, convert it to a FunctionJson bindings. - // Every parameter can potentially contain more than 1 attribute that will be converted into a binding object. - Bindings = method.HasNoAutomaticTriggerAttribute() ? new[] { method.ManualTriggerBinding() } : method.Parameters - .Where(p => p.IsWebJobSdkTriggerParameter()) - .Select(p => p.ToFunctionJsonBindings()) - .SelectMany(i => i) - .ToArray(), + Bindings = allBindings, // Entry point is the fully qualified name of the function EntryPoint = $"{method.DeclaringType.FullName}.{method.Name}", ScriptFile = assemblyPath, @@ -77,6 +83,36 @@ public static FunctionJsonSchema ToFunctionJson(this MethodDefinition method, st }; } + /// + /// Gets bindings from return expression used with a binding expression. + /// Ex: + /// [FunctionName("HttpTriggerWriteToQueue1")] + /// [return: Queue("myqueue-items-a", Connection = "MyStorageConnStra")] + /// public static string Run([HttpTrigger] HttpRequestMessage request) => "foo"; + /// + private static JObject[] GetOutputBindingsFromReturnAttribute(MethodDefinition method) + { + if (method.MethodReturnType == null) + { + return Array.Empty(); + } + + var outputBindings = new List(); + foreach (var attribute in method.MethodReturnType.CustomAttributes.Where(a=>a.IsWebJobsAttribute())) + { + var bindingJObject = attribute.ToReflection().ToJObject(); + + // return binding must have the direction attribute set to out. + // https://github.com/Azure/azure-functions-host/blob/dev/src/WebJobs.Script/Utility.cs#L561 + bindingJObject["name"] = "$return"; + bindingJObject["direction"] = "out"; + + outputBindings.Add(bindingJObject); + } + + return outputBindings.ToArray(); + } + /// /// Gets a function name from a /// diff --git a/test/Microsoft.NET.Sdk.Functions.Generator.Tests/FunctionJsonConverterTests.cs b/test/Microsoft.NET.Sdk.Functions.Generator.Tests/FunctionJsonConverterTests.cs index e7f5db9..deaea93 100644 --- a/test/Microsoft.NET.Sdk.Functions.Generator.Tests/FunctionJsonConverterTests.cs +++ b/test/Microsoft.NET.Sdk.Functions.Generator.Tests/FunctionJsonConverterTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; @@ -8,6 +10,7 @@ using Microsoft.Azure.EventHubs; using Microsoft.Azure.WebJobs; using Microsoft.WindowsAzure.Storage.Queue; +using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.NET.Sdk.Functions.Test @@ -24,6 +27,26 @@ public class FunctionsClass [FunctionName("MyHttpTrigger")] public static void Run1([HttpTrigger] HttpRequestMessage request) { } + [FunctionName("HttpTriggerQueueReturn")] + [return: Queue("myqueue-items-a", Connection = "MyStorageConnStrA")] + public static string HttpTriggerQueueReturn([HttpTrigger] HttpRequestMessage request) => "foo"; + + [FunctionName("HttpTriggerQueueOutParam")] + public static void HttpTriggerQueueOutParam([HttpTrigger] HttpRequestMessage request, + [Queue("myqueue-items-b", Connection = "MyStorageConnStrB")] out string msg) + { + msg = "foo"; + } + + [FunctionName("HttpTriggerMultipleOutputs")] + public static void HttpTriggerMultipleOutputs([HttpTrigger] HttpRequestMessage request, + [Blob("binding-metric-test/sample-text.txt", Connection = "MyStorageConnStrC")] out string myBlob, + [Queue("myqueue-items-c", Connection = "MyStorageConnStrC")] IAsyncCollector qCollector) + { + myBlob = "foo-blob"; + qCollector.AddAsync("foo-queue"); + } + [FunctionName("MyBlobTrigger")] public static void Run2([BlobTrigger("blob.txt")] string blobContent) { } @@ -46,24 +69,237 @@ public class FunctionsClass public static void Run8() { } } + public class BindingAssertionItem + { + public string FunctionName { set; get; } + + public Dictionary[] Bindings { set; get; } + } + + public class BindingTestData : IEnumerable + { + public IEnumerator GetEnumerator() + { + yield return new object[] { + new BindingAssertionItem + { + FunctionName="MyHttpTrigger", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "httpTrigger" }, + {"name" , "request"}, + {"authLevel" , "function"} + } + } + } + }; + + yield return new object[] { + new BindingAssertionItem + { + FunctionName="HttpTriggerQueueReturn", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "httpTrigger" }, + {"name" , "request"}, + {"authLevel" , "function"} + }, + new Dictionary + { + {"type", "queue" }, + {"name" , "$return"}, + {"connection" , "MyStorageConnStrA"}, + {"queueName","myqueue-items-a" } + } + } + } + }; + + yield return new object[] { + new BindingAssertionItem + { + FunctionName="HttpTriggerQueueOutParam", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "httpTrigger" }, + {"name" , "request"}, + {"authLevel" , "function"} + }, + new Dictionary + { + {"type", "queue" }, + {"name" , "msg"}, + {"connection" , "MyStorageConnStrB"}, + {"queueName","myqueue-items-b" } + } + } + } + }; + + yield return new object[] { + new BindingAssertionItem + { + FunctionName="HttpTriggerMultipleOutputs", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "httpTrigger" }, + {"name" , "request"}, + {"authLevel" , "function"} + }, + new Dictionary + { + {"type", "queue" }, + {"name" , "qCollector"}, + {"connection" , "MyStorageConnStrC"}, + {"queueName","myqueue-items-c" } + }, + new Dictionary + { + {"type", "blob" }, + {"name" , "myBlob"}, + {"blobPath", "binding-metric-test/sample-text.txt" }, + {"connection" , "MyStorageConnStrC"} + } + } + } + }; + + yield return new object[] { + new BindingAssertionItem + { + FunctionName="MyBlobTrigger", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "blobTrigger" }, + {"name" , "blobContent"}, + {"path" , "blob.txt"} + } + } + } + }; + + yield return new object[] { + new BindingAssertionItem + { + FunctionName="MyEventHubTrigger", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "eventHubTrigger" }, + {"name" , "message"}, + {"eventHubName" , "hub"} + } + } + } + }; + + yield return new object[] { + new BindingAssertionItem + { + FunctionName="MyTimerTrigger", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "timerTrigger" }, + {"name" , "timer"}, + {"schedule" , "00:30:00"}, + {"useMonitor" , "True"}, + {"runOnStartup" , "False"} + } + } + } + }; + + yield return new object[] { + new BindingAssertionItem + { + FunctionName="MyServiceBusTrigger", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "serviceBusTrigger" }, + {"name" , "message"}, + {"queueName" , "queue"}, + {"isSessionsEnabled" , "False"} + } + } + } + }; + + yield return new object[] { + new BindingAssertionItem + { + FunctionName="MyManualTrigger", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "manualTrigger" }, + {"name" , "input"} + } + } + } + }; + + yield return new object[] { + new BindingAssertionItem + { + FunctionName="MyManualTriggerWithoutParameters", + Bindings = new Dictionary[] + { + new Dictionary + { + {"type", "manualTrigger" } + } + } + } + }; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + [Theory] - [InlineData("MyHttpTrigger", "httpTrigger", "request")] - [InlineData("MyBlobTrigger", "blobTrigger", "blobContent")] - [InlineData("MyQueueTrigger", "queueTrigger", "queue")] - [InlineData("MyEventHubTrigger", "eventHubTrigger", "message")] - [InlineData("MyTimerTrigger", "timerTrigger", "timer")] - [InlineData("MyServiceBusTrigger", "serviceBusTrigger", "message")] - [InlineData("MyManualTrigger", "manualTrigger", "input")] - [InlineData("MyManualTriggerWithoutParameters", "manualTrigger", null)] - public void FunctionMethodsAreExported(string functionName, string type, string parameterName) + [ClassData(typeof(BindingTestData))] + public void FunctionMethodsAreExported(BindingAssertionItem item) { var logger = new RecorderLogger(); var converter = new FunctionJsonConverter(logger, ".", ".", functionsInDependencies: false); - var functions = converter.GenerateFunctions(new[] { TestUtility.GetTypeDefinition(typeof(FunctionsClass)) }); - var schema = functions.Single(e => Path.GetFileName(e.Value.outputFile.DirectoryName) == functionName).Value.schema; - var binding = schema.Bindings.Single(); - binding.Value("type").Should().Be(type); - binding.Value("name").Should().Be(parameterName); + var functions = converter.GenerateFunctions(new[] { TestUtility.GetTypeDefinition(typeof(FunctionsClass)) }).ToArray(); + var schemaActual = functions.Single(e => Path.GetFileName(e.Value.outputFile.DirectoryName) == item.FunctionName).Value.schema; + + foreach (var expectedBindingItem in item.Bindings) + { + var expectedBindingType = expectedBindingItem.FirstOrDefault(a => a.Key == "type"); + + // query binding entry from actual using the type. + var matchingBindingFromActual = schemaActual.Bindings + .First(a => a.Properties().Any(g => g.Name == "type" + && g.Value.ToString()== expectedBindingType.Value)); + + // compare all props of binding entry from expected entry with actual. + foreach (var prop in expectedBindingItem) + { + // make sure the prop exist in the binding. + matchingBindingFromActual.ContainsKey(prop.Key).Should().BeTrue(); + + // Verify the prop values matches between expected and actual. + expectedBindingItem[prop.Key].Should().Be(matchingBindingFromActual[prop.Key].ToString()); + } + } + logger.Errors.Should().BeEmpty(); logger.Warnings.Should().BeEmpty(); }