Skip to content
Closed
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
9 changes: 8 additions & 1 deletion src/DotNetWorker.Core/Invocation/DefaultMethodInfoLocator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Reflection;
#if NET6_0_OR_GREATER
using System.Runtime.Loader;
Expand Down Expand Up @@ -36,7 +37,13 @@ public MethodInfo GetMethod(string pathToAssembly, string entryPoint)

Type? functionType = assembly.GetType(typeName);

MethodInfo? methodInfo = functionType?.GetMethod(methodName);
var methods = functionType?.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly).Where(m => m.Name == methodName).ToArray();
MethodInfo? methodInfo = methods?.Length switch
{
1 => methods[0],
> 1 => methods.SingleOrDefault(m => m.GetCustomAttributes().Any(a => a.GetType().Name == "FunctionAttribute")),
_ => null
};

if (methodInfo == null)
{
Expand Down
143 changes: 143 additions & 0 deletions test/DotNetWorker.Tests/GrpcFunctionDefinitionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,110 @@ public void GrpcFunctionDefinition_TableInput_Creates()
});
}

[Fact]
public void GrpcFunctionDefinition_OverloadedMethod_Creates()
{
using var testVariables = new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_DIRECTORY", ".");

var methodInfoLocator = new DefaultMethodInfoLocator();

string fullPathToThisAssembly = GetType().Assembly.Location;
var functionLoadRequest = new FunctionLoadRequest
{
FunctionId = "abc",
Metadata = new RpcFunctionMetadata
{
EntryPoint = $"Microsoft.Azure.Functions.Worker.Tests.{nameof(GrpcFunctionDefinitionTests)}+{nameof(MyOverloadedFunctionClass)}.{nameof(MyOverloadedFunctionClass.Run)}",
ScriptFile = Path.GetFileName(fullPathToThisAssembly),
Name = "myfunction"
}
};

// We base this on the request exclusively, not the binding attributes.
functionLoadRequest.Metadata.Bindings.Add("req", new BindingInfo { Type = "HttpTrigger", Direction = Direction.In });
functionLoadRequest.Metadata.Bindings.Add("$return", new BindingInfo { Type = "Http", Direction = Direction.Out });

FunctionDefinition definition = functionLoadRequest.ToFunctionDefinition(methodInfoLocator);

Assert.Equal(functionLoadRequest.FunctionId, definition.Id);
Assert.Equal(functionLoadRequest.Metadata.EntryPoint, definition.EntryPoint);
Assert.Equal(functionLoadRequest.Metadata.Name, definition.Name);
Assert.Equal(fullPathToThisAssembly, definition.PathToAssembly);

// Parameters - should match the overload with [Function] attribute
Assert.Collection(definition.Parameters,
p =>
{
Assert.Equal("req", p.Name);
Assert.Equal(typeof(HttpRequestData), p.Type);
});

// InputBindings
Assert.Collection(definition.InputBindings,
p =>
{
Assert.Equal("req", p.Key);
Assert.Equal(BindingDirection.In, p.Value.Direction);
Assert.Equal("HttpTrigger", p.Value.Type);
});

// OutputBindings
Assert.Collection(definition.OutputBindings,
p =>
{
Assert.Equal("$return", p.Key);
Assert.Equal(BindingDirection.Out, p.Value.Direction);
Assert.Equal("Http", p.Value.Type);
});
}

[Fact]
public void GrpcFunctionDefinition_OverloadedMethod_NoFunctionAttribute_ThrowsException()
{
using var testVariables = new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_DIRECTORY", ".");

var methodInfoLocator = new DefaultMethodInfoLocator();

string fullPathToThisAssembly = GetType().Assembly.Location;
var functionLoadRequest = new FunctionLoadRequest
{
FunctionId = "abc",
Metadata = new RpcFunctionMetadata
{
EntryPoint = $"Microsoft.Azure.Functions.Worker.Tests.{nameof(GrpcFunctionDefinitionTests)}+{nameof(MyOverloadedFunctionClassNoAttribute)}.{nameof(MyOverloadedFunctionClassNoAttribute.Run)}",
ScriptFile = Path.GetFileName(fullPathToThisAssembly),
Name = "myfunction"
}
};

// This should fail because neither overload has [Function] attribute
var exception = Assert.Throws<InvalidOperationException>(() => functionLoadRequest.ToFunctionDefinition(methodInfoLocator));
Assert.Contains("was not found", exception.Message);
}

[Fact]
public void GrpcFunctionDefinition_OverloadedMethod_MultipleFunctionAttributes_ThrowsException()
{
using var testVariables = new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_DIRECTORY", ".");

var methodInfoLocator = new DefaultMethodInfoLocator();

string fullPathToThisAssembly = GetType().Assembly.Location;
var functionLoadRequest = new FunctionLoadRequest
{
FunctionId = "abc",
Metadata = new RpcFunctionMetadata
{
EntryPoint = $"Microsoft.Azure.Functions.Worker.Tests.{nameof(GrpcFunctionDefinitionTests)}+{nameof(MyOverloadedFunctionClassMultipleAttributes)}.{nameof(MyOverloadedFunctionClassMultipleAttributes.Run)}",
ScriptFile = Path.GetFileName(fullPathToThisAssembly),
Name = "myfunction"
}
};

// This should fail because multiple overloads have [Function] attribute
Assert.Throws<InvalidOperationException>(() => functionLoadRequest.ToFunctionDefinition(methodInfoLocator));
}

private class MyFunctionClass
{
public HttpResponseData Run(HttpRequestData req)
Expand Down Expand Up @@ -297,5 +401,44 @@ public HttpResponseData Run(
return req.CreateResponse();
}
}

private class MyOverloadedFunctionClass
{
// Helper overload - no Function attribute
public void Run(string message) { }

// Actual function - has Function attribute
[Function("MyFunction")]
public HttpResponseData Run(HttpRequestData req)
{
return req.CreateResponse();
}
}

private class MyOverloadedFunctionClassNoAttribute
{
// Overload 1 - no Function attribute
public void Run(string message) { }

// Overload 2 - also no Function attribute
public void Run(int count) { }
}

private class MyOverloadedFunctionClassMultipleAttributes
{
// Overload 1 - has Function attribute
[Function("MyFunction1")]
public HttpResponseData Run(HttpRequestData req)
{
return req.CreateResponse();
}

// Overload 2 - also has Function attribute
[Function("MyFunction2")]
public HttpResponseData Run(string message)
{
throw new NotImplementedException();
}
}
}
}