Fortuna Entwickler Blog

Hier wird Ihnen geholfen

Design Projekt .NET Core Web API

Um in der Zukunft einen einheitlichen Aufbau aller Projekte zu haben, wird hier ein Beispielaufbau einer Anwendung erklärt. Alle Angaben beziehen sich auf .NET Core Web APIs.

Aufbau Struktur

In jedem Projekt sollte es folgende Struktur geben:
  • Controllers => Beinhaltet jegliche Logik zu den Endpoints
  • Services => Beinhaltet jegliche Logik zur Verarbeitung der Daten (Anfragen an Datenbank, Manipulation von Daten, Anfrage an andere APIs usw.)
  • Mappings (wenn gebraucht) => Automapper Konfiguration Dateien
  • Resources (wenn gebraucht) => alle Projektspezifischen Ressourcen wie AppSettings Konfigurations Model 
  • Interfaces (wenn gebraucht) => Selbsterklärend
  • Extensions (wenn gebraucht) => Selbsterklärend
  • Models (wenn gebraucht)  => Selbsterklärend

Folgende Nuget-Pakete sollten in allen Anwendungen installiert sein:

Damit alle Pakete korrekt funktionieren müssen folgende Dinge in das Programm eingebaut werden.

Microsoft.AspNetCore.Mvc.NewtonsoftJson
Folgendes muss unter in der Startup.cs Datei in der Methode ConfigureServices reingeschrieben werden. Dies setzt die neue JSON Libary fest.


Swashbuckle.AspNetCore

Um Swagger nutzen zu können muss an zwei verschiedenen Stellen in der Startup.cs Datei Code eingefügt werden.

Zuerst in der ConfigureServices Methode nach der AddControllers Funktion muss Swagger hinzugefügt werden.

Danach muss Swagger noch aktiviert werden, dazu muss in der Configure Methode folgendes geschrieben werden.

Hier sind vor allem die Zeilen 35-39 wichtig. Der Aufbau der restlichen Zeilen kann aber übernommen werden. Hier gilt zu beachten, dass, dass der Name der API jeweils angepasst wird.

NLog NLog.Web.AspNetCore

http://webentw4/Blog/post/2020/05/12/nlog-in-in-asp-net-core-einbinden


ContentTypes

Die ContentTypes werden zentral in der Startup.cs eingestellt. Die meiste Zeit wird nur application/json benötigt. Sollten weitere benötigt werden, muss geschaut werden, ob diese zentral eingestellt werden oder nur für eine bestimmte Funktion. Zu beachten ist, wenn die ContentTypes überschrieben werden müssen an der Funktion dann alle angegeben werden.

Aufbau des Controllers

Ein Controller dient als Sammelstelle aller Endpoints. Hier ist nochmal zu erwähnen, jegliche Logik sollte in die Services geschrieben werden und nur der Zugriff auf die Services bzw. auf die Endpoints sollte im Controller beschrieben werden.

Im ersten Schritt sollte ein BaseController implementiert werden. Dieser beinhaltet alle allgemein gültigen Funktionen zur Verarbeitung von HTTP Requests bzw. Responses.


Der BaseController sollte zwingend die HandleError Methode besitzen, damit alle Fehler geloggt werden. Außerdem ist wichtig, dass der Controller mit dem Attribute ApiController ausgestattet wird, damit beim Kompilieren erkannt wird, dass alle zu erbenden Klassen bzw. Controller Teil der API sind.

Wird dann ein Blick auf die "richtigen" Controller geworfen, müssen ein paar Dinge erledigt werden. Zuerst muss die Route bestimmt werden. Diese wird mit api/[controller] angegeben. Daraus resultiert in dem Fall immer der Pfad api/vertrag/xxx. Dies hat den Vorteil, dass wir nicht in jeder Funktion gewisse Teile des Pfads doppelt schreiben müssen. 
Darüber hinaus muss der Logger und die benötigten Services per Dependency injection eingebunden werden.


Aufbau eines Endpoints

Alle Endpoints sollten folgenden Aufbau besitzen:

  • Http Request Methode mit Pfad => Funktionsname bzw. Abwandlung angeben und jeweilige Parameter mit Datentypen
  • Consume/Produces Types => Content-Types => In der Regel brauchen wir nur application/json (allgemein) => Diese werden vorher zentral in der Startup.cs eingestellt und nur genauer angegeben, wenn zusätzliche fehlen
  • ProducesResponseType => Rein zu Dokumentationszwecken gedacht, aber sinnvoll zum testen => Alle Rückgabetypen mit Statuscodes eintragen
  • Rückgabetyp => Sollte eigentlich immer IActionResult sein, da wir unterschiedliche Rückgabetypen haben wie unter ProducesResponseType zu sehen ist, außer bei asynchronen Aufrufen, dann sollte es Task<IActionResult> sein
  • Funktionsname => Titel der Funktion, wenn asynchron an den Namen immer ein Async anhängen
  • Try-Catch-HandleError => Für unerwartete Fehler wie z.B. in der Datenbank 

Einbinden der Newtonsoft.Json Libary in einer .NET Core Anwendung

Seit .NET Core 3.0 hat Microsoft eine eigene Libary (System.Text.Json) freigegeben, um Operationen im JSON-Format durchzuführen. Darunter fällt auch das serialisieren und deserialisieren von Requests & Respones an eine API.

Leider hat die Libary noch ein paar Schwierigkeiten (Stand 09.10.2020) komplexere Datentypen, wie Dictionaries zu verarbeiten. Teilweise treten System.NotSupportExceptions auf, da die Libary, die Datentypen nicht unterstützt.

Damit das Problem umgangen werden kann, gibt es weiterhin die alt bekannte Newtonsoft.Json-Libary, die genutzt werden kann und auch mit komplexeren Datentypen umgehen kann.

Folgende Schritte müssen durchgeführt werden, um die Libary einzubinden:

  1. Newtonsoft.Json als Nuget Paket hinzufügen
  2. Als nächstes abhängig davon was für ein Projekt vorliegt, muss in der Startup.cs-Datei folgende Zeile geändert/hinzugefügt werden:

Sobald der Befehl abgeändert worden ist, wird das Paket automatisch genutzt in der Vearbeitung aller JSON-Operationen.

RESTful Service erstellen und Konsumieren mit RestSharp

Ich habe mich im Rahmen der Restschuldversicherung für die von Essen Bank mal mit RESTful Services beschäftigt, da ich eine ganze Reihe an Daten an den Service übergeben muss und auch viele Daten zurückbekomme, liegt der Fokus erst einmal auf POST.

Das Ganze wollte ich erst einmal einfach halten, um bei etwaigen Fehlern nicht in Unmengen von Code zu suchen. Also habe ich erst einmal mit einem ganz kleinen Service begonnen.

In Visual Studio habe ich ein neues Projekt erstellt: Installed -> Visual C# -> WCF -> WCF Service Application. Im neuen Projekt bekommt man dann ein Interface und einen Service vorgegeben, die man nach eigenem Gusto benennen kann. In meinem Fall war dies hinsichtlich des eigentlichen Ziels Fortuna.RSV.Services

Im Interface (Fortuna.RSV.Services.IRSVCalc.cs) habe ich dann erst mal einen ganz einfachen Service angelegt:

        [OperationContract]
        [WebInvoke(Method = "POST",
            ResponseFormat = WebMessageFormat.Json,
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            UriTemplate = "Berechnen")]
        int Berechnen(int i);

Der zugehörige Code (Fortuna.RSV.Services.RSVCalc.svc.cs) im Service selbst ist dazu denkbar einfach:

        public int Berechnen(int i)
        {
            return i * 2;
        }

In der Web.config ist der Service zudem noch einzutragen:

  <system.serviceModel>
    <services>
      <service name="Fortuna.RSV.Services.RSVCalc" behaviorConfiguration="serviceBehavior">
        <endpoint address="" binding="webHttpBinding" contract="Fortuna.RSV.Services.IRSVCalc" behaviorConfiguration="web"></endpoint>
      </service>
    </services>
    <behaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp />
        </behavior>
      </endpointBehaviors>
      <serviceBehaviors>
        <behavior name="serviceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>

Damit ist der Service auch schon fertig und grundsätzlich einsatzbereit. Man kann ihn schon im Visual Studio testen.

Nun wollte ich natürlich auch mal sehen, wie man den Service konsumieren kann. Natürlich geht das, indem man einen Request an den Service schickt und die Response auswertet. Anstatt alles 'von Hand' zu machen, habe ich mich entschieden, RestSharp zu verwenden. Informationen dazu findet man auf RestSharp.org und man kann das Package über nuget in sein Projekt einbinden.

Um das nun zu testen habe ich ein neues Projekt (einfach eine C# ClassLibrary) hinzugefügt und über nuget RestSharp in den Referenzen ergänzt. Das Projekt habe ich Fortuna.RSV.ServiceClient genannt, aus class1.cs wurde RSVService.cs:

    public class RSVService
    {
        private string _ServiceBaseUri;
        public RSVService()
        {
        }
        public RSVService(string Uri)
        {
            _ServiceBaseUri = Uri + "RSVCalc.svc/";
        }
        public int Berechnen(int Eingabe)
        {
            int Ausgabe = 0;
            var client = new RestClient(_ServiceBaseUri);
            var request = new RestRequest("Berechnen", Method.POST);
            request.RequestFormat = DataFormat.Json;
            request.AddBody(Eingabe);
            var response = client.Execute(request);
            if (response.ResponseStatus == ResponseStatus.Completed)
                Ausgabe = int.Parse(response.Content);
            return Ausgabe;
        }
    }

Was passiert hier in der Methode Berechnen?

Es wird ein RestClient erstellt; dies ist ein Objekt, welches von RestSharp zur Verfügung gestellt wird. Als Parameter wird die Uri des Service angegeben. Selbige wird im Konstruktor meiner Klasse übergeben. Anschliessend wird der RestRequest definiert. Auch dies ist ein Objekt von RestSharp. Hier geben wir schon an, wie die Methode heisst, die aufgerufen wird, in unserem Fall 'Berechnen', was dem Wert entspricht, der oben als 'UriTemplate' im Contract steht. Als Methode habe ich auch hier (wie im Contract) POST definiert. Ebenso analog zum Contract verwende ich JSON als Datenformat.

Der Parameter (int i) wird dem RequestBody hinzugefügt; RestSharp kümmert sich um die Serialisierung in JSON, was in diesem Fall denkbar wenig ist, aber für die eigentliche Zielsetzung durchaus von Belang ist.

Anschliessend wird der Request mit Execute ausgeführt und das Ergebnis (die Response) in einer Variablen abgelegt. Das Ergebnis des Service erhält man bei dieser Art des Aufrufs in der RestResponse-Property 'Content'. Fertig.

Hier würde natürlich erst mal alles ganz einfach gehalten: Ein integer wird übergeben und ein integer wird zurückgeliefert. Im nächsten Schritt wollte ich dann ein einfaches Objekt mit zwei integer-Werten liefern und einen integer zurückbekommen. Im Service-Interface sieht das dann so aus:

        [OperationContract]
        [WebInvoke(Method = "POST",
            ResponseFormat = WebMessageFormat.Json,
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            UriTemplate = "Berechnen3")]
        int Berechnen3(BerechnenClass data);

Die Berechnen Class ist denkbar übersichtlich:

    [DataContract(Namespace="http://fortuna.mylife-leben.de/RSV")]
    public class BerechnenClass
    {
        [DataMember]
        public int parm1 { get; set; }
        [DataMember]
        public int parm2 { get; set; }
    }

Und auch den Code des Service habe ich einfach gehalten:

        public int Berechnen3(BerechnenClass data)
        {
            return data.parm1 * data.parm2;
        }

In der Web.config ist jetzt nichts weiter einzutragen; der Service ist dort ja bereits vorhanden; es kommt nur eine neue Methode dazu.

Im Client benötige ich natürlich auch das Objekt und der Code ist in grossen Teilen mit dem ersten identisch:

        public int Berechnen3(int parm1, int parm2)
        {
            BerechnenClass c = new BerechnenClass() { parm1 = parm1, parm2 = parm2 };
            int Ausgabe = 0;
            var client = new RestClient(_ServiceBaseUri);
            var request = new RestRequest("Berechnen3", Method.POST);
            request.RequestFormat = DataFormat.Json;
            request.AddBody(c);
            var response = client.Execute(request);
            if (response.ResponseStatus == ResponseStatus.Completed)
                Ausgabe = int.Parse(response.Content);
            return Ausgabe;
        }

Auch hier wieder: Client erstellen, Request erstellen, dann aber eben das zu übergebende Objekt dem RequestBody hinzufügen, die Serialisierung nach JSON wird von RestSharp übernommen, dann der Aufruf des Services und die Verarbeitung der Response. Fertig.

Nachdem auch dies funktioniert wollte ich nun das Objekt übergeben, welches zur Berechnung und als Ergebnis bei der Restschuldversicherung bereits vorhanden ist. Ein Klasse mit deutlich über 30 Parametern, sowohl integer als auch double Werte, also schon etwas größer als das, was ich hier bisher verwendet habe. Die Klasse heisst 'RSVdaten'

Das Interface ist ähnlich wie gehabt:

        [OperationContract]
        [WebInvoke(Method = "POST",
            ResponseFormat = WebMessageFormat.Json,
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            UriTemplate = "Berechnen2")]
        RSVdaten Berechnen2(RSVdaten data);

Der Code im Service ist sehr einfach, weil die eigentliche Berechnung in C++ abgebildet ist:

        public RSVdaten Berechnen2(RSVdaten data)
        {
            RSVInterfaceUnmanaged.ClassRSVBer myClass = new ClassRSVBer();
            myClass.ber(data);
            return data;
        }

Während dies alles nicht weiter aufregend ist, gab es jetzt für den Client eine gewisse Herausforderung. Bisher haben die Methoden int zurückgeliefert, hier jetzt aber ein komplexes Objekt. RestSharp bietet hier eine schöne Variante des 'Execute' an:

        public RSVdaten Berechnen2(RSVdaten data)
        {
            RSVdaten Ausgabe = new RSVdaten();

            var client = new RestClient(_ServiceBaseUri);
            var request = new RestRequest("Berechnen2", Method.POST);
            request.RequestFormat = DataFormat.Json;
            request.AddBody(data);

            var response = client.Execute<RSVdaten>(request);
            if (response.ResponseStatus == ResponseStatus.Completed)
                Ausgabe = response.Data;

            return Ausgabe;
        }

Beim Execute gibt man bereits an, von welchem Typ man die Response erwartet. Den Inhalt findet man dann nciht im Parameter Content, sondern in Data, welcher dann schon vom angegeben Typ ist. Mehr ist auch hier nicht zu tun.