Web APIs mit Azure Functions: Projektstruktur (Teil 1)

post-thumb

Die Entwicklung und Bereitstellung von Web APIs mit Azure Functions bringt in größeren Projekten Herausforderungen mit sich. Dabei helfen eine entsprechende Projektstruktur und programmatische Erweiterungen wie auch Hilfsfunktionen dabei, effizient Web APIs mit Azure Functions zu entwickeln.

In den folgenden Wochen werde ich ein Best-Practice bereitstellen, welche sich auf die folgenden Beiträge aufteilt:

Überblick

Im ersten Teil werden einige bekannte Pattern vorgestellt, die insbesondere auf die Projektstruktur eines Web-API-Projekts mit Azure Functions angewandt werden kann. Hierbei kommen insbesondere das Clean Architecture Project Solution Pattern (Clean Architecture) wie auch das Command-Query-Responsibility-Segregation-Pattern (CQRS) zum Einsatz.

Clean Architecture Project Solution Pattern

Das Clean Architecture Project Solution Pattern (Clean Architecture) sieht für die Organisation der Solution mehrere Projekte vor, welche unterschiedliche Aufgaben übernehmen:

  • Domain
  • Infrastructure
  • Application
  • Function

Der Hauptvorteil hiervon ist die Abgrenzung der jeweiligen Zuständigkeiten. Im Zentrum steht das Domain-Projekt, auf welches die jeweiligen Entitäten einer Anwendung darstellt.

Domain

Das Domain-Projekt stellen die Domänen-Modelle der Geschäftsanwendung der Solution bereit. In den Entitätsklassen können nicht nur die Daten gehalten werden, es können auch unternehmensweite Regeln (Business Logik) darin definiert werden, welche von allen Projekten gleichermaßen verwendet werden kann.

Im Zuge dieses Beitrags werden die Domain-Klassen lediglich zur Datenhaltung der entsprechenden Entitäten verwendet.

Infrastructure

Das Infrastructure-Projekt implementiert entsprechende Service-Aufrufe auf außenliegende Dienste und Datenquellen her und stellt somit Funktionen für die spätere Entwicklung der Web APIs bereit. So werden Zugriffe auf die entsprechenden Ressourcen orchestriert, z.B. Funktionen um Datensätze in einem Azure Table Storage zu speichern oder abzurufen.

Application

Das Application-Projekt bildet die Schnittstelle zwischen dem Application.Function-Projekt und den Domain- und Infrastructure-Projekten. In diesem Projekt werden sogenannte Command- und Query-Modelle, welche einen entsprechenden Request an die Web API repräsentieren, bereitgestellt. Auf diese Modelle und das dahinterliegende CQRS-Pattern wird im weiteren Verlauf noch genauer eingegangen.

Vom Application-Projekt werden verweise auf das Domain-Projekt und das Infrastructure-Projekt gesetzt, um die jeweiligen Entitäten (Domain) und Services (Infrastructure) zu verwenden.

Außerdem kann in diesem Projekt bereits die Dependency Injection umgesetzt werden. Hierdurch können die notwendigen Services aus dem Infrastructure-Projekt oder andere Abhängigkeiten wie z.B. FluentValidation in der späteren Entwicklung von Azure Functions verwendet werden, indem die entsprechenden Services dann über den Konstruktor injiziert werden.

Application.Function

Im Application.Function-Projekt werden die eigentlichen Azure Functions erstellt und die Web APIs hierüber bereitgestellt. Die Function greift auf alle notwendigen Services und Domain-Klassen zu, um die entsprechende Business Logik abzubilden.

Command-Query-Responsibility-Segregation-Pattern (CQRS)

Das CQRS-Pattern löst das Problem, dass z.B. Abfragen oder die Anlage in einer Datenbank das gleiche Datenmodell verwendet. Dabei unterscheiden sich beide Arten des Zugriffs grundlegend voneinander. So kann eine Abfrage den Wert einer ID bereits beinhalten, wohingegen eine Anlage einer Entität den Wert der ID noch nicht kenne, da dies beispielsweise von der Datenbank erst zum Zeitpunkt des Speicherns erzeugt wird. Genau hier greift das CQRS-Pattern.

Das bedeutet die Business Logik zwischen Commands und Queries wird getrennt. Auf HTTP Web APIs angewandt können diese folgendermaßen dargestellt werden:

  • Queries: GET-Methoden
  • Commands: POST-/ PUT-/ DELETE-Methoden

Durch die Verwendung von Command und Query Objekten besteht eine lose Kopplung zwischen der jeweiligen Anforderung an die Ziel-Entität und die Ziel-Entität selbst.

Weitere Vorteile:

  • Übersichtliche Ziel-Entität mit einer Liste an Commands und Queries
  • Isolierung jedes Commands innerhalb einer Ziel-Entität
  • Einfache Definition der einzelnen Commands und Queries
  • Einfache Erweiterbarkeit weiterer Commands und Queries

Auf Web APIs mit Azure Functions angewandt, repräsentieren die Commands und Queries die entsprechenden HTTP-Requests.

Beispielhafte Implementierung

Die beiden vorgestellten Pattern werden anhand eines Beispiels anschaulich dargestellt. Hierbei liegt der Fokus primär auf dem CQRS-Pattern. Damit wird die Konvertierung eines Requests in ein Command-/Query-Objekt betrachtet.

Die Betrachtung von Services, welche im Infrastructure-Projekt definiert werden können, ist nicht Teil des Beispiels.

Die Entitäten werden im Domain-Projekt, die Commands-/Query-Objekte im Application-Projekt und die Azure Functions im Application.Functions-Projekt angelegt.

Die verwendete Entität der beispielhaften Implementierung orientieren sich am Petstore von Swagger.

Domain

Die Domain-Klasse Pet beschreibt die Tier-Entität, welche einige Attribute, wie den Namen oder die Verfügbarkeit beinhaltet.

namespace Domain
{
    public class Pet
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Species Species { get; set; }
        public Availability Status { get; set; }
    }
 
    public enum Species
    {
        Dog,
        Cat,
        Bird
    }
 
    public enum Availability
    {
        Available,
        Pending,
        Sold
    }
}

Application

Für den Zugriff auf die Pet-Entität stehen vier Funktionen zur Verfügung:

  • Suche nach einem Pet mit der ID (GET: FindPetByIdQuery)
  • Auflistung aller Pets (GET: GetPetQuery)
  • Speichern eines Pets (POST: StorePetCommand)
  • Aktualisierung eines Pets (POST: UpdatePetCommand)
using Domain;
 
namespace Application.Pet
{
    public class GetPetQuery
    {
        public Species Species { get; set; }
    }
}

Die Query-Klasse GetPetQuery enthält lediglich das Attribut Species, welches für die Suche nach Pet-Entitäten mittels Tierart verwendet wird. Die Species wird über Query-Parameter im Request angegeben.

Application.Function

Die Implementierung der Azure Function beinhaltet die Zuordnung der Query-Parameter zum entsprechenden Attribut Species von GetPetQuery.

using System;
using System.Threading.Tasks;
using Application.Pet;
using Domain;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
 
namespace Application.Function
{
    public static class GetPetQueryFunction
    {
        [FunctionName("GetPetQueryFunction")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "pet")] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");
 
            var query = new GetPetQuery();
 
            query.Species = (Species) Enum.Parse(typeof(Species), req.Query["Species"]);
 
            // Call-Search-Service
 
            return new EmptyResult();
        }
    }
}

Das Beispiel zeigt ganz gut die Grundsätzliche Funktionswiese des CQRS-Patterns und die damit verbundene Trennung von Zuständigkeiten. So wird das Command-/ Query-Objekt als Transferobjekt verwendet, um die jeweilige Anforderung in Richtung der Ziel-Entität umzusetzen.

Allerdings müssen Zuordnungen, wie die des Query-Parameters manuell erfolgen. Somit müssen notwendige Konvertierungen des Requests in jeder Azure Function durchgeführt werden. Hierzu gehört neben der Zuordnung von Query- und Header-Parametern, auch das mappen der Attribute, welche über den Request-Body angegeben werden (bei einem POST-/ PUT-Request).

Zusammenfassung

Die Verwendung des Clean Architecture Pattern stellt eine gute Struktur des Projektes dar und stellt die jeweilige Zuständigkeit des Projektes dar. Die Verwendung des CQRS-Patterns stellt eine Trennung der Requests von der Ziel-Entität sicher und bietet die Möglichkeit neue Zugriffe (neue Web APIs) hierüber zu orchestrieren.

Allerdings hat die beispielhafte Implementierung gezeigt, dass die Umwandlung der Requests in die jeweiligen Command- und Query-Objekte in der Azure Function einen wiederkehrenden Aufwand bedeutet und in jeder Azure Function individuell angewandt werden muss. Um die Entwicklung effizient zu gestalten wird eine Erweiterung benötigt, die den Request entsprechend in das jeweilige Query-/ Command-Objekt konvertiert. Im nächsten Teil wird dieser Umstand aufgegriffen und eine Lösung für dieses Problem bereitgestellt.

Lernen Sie uns kennen

Das sind wir

Wir sind ein Software-Unternehmen mit Hauptsitz in Karlsruhe und auf die Umsetzung von Digitalstrategien durch vernetzte Cloud-Anwendungen spezialisiert. Wir sind Microsoft-Partner und erweitern Standard-Anwendungen bei Bedarf – egal ob Modernisierung, Integration, Implementierung von CRM- oder ERP-Systemen, Cloud Security oder Identity- und Access-Management: Wir unterstützen Sie!

Mehr über uns

Der Objektkultur-Newsletter

Mit unserem Newsletter informieren wir Sie stets über die neuesten Blogbeiträge,
Webcasts und weiteren spannenden Themen rund um die Digitalisierung.

Newsletter abonnieren