Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include all bindings when generating function metadata #542

Merged
merged 6 commits into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions common.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<ContinuousIntegrationBuild Condition="'$(TF_BUILD)' == 'true'">true</ContinuousIntegrationBuild>

<MajorProductVersion>4</MajorProductVersion>
<MinorProductVersion>0</MinorProductVersion>
<PatchProductVersion>1</PatchProductVersion>
<MinorProductVersion>1</MinorProductVersion>
<PatchProductVersion>0</PatchProductVersion>

<!-- Clear this value for non-preview releases -->
<PreviewProductVersion></PreviewProductVersion>
Expand Down
50 changes: 43 additions & 7 deletions src/Microsoft.NET.Sdk.Functions.Generator/MethodInfoExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
Expand Down Expand Up @@ -59,15 +60,20 @@ public static JObject ManualTriggerBinding(this MethodDefinition method)
/// <returns><see cref="FunctionJsonSchema"/> object that represents the passed in <paramref name="method"/>.</returns>
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,
Expand All @@ -77,6 +83,36 @@ public static FunctionJsonSchema ToFunctionJson(this MethodDefinition method, st
};
}

/// <summary>
/// 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";
/// </summary>
private static JObject[] GetOutputBindingsFromReturnAttribute(MethodDefinition method)
Copy link
Member

@mathewc mathewc Feb 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to say that a return should/can only be bound once, but it looks like in this generator, we're handling 1->N (e.g. here). I'd expect at runtime that this would fail however, since parameter/return bindings must be 1:1. Not sure if we actually have explicit validation for this in the host or SDK. Ultimately the binding provider model for parameters is first one wins https://github.com/Azure/azure-webjobs-sdk/blob/88ed7806c63d9c589ecf1a44ec7575f1e72384e5/src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndexer.cs#L172.

{
if (method.MethodReturnType == null)
{
return Array.Empty<JObject>();
}

var outputBindings = new List<JObject>();
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();
}

/// <summary>
/// Gets a function name from a <paramref name="method"/>
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
Expand All @@ -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
Expand All @@ -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<string> qCollector)
{
myBlob = "foo-blob";
qCollector.AddAsync("foo-queue");
}

[FunctionName("MyBlobTrigger")]
public static void Run2([BlobTrigger("blob.txt")] string blobContent) { }

Expand All @@ -46,24 +69,237 @@ public static void Run7(string input) { }
public static void Run8() { }
}

public class BindingAssertionItem
{
public string FunctionName { set; get; }

public Dictionary<string, string>[] Bindings { set; get; }
}

public class BindingTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyHttpTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "httpTrigger" },
{"name" , "request"},
{"authLevel" , "function"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="HttpTriggerQueueReturn",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "httpTrigger" },
{"name" , "request"},
{"authLevel" , "function"}
},
new Dictionary<string, string>
{
{"type", "queue" },
{"name" , "$return"},
{"connection" , "MyStorageConnStrA"},
{"queueName","myqueue-items-a" }
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="HttpTriggerQueueOutParam",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "httpTrigger" },
{"name" , "request"},
{"authLevel" , "function"}
},
new Dictionary<string, string>
{
{"type", "queue" },
{"name" , "msg"},
{"connection" , "MyStorageConnStrB"},
{"queueName","myqueue-items-b" }
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="HttpTriggerMultipleOutputs",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "httpTrigger" },
{"name" , "request"},
{"authLevel" , "function"}
},
new Dictionary<string, string>
{
{"type", "queue" },
{"name" , "qCollector"},
{"connection" , "MyStorageConnStrC"},
{"queueName","myqueue-items-c" }
},
new Dictionary<string, string>
{
{"type", "blob" },
{"name" , "myBlob"},
{"blobPath", "binding-metric-test/sample-text.txt" },
{"connection" , "MyStorageConnStrC"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyBlobTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "blobTrigger" },
{"name" , "blobContent"},
{"path" , "blob.txt"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyEventHubTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "eventHubTrigger" },
{"name" , "message"},
{"eventHubName" , "hub"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyTimerTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "timerTrigger" },
{"name" , "timer"},
{"schedule" , "00:30:00"},
{"useMonitor" , "True"},
{"runOnStartup" , "False"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyServiceBusTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "serviceBusTrigger" },
{"name" , "message"},
{"queueName" , "queue"},
{"isSessionsEnabled" , "False"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyManualTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "manualTrigger" },
{"name" , "input"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyManualTriggerWithoutParameters",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"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<string>("type").Should().Be(type);
binding.Value<string>("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();
}
Expand Down