Web APIs mit Azure Functions: Responsehandling (Teil 3)

post-thumb

In meiner Serie zum Erstellen von Web APIs mit Azure Functions stelle ich neben einigen Pattern auch Codebeispiele vor. In Summe sollen diese Beiträge als Handlungsanweisung dienen, um effizient Web APIs mit Azure Functions zu erstellen.

Hier nochmal eine Übersicht der bisher veröffentlichten Beiträge:

Überblick

Nachdem ich im zweiten Teil eine Erweiterung zum Requesthandling vorgestellt habe, möchte ich in diesem Beitrag auf das Response-Handling eingehen. Hierzu möchte ich die bestehende Extension aus dem letzten Beitrag nutzen und eine generische Lösung bereitstellen, wie Antworten einer Web API mit Azure Functions einfach erstellt werden können.

Response Handling in Azure Functions

Wenn eine Function einen Response sendet, beinhaltet die Antwort i.d.R. folgende Komponenten:

  • Header-Parameter
  • Contenttype (spezieller Header)
  • Http-Statuscode
  • Content

Das bedeutet abhängig von der vorgenommenen Aktion (GET, PUT, DELETE usw.) erhält der Aufrufer neben dem Inhalt auch einen entsprechenden Http-Statuscode, der dem Aufrufer über den Erfolg, oder Misserfolg des Aufrufs mitteilt.

Fluent Builder Pattern

Für die Erzeugung von neuen Objekten verwenden Entwickler für gewöhnlich (Klassen-) Konstruktoren, sodass Parameter korrekt zugewiesen werden.

Für unser Szenario der Erstellung eines Response-Objekts ist eine mögliche Implementierung der HttpResponse-Klasse im Folgenden dargestellt:

public class HttpResponse
{
        public int StatusCode;
        public string ContentType;
        public string Content;
        public IDictionary<string string> Headers;
 
public HttpResponse(int? statusCode, string contentType, string content, IDictionary<string string> headers)
        {
            this.StatusCode = StatusCode;
            this.ContentType = ContentType;
            this.Content = Content;
            this.Headers = Headers;
        }
}

Bei diesem Beispiel wird sofort klar, eine Erzeugung eines HttpResponse-Ojbekt ist mit einem größeren Aufwand verbunden, da zum einen sämtliche Informationen zum Zeitpunkt der Erzeugung des Response-Objekts vorliegen müssen und zum anderen die Leserbarkeit beim Aufruf leidet.

Beispielhafter Aufruf:

var response = new HttpResponse(200, "application/json", "", headers);

In C# kann durch die Verwendung von statischen Extension-Methoden (ähnlich zu den Request-Handling-Methoden aus dem letzten Beitrag) das Fluent Builder Pattern angewandt werden.

Das Fluent Builder Pattern hilft dabei die Logik zur Erstellung eines Objekts in einen Builder zu extrahieren. Hierbei ist der Builder für die Instanziierung der Klasse zuständig. Dadurch wird die Lesbarkeit und Einfachheit des erstellen Codes erhöht. Ein weiterer Vorteil wäre, dass der Aufrufer der Builder-Methoden lediglich die konkreten Werte übergibt.

Der obige Aufruf würde unter der Verwendung des Fluent Builder Pattern exemplarisch so aussehen:

var response = new HttpResponse();
 
response.AddStatusCode(200)
        .AddContentType("application/json")
        .AddCotent(objectJsonStream)
        .AddHeader("HeaderKey1", "HeaderValue1")
        .AddHeader("HeaderKey2", "HeaderValue2")
        .Build();

Erweiterung Extension

Die bestehende Extension soll für das Response-Handling verwendet und erweitert werden. Hierzu wird die Implementierung in zwei Teile unterteilt:

  • Builder: in einem neuen Typ (HttpResponse) werden die benötigten Informationen gehalten. Dieser Typ wird durch die Verwendung von Builder-Methoden in einer separaten Builder-Klasse erzeugt.
  • Response Handling: die bestehende Extension zum Konvertieren von Requests wird um weitere Funktionalität erweitert. Hierzu gehören Methoden, welche das Builder-Objekt befüllen und hierüber das Zielobjekt HttpResponse erzeut. Diese Klasse soll auch das Senden von einem erfolgreichen wie auch fehlerhaften (Exception) Response ermöglichen.

Builder

Das Basis-Objekt für die Builder-Klasse stellt den Response dar. Im Microsoft.AspNetCore.Mvc-Namespace bietet C# (u.a. mit .Net Core 3.1) die Klasse ContentResult zur Verfügung. Diese erbt wiederum von der Klasse ActionResult und bietet bereits drei Properties, welche im HttpResponse verwendet werden sollen. Darüber hinaus kann hierdurch später einfach ein ActionResult aus der Azure Function zurückgegeben werden, was die Rückgabe generisch macht.

ContentResult stellt diese drei Eigenschaften bereit:

  • Content (string)
  • ContentType (string)
  • StatusCode (int)

Somit fehlt nur noch ein Dictionary aus Strings (IDictionary<string, string>) zur Repräsentation des Headers. Aus diesem Grund bietet es sich an, eine eigene Klasse zu erstellen, welche von ContentResult erbt und um die Header-Property erweitert wird.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace Functions.Extension.ResponseHandler
{
    public class HttpResponse : ContentResult
    {
        public HttpResponse(int? statusCode, string contentType, string content, IDictionary<string, string> headers)
        {
            this.StatusCode = statusCode;
            this.ContentType = contentType;
            this.Content = content;
            this.Headers = headers;
        }

        public IDictionary<string, string> Headers { get; } = new Dictionary<string, string>();

        public override Task ExecuteResultAsync(ActionContext actionContext)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException(nameof(actionContext));
            }

            foreach (var keyValuePair in this.Headers)
            {
                actionContext.HttpContext.Response.Headers.Add(keyValuePair.Key, keyValuePair.Value);
            }

            return base.ExecuteResultAsync(actionContext);
        }
    }
}

Die Builder-Klasse besteht aus den Properties, welche später zur Erzeugung des HttpResponse-Objekts verwendet werden sollen. Daneben existiert noch eine Methode zur Ergänzung von Header-Einträgen und der Build-Methode, welche ein neues HttpResponse-Objekt erzeugt.

using System.Collections.Generic;

namespace Functions.Extension.ResponseHandler
{
    public class HttpResponseBuilder
    {
        public int? StatusCode { get; set; }

        public string ContentType { get; set; }

        public string Content { get; set; }

        public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();

        public HttpResponse Build()
        {
            var contentResult = new HttpResponse(StatusCode, ContentType, Content, Headers);

            return contentResult;
        }

        public HttpResponseBuilder AddHeaderEntry(string headerKey, string headerValue)
        {
            if (!Headers.ContainsKey(headerKey))
            {
                Headers.Add(headerKey, headerValue);
            }
            else
            {
                Headers[headerKey] = headerValue;
            }

            return this;
        }
    }
}

Responsehandler

Die bestehende Klasse HttpHandler wird um weitere Methoden erweitert werden. Diese sollen zum einen Funktionen bereitstellen, um das Builder-Objekt zu bestücken und zum anderen standardisierte Methoden zur Erstellung von Responses bereitstellen.

Um dem Nutzer Aufwand bei der Entwicklung zu ersparen, werden typische Einstellungen wie der Statuscode 200 und der Contenttype „application/json“ als Standardwerte gesetzt. Somit muss der Aufrufer diese nicht jedes Mal erneut setzen, kann diese Werte aber zum Zeitpunkt des Aufrufes natürlich übergeben und dadurch die Werte entsprechend ändern.

using System;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Functions.Extension.ResponseHandler;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Functions.Extension.Handler
{
    public static partial class HttpHandler
    {
        private static readonly string DefaultContentTypeApplicationJson = "application/json";

        #region Response

        public static Task<IActionResult> Send<TResult>(this HttpResponseBuilder builder, TResult result)
        {
            var response = builder.Build();

            response.StatusCode = response.StatusCode ?? (int)HttpStatusCode.OK;
            response.ContentType = !string.IsNullOrEmpty(response.ContentType) ? response.ContentType : DefaultContentTypeApplicationJson;

            if (response.ContentType == DefaultContentTypeApplicationJson)
            {
                if (result != null)
                {
                    response.Content = JsonSerializer.Serialize(result, JsonOptions);
                }
            }
            else
            {
                response.Content = result.ToString();
            }

            return Task.FromResult((IActionResult)response);
        }

        public static Task<IActionResult> SendFromException(this HttpResponseBuilder builder, Exception ex)
        {
            var response = builder.Build();

            response.Content = JsonSerializer.Serialize(new { StatusCode = (int)HttpStatusCode.InternalServerError, Message = ex.Message }, JsonOptions);
            response.StatusCode = response.StatusCode ?? (int)HttpStatusCode.InternalServerError;
            response.ContentType = DefaultContentTypeApplicationJson;

            return Task.FromResult((IActionResult)response);
        }

        #endregion

        #region Builder

        public static HttpResponseBuilder CreateHttpResponse(this HttpRequest req)
        {
            var responseBuilder = new HttpResponseBuilder();

            return responseBuilder;
        }

        public static HttpResponseBuilder AddHeaderEntry(this HttpResponseBuilder builder, string headerKey, string headerValue)
        {
            builder.AddHeaderEntry(headerKey, headerValue);
            return builder;
        }

        public static HttpResponseBuilder SetStatusCode(this HttpResponseBuilder builder, HttpStatusCode httpStatusCode)
        {
            builder.StatusCode = (int)httpStatusCode;
            return builder;
        }

        public static HttpResponseBuilder SetContentType(this HttpResponseBuilder builder, string contentType)
        {
            builder.ContentType = contentType;
            return builder;
        }

        #endregion
    }
}

Da die eigene Klasse HttpResponse von ContentResult abgeleitet wurde, entspricht der zurückgegebene Datentyp einem ActionResult. Dies entspricht dem Standardmäßigen Rückgabetyp von Azure Functions und muss somit nicht erneut gecastet werden, wenn ein Response über die Extension erzeugt wird.

Beispielhafte Implementierung

Im bestehenden GitHub-Repo aus dem letzten Beitrag wurde die Implementierung um das Response-Handling erweitert. Darin sind auch die bestehenden API-Endpunkte entsprechend erweitert worden.

Ein Beispielhafter Aufruf ist hier dargestellt:

using System;
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.Extensions.Logging;

namespace Application.Functions.Todo
{
    public static class StoreTodoItemCommandFunction
    {
        [FunctionName("StoreTodoItemCommandFunction")]
        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;
        }
    }
}

Die Azure Functions können nun immer nach demselben Schema aufgebaut werden. Somit ist neben dem Request-Handling auch das Response- und Exception Handling abgebildet. Außerdem sieht man beim Response sofort, dass der entsprechende Statuscode gesetzt wurde, ohne dass dieser beim Aufruf explizit angegeben werden musste (siehe auch Screenshot von Postman).

Postman Test

Zusammenfassung

Die Nutzung der Extension hat nun einen Reifegrad erreicht, um bereits einfach und in einem größeren Umfeld Web APIs mit Azure Functions zu erstellen. Die Verwendung steigert die Effizienz der jeweiligen Implementierung ungemein und kann einfach verwendet werden.

Bevor APIs von außen verwendet werden können, benötigt der Aufrufer neben der URL des Endpunkts auch weitere Informationen (Query-Parameter, Http-Verb, usw.) für den Aufruf. Mit Swagger/ Open API besteht die Möglichkeit eine API-Definition für Aufrufer bereitzustellen. Unsere aktuelle Implementierung bietet diese Funktion aktuell noch nicht an. Die Verwendung von Swagger und der notwendigen Einstellungen werden im nächsten Blogbeitrag vorgestellt.

Der Objektkultur-Newsletter

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

Newsletter abonnieren