In meiner Serie zur Erstellung von Web APIs mit Azure Functions stelle ich neben einigen Pattern auch Codebeispiele vor. In Summe können diese Beiträge als Handlungsanweisung verwendet werden, um effizient Web APIs mit Azure Functions zu erstellen.
Hier nochmal eine Übersicht der bisher veröffentlichten Beiträge:
Überblick
Mit der Umsetzung der Funktionalität um ein Responsehandling, sind wir nun in der Lage, effizient Web APIs zu entwickeln. Um den Nutzern unserer Schnittstelle die Verwendung zu erleichtern und einfacher zugänglich zu machen, soll diese dokumentiert werden.
Swagger/OpenAPI
Swagger unterstützt mit seinen OpenSource-Tools die Konzeption, Entwicklung und Dokumentation von Webservices auf Basis von HTTP. Dabei setzt Swagger auf den Beschreibungsstandard OpenAPI. Beide Begriffe werden äquivalent zueinander verwendet.
In diesem Beitrag möchte ich mich primär auf die Dokumentation von Webservices mittels OpenAPI/Swagger beschränken.
Benutzeroberfläche
Swagger generiert auf Basis der openapi.json eine webbasierte Bedienungsoberfläche und stellt die jeweiligen Funktionen grafisch dar.
Die einzelnen Funktionen erweitern die Sicht bei einem weiteren Klick und stellen nicht nur dar, welche Parameter für einen erfolgreichen Aufruf notwendig sind (z.B. Query-, Path-Parameter, Beispielbody), sondern stellen daneben auch mögliche Antworten des Endpunkts dar (HTTP-Statuscode). Darüber hinaus ist es möglich, die API im Browser direkt zu testen.
API Dokumentation in Azure Functions
Für .NET stehen im Wesentlichen zwei Implementierungen für die OpenAPI zur Verfügung: NSwag und Swashbuckle. Wobei auf Azure Functions bezogen lediglich Swashbuckle einen Support für die V3-Runtime von Azure Functions bietet. Zu finden ist eine entsprechende Implementierung als Nuget-Paket unter AzureExtensions.Swashbuckle.
Außerdem arbeitet Microsoft an dem Nuget-Paket Azure Functions OpenAPI Extension, welches die Verwendung von OpenAPI mit Azure Functions unterstützt. Unter dem Namen Microsoft.Azure.WebJobs.Extensions.OpenApi kann das entsprechende Nuget-Paket bezogen werden. Allerdings sei hier erwähnt, dass es aktuell eine Preview ist. Daher ist von einem produktiven Einsatz hier zunächst abzuraten.
Swashbuckle vs. Microsoft OpenAPI-Extension
Um eine Entscheidungsgrundlage bereitzustellen, werden die Lösungen von Swashbuckle wie auch Microsoft zur Dokumentation von Endpunkten mit Azure Functions gegenübergestellt. Vorweg sei zu sagen, dass eine finale Version (unabhängig vom Ersteller des Nuget-Paketes) einer Preview-/Beta-Version eines Paketes immer vorzuziehen ist. In diesem Zusammenhang kommt entscheidend hinzu, dass Swashbuckle bereits im ASP.NET-Umfeld eine breite Nutzung aufweisen kann und entsprechend auch für die produktive Verwendung der Azure Functions verwendet werden kann.
Einrichtung
Beide Lösungen bieten die Möglichkeit, die entsprechenden Pakete entweder direkt (static) oder mithilfe von Dependency Injection (IoC (Inversion of Control)) im Projekt verfügbar zu machen.
Die Verwendung von Microsoft.Azure.WebJobs.Extensions.OpenApi kann bereits beim Erstellen des Azure-Functions-Projekt bzw. bei der Erstellung einer neuen Function über den Wizzard in Visual Studio automatisiert hinzugefügt werden. Hierzu muss lediglich der entsprechende Haken gesetzt werden (siehe Screenshot).
Für die Verwendung von Swashbuckle wird die Startup-Klasse benötigt, um Swashbuckle korrekt in das Projekt einzubinden. Im folgenden Listing wird der entsprechende Code mit rudimentären Einstellungen für die Swagger-Dokumentation dargestellt.
using System.Reflection;
using Application.Functions;
using AzureFunctions.Extensions.Swashbuckle;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
[assembly: FunctionsStartup(typeof(Startup))]
namespace Application.Functions
{
using AzureFunctions.Extensions.Swashbuckle.Settings;
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.AddSwashBuckle(Assembly.GetExecutingAssembly(), opts =>
{
opts.Documents = new[]
{
new SwaggerDocument
{
Name = "v1",
Title = "Swagger Application.Functions",
Description = "Description",
Version = "v1"
}
};
opts.Title = "Swagger Documentation for my Azure Functions";
});
}
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
base.ConfigureAppConfiguration(builder);
var context = builder.GetContext();
}
}
}
Außerdem werden zwei Functions benötigt, um zum einen die Swagger-Oberfläche und zum anderen die entsprechende OpenAPI-Definition (JSON) darstellen zu können.
namespace Application.Functions
{
using System.Net.Http;
using System.Threading.Tasks;
using AzureFunctions.Extensions.Swashbuckle;
using AzureFunctions.Extensions.Swashbuckle.Attribute;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
public static class SwaggerController
{
[SwaggerIgnore]
[FunctionName("OpenApiJson")]
public static Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "swagger/json")]
HttpRequestMessage req,
[SwashBuckleClient] ISwashBuckleClient swashBuckleClient)
{
return Task.FromResult(swashBuckleClient.CreateSwaggerDocumentResponse(req));
}
[SwaggerIgnore]
[FunctionName("OpenApiUi")]
public static Task<HttpResponseMessage> Run2(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "swagger/ui")]
HttpRequestMessage req,
[SwashBuckleClient] ISwashBuckleClient swashBuckleClient)
{
return Task.FromResult(swashBuckleClient.CreateSwaggerUIResponse(req, "openapi/json"));
}
}
}
Dokumentation von OpenAPI
Die entsprechende Dekoration der Azure Functions unterscheidet sich minimal in der Verwendung. Um einen Vergleich zu erhalten, sind im folgenden zwei Listings, welche die Function StoreTodoItemCommandFunction darstellen. Zum einen mit Swashbuckle (erstes Listing) zum anderen mit Microsoft OpenAPI-Extension (zweites Listing).
using System;
using System.Net;
using System.Threading.Tasks;
using Application.Todo;
using AzureFunctions.Extensions.Swashbuckle.Attribute;
using Functions.Extension.Handler;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
namespace Application.Functions.Todo
{
public static class StoreTodoItemCommandFunction
{
[FunctionName("StoreTodoItemCommandFunction")]
[ApiExplorerSettings(GroupName = "todo")]
[ProducesResponseType(typeof(StoreTodoItemCommand), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = "Todo")] HttpRequest req,
[RequestBodyType(typeof(StoreTodoItemCommand), "StoreTodoItemCommand Request-Body")],
ILogger log)
{
Task<IActionResult> result = null;
try
{
var command = await req.ConvertRequestToCommand<StoreTodoItemCommand>();
// Access Database and Delete
// todo
result = req.CreateHttpResponse()
.AddHeaderEntry("MyHeader", "MyKey")
.Send(command);
}
catch (Exception ex)
{
result = req.CreateHttpResponse()
.SendFromException(ex);
}
return await result;
}
}
}
using System;
using System.Net;
using System.Threading.Tasks;
using Application.Todo;
using Functions.Extension.Handler;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
namespace Application.Functions.Todo
{
public static class StoreTodoItemCommandFunction
{
[FunctionName("StoreTodoItemCommandFunction")]
[OpenApiOperation(operationId: "Run", tags: new[] { "name" })]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(StoreTodoItemCommand), Description = "The OK response")]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = "Todo")] HttpRequest req, ILogger log)
{
Task<IActionResult> result = null;
try
{
var command = await req.ConvertRequestToCommand<StoreTodoItemCommand>();
// Access Database and Delete
// todo
result = req.CreateHttpResponse()
.AddHeaderEntry("MyHeader", "MyKey")
.Send(command);
}
catch (Exception ex)
{
result = req.CreateHttpResponse()
.SendFromException(ex);
}
return await result;
}
}
}
Handlungsempfehlung
Auch wenn die Integration der Microsoft-OpenAPI-Extension bereits in Visual Studio existiert und grundsätzlich auch gut funktioniert, ist die Verwendung von Swashbuckle vorzuziehen, da Swashbuckle zum einen eine breite Verwendung aufweist und sich die Microsoft-OpenAPI-Extension zum anderen nach wie vor in einer Preview (beta) befindet und somit grundsätzlich nicht für den produktiven Einsatz geeignet ist.
Beispielhafte Implementierung
Im bestehenden GitHub-Repo
aus dem letzten Beitrag wurden die Endpunkte um eine OpenAPI-Definition mithilfe von Swashbuckle erweitert.
Zusammenfassung
Mithilfe von Swagger und Swashbuckle sind die entwickelten Endpunkte nun auch dokumentiert, sodass Nutzer von außerhalb sehen können, wie die APIs verwendet werden müssen und testen können, wie der Endpunkt sich verhält (Beispiel-Body des Responses; Response-HTTP-Statuscodes usw.).
Wenn wir nun mehrere Projekte mit Azure Functions entwickeln, möchten wir den Nutzern der Dienste nicht unterschiedliche Function-Host-URLs mitteilen müssen. Eine einheitliche und zentrale Host-URL ist deutlich komfortabler. Dies kann beispielsweise mit einem API Management (APIM) realisiert werden. Das APIM agiert hierbei lediglich als Fassade für den Aufrufer, welche den Aufruf an die jeweilige Function weiterleitet. Somit sieht der Aufrufer unter anderem auch nicht, welche Function nun konkret aufgerufen wurde.
Um die Verwaltung des APIMs zu vereinfachen, ist eine entsprechende Konfiguration mithilfe der OpenAPI-Definitionen notwendig. Das Ziel ist also die Azure Functions nach dem Deployment in Azure automatisiert in das APIM einzubinden. Das soll auch das Thema für den nächsten Teil dieser Serie sein.