Azure / Azure/azure-functions-dotnet-worker
FunctionMetadataProviderGenerator Generates duplicate endpoints if partial class/methods used
- Dominant language
- C#
- Stars
- 466
- Forks
- 215
- Avg merge
- 3d 10h
- Merged PRs (30d)
- 7
Description
## Background
We use the Microsoft.Azure.Functions.Worker.Extensions.OpenApi library to generate OpenAPI specs for our HttpTriggers and upgrading from .NET 6 InProcess to .NET 8 Isolated
Because of the number of attributes we typically add to a function endpoint to describe it, we separated each function endpoint into 2 partial methods so we can keep the "noise" of the OpenAPI attributes away from the actual http trigger implementation.
## Issue
We have found that the FunctionMetadataProvider source generator incorrectly duplicates the endpoint metadata, treating the empty partial method another endpoint.
## Reproduction Steps
So, starting with the default function template ( removed the post verb for simplicity )
1. Add partial to the class "Function1"
2. Add partial to the Run method
3. Add a new file "Function1.OpenApi.cs"
4. Add the same corresponding empty partial method
5. Add "OpenApiOperation" attribute.
When you add the OpenApiOperation attribute, the FunctionMetadataProviderGenerator source generator duplicates metadata items in GetFunctionMetadataAsync function.
Adding *any* attribute to the empty partial declaration replicates the situation.
This scenario worked fine in .net 6.0 - InProcess.
``` c#
// Function1.cs
namespace FunctionApp1
{
public partial class Function1
{
private readonly ILogger _logger;
public Function1(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger();
}
[Function("Function1")]
public partial HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
response.WriteString("Welcome to Azure Functions!");
return response;
}
}
}
```
``` c#
// Function1.OpenApi.cs
namespace FunctionApp1
{
public partial class Function1
{
[OpenApiOperation("Function1", tags: new[] { "Thingy" }, Summary = "Process event.", Visibility = OpenApiVisibilityType.Important)]
public partial HttpResponseData Run(HttpRequestData req);
}
}
```
```c#
// GeneratedFunctionMetadataProvider.g.cs
namespace FunctionApp1
{
///
/// 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"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Function"",""methods"":[""get""]}");
Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}");
var Function0 = new DefaultFunctionMetadata
{
Language = "dotnet-isolated",
Name = "Function1",
EntryPoint = "FunctionApp1.Function1.Run",
RawBindings = Function0RawBindings,
ScriptFile = "FunctionApp1.dll"
};
metadataList.Add(Function0);
var Function1RawBindings = new List();
Function1RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Function"",""methods"":[""get""]}");
Function1RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}");
var Function1 = new DefaultFunctionMetadata
{
Language = "dotnet-isolated",
Name = "Function1",
EntryPoint = "FunctionApp1.Function1.Run",
RawBindings = Function1RawBindings,
ScriptFile = "FunctionApp1.dll"
};
metadataList.Add(Function1);
...
}
}
```
```
Azure Functions Core Tools
Core Tools Version: 4.0.5455 Commit hash: N/A (64-bit)
Function Runtime Version: 4.27.5.21554
[2023-12-06T06:12:19.927Z] Found C:\src\Temp\FunctionApp1\FunctionApp1.csproj. Using for user secrets file configuration.
[2023-12-06T06:12:23.177Z] Worker process started and initialized.
[2023-12-06T06:12:23.202Z] Loading function failed.
[2023-12-06T06:12:23.204Z] System.Linq: Sequence contains more than one matching element.
[2023-12-06T06:12:23.221Z] Loading function failed.
[2023-12-06T06:12:23.224Z] System.Linq: Sequence contains more than one matching element.
[2023-12-06T06:12:23.326Z] The 'Function1' function is in error: The route specified conflicts with the route defined by function 'Function1'.
Functions:
Function1: [GET] http://localhost:7051/api/Function1
RenderOAuth2Redirect: [GET] http://localhost:7051/api/oauth2-redirect.html
RenderOpenApiDocument: [GET] http://localhost:7051/api/openapi/{version}.{extension}
RenderSwaggerDocument: [GET] http://localhost:7051/api/swagger.{extension}
RenderSwaggerUI: [GET] http://localhost:7051/api/swagger/ui
For detailed output, run func with --verbose flag.
[2023-12-06T06:12:24.568Z] Worker process started and initialized.
[2023-12-06T06:12:24.595Z] Loading function failed.
[2023-12-06T06:12:24.596Z] System.Linq: Sequence contains more than one matching element.
[2023-12-06T06:12:24.676Z] Exceeded language worker restart retry count for runtime:dotnet-isolated. Shutting down and proactively recycling the Functions Host to recover
C:\Program Files\dotnet\dotnet.exe (process 24736) exited with code 0.
```
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.