4 min read

OpenAPI Extension to Support Azure Functions V1

Justin Yoo

There is an open-source project that generates an Open API document on-the-fly on an Azure Functions app. The open-source project also provides a NuGet package library. This project has been supporting the whole Azure Functions runtimes from v1 since Day 1. Although it does, the v1 runtime has got its own limitation that cannot generate the Open API document automatically. But, as always, there's a workaround. I'm going to show how to generate the Open API document on-the-fly and execute the v1 function through the Azure Function Proxy feature.

Legacy V1 Azure Function

Generally speaking, many legacy enterprise applications still need Azure Functions v1 runtime due to their reference dependencies. Let's assume that the Azure Functions v1 endpoint looks like the following:

namespace MyV1LegacyFunctionApp
{
public static class LoremIpsumHttpTrigger
{
[FunctionName("LoremIpsumHttpTrigger")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, "GET", Route = "lorem/ipsum")] HttpRequestMessage req,
ILogger log)
{
var content = new MyReturnObject();
var result = req.CreateResponse(HttpStatusCode.OK, content);
return await Task.FromResult(result).ConfigureAwait(false); }
}
}
view raw 01-v1-legacy.cs hosted with ❤ by GitHub

The v1 runtime has a strong tie to the Newtonsoft.Json package version 9.0.1. Therefore, if the return object of MyReturnObject has a dependency on Newtonsoft.Json v10.0.1 and later, the Open API extension cannot be used.

Azure Functions Proxy for Open API Document

The Azure Functions Proxy feature comes the rescue! Although it's not a perfect solution, it provides with the same developer experience, which is worth trying. Let's build an Azure Functions app targeting the v3 runtime. The name of the proxy function is MyV1ProxyFunctionApp (line #1). All the rest are set to be the same as the legacy v1 app (line #3-7). However, make sure this is the proxy purpose, meaning it does nothing but returns an OK response (line #10).

namespace MyV1ProxyFunctionApp
{
public static class LoremIpsumHttpTrigger
{
[FunctionName("LoremIpsumHttpTrigger")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "GET", Route = "lorem/ipsum")] HttpRequest req,
ILogger log)
{
return await Task.FromResult(new OkResult()).ConfigureAwait(false);
}
}
}
view raw 02-v1-proxy.cs hosted with ❤ by GitHub

Once installed the Open API library, let's add decorators above the FunctionName(...) decorator (line #5-9).

namespace MyV1ProxyFunctionApp
{
public static class LoremIpsumHttpTrigger
{
[OpenApiOperation(operationId: "getIpsum", tags: new[] { "ipsum" }, Summary = "Gets Ipsum from Lorem", Description = "This gets Ipsum from Lorem.", Visibility = OpenApiVisibilityType.Important)]
[OpenApiParameter(name: "name", In = ParameterLocation.Query, Required = true, Type = typeof(string), Summary = "Lorem name", Description = "Lorem name", Visibility = OpenApiVisibilityType.Important)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(MyReturnObject), Summary = "The Ipsum response", Description = "This returns the Ipsum response")]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NotFound, Summary = "Name not found", Description = "Name parameter is not found")]
[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.BadRequest, Summary = "Invalid Lorem", Description = "Lorem is not valid")]
[FunctionName("LoremIpsumHttpTrigger")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "GET", Route = "lorem/ipsum")] HttpRequest req,
ILogger log)
{
return await Task.FromResult(new OkResult()).ConfigureAwait(false);
}
}
}

All done! Run this proxy app, and you will be able to see the Swagger UI page. As I mentioned above, this app doesn't work but show the UI page. For this app to work, extra work needs to be done.

proxies.json to Legacy Azure Functions V1

Add the proxies.json file to the root folder. As we added the same endpoint as the legacy function app on purpose (line #6,11), API consumers should have the same developer experience as before except the hostname change. In addition to that, both querystring values and request headers are relayed to the legacy app (line #13-14).

{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"DummyOnOff": {
"matchCondition": {
"route": "/api/lorem/ipsum",
"methods": [
"GET"
]
},
"backendUri": "https://mylegacyfunctionapp.azurewebsites.net/api/lorem/ipsum",
"requestOverrides": {
"backend.request.headers": "{request.headers}",
"backend.request.querystring": "{request.querystring}"
}
}
}
}
view raw 04-proxies.json hosted with ❤ by GitHub

Then update the .csproj file to deploy the proxies.json file together (line #10-12).

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
...
</PropertyGroup>
...
<ItemGroup>
...
<None Update="proxies.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
...
</Project>
view raw 05-v1-proxy.csproj hosted with ❤ by GitHub

All done! Run this proxy function on your local machine or deploy it to Azure, and hit the proxy API endpoint. Then you'll be able to see the Open API document generated on-the-fly and execute the legacy API through the proxy.


So far, we have created an Azure Functions app using the Azure Functions Proxy feature. It also supports the Open API document generation for the v1 runtime app. The flip-side of this approach costs doubled because all API requests hit the proxy then the legacy. The cost optimisation should be investigated from the enterprise architecture perspective.