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 

Administration von Benutzern für myInfoPoint

Zu den täglich anfallenden Aufgaben gehört es, Einstellungen zu myInfoPoint Benutzern zu ändern bzw. anzulegen. Dies geschieht derzeit noch direkt auf der Datenbank und wird damit von IT erledigt. Der Auftrag hierzu kommt üblicherweise von Service, also Versicherungsbetrieb.

Um Wartezeiten für Versicherungsbetrieb zu minimieren und gleichzeitig die Arbeiten aus IT auszulagern ist für die anfallenden Aufgaben eine Weboberfläche zu erstellen, um die Aufgaben durch Versicherungsbetrieb ausführen zu lassen.


1. Einstellen / Ändern der Sicht eines Benutzers in myInfoPoint

1.1 Abfrage der Aktuellen Sicht
Für jeden Benutzer kann eine individuelle Sicht in myInfoPoint eingestellt werden. Dies kann auf mehreren Ebenen geschehen und wird in dieser Reihenfolge geprüft. Die letzte Zuordnung ist dann die gültige:

  1. Sicht aufgrund des zugrunde liegenden Vertriebsweges (der Kunde hat über einen Vermittler einen Versicherungsvertrag abgeschlossen und der Vermittler ist einem Vertriebsweg zugeordnet. Die Vertriebswege sind alle mit Sichten in myInfoPoint hinterlegt)
  2. Sicht aufgrund des Wunsches des Vermittlers für alle seine Kunden (zum Vermittler wurde auf dessen Veranlassung eine Sicht hinterlegt, die sich vom Vertriebsweg unterscheiden kann)
  3. Sicht aufgrund des Wunsches des Vermittlers für einen bestimmten Kunden (zum Kunden wurde auf Veranlassung des zugeordneten Vermittlers eine Sicht hinterlegt, die sich von den Sichten der anderen Kunden des Vermittlers unterscheiden kann)

Die aktuelle Einstellung zur Sicht erhält man z.B. über folgende Abfrage auf WEBDATA:

SELECT v.V_NR, r.P_NR, ov.VKVO, v.VERTRIEBSWEG_KZ, 
NVL(msz1.SICHT_KZ, 0) AS SICHT_VERTRIEBSWEG, NVL(msz2.SICHT_KZ, 0) AS SICHT_VKVO, NVL(msz3.SICHT_KZ, 0) AS SICHT_P_NR
FROM V@BSTDB v
INNER JOIN OBJ_VM@BSTDB ov ON v.V_NR = ov.OBJ_NR AND ov.OBJ_KZ = 1 AND ov.HIST_KZ = 2
INNER JOIN ROLLE@BSTDB r ON v.V_NR = r.V_NR AND r.R_KZ = 1
LEFT OUTER JOIN MLS_SICHT_ZUORDNUNG msz1 ON v.VERTRIEBSWEG_KZ = msz1.VERTRIEBSWEG_KZ
LEFT OUTER JOIN MLS_SICHT_ZUORDNUNG msz2 ON ov.VKVO = msz2.VKVO
LEFT OUTER JOIN MLS_SICHT_ZUORDNUNG msz3 ON r.P_NR = msz3.P_NR
WHERE v.HIST_KZ = 2
ORDER BY v.V_NR DESC;
Das Ergebnis der Abfrage muss auf einen Kunden (P_NR) oder einen Vermittler (VKVO) gefiltert werden um den jeweiligen Wert korrekt abzufragen.
Bei Abfrage auf einen Kunden (P_NR) gilt:
Die Abfrage kann keinen, einen oder mehrer Datensätze liefern. Wird kein Datensatz geliefert, hat der Kunde keinen Vertrag und damit Sicht 0, also keine Sicht. Dies ist allerdings nur ein theoretische Konstellation; die Abfrage auf einen 'Kunden' der keinen Vertrag hat, erscheint unsinnig; ein Kunde definiert sich dadurch, daß er einen Vertrag hat.
Wird genau ein Datensatz von der Abfrage geliefert, so liefert SICHT_P_NR die für den Kunden eingestellte Sicht. Ist der Wert 0, gilt der Wert aus SICHT_VKVO. Ist dieser Wert auch 0, liefert SICHT_VERTRIEBSWEG die gültige Sicht für den Kunden.
Werden mehrere Datensätze geliefert, so gilt der erste Datensatz (aktuellster Vertrag durch absteigende Sortierung nach V_NR) und damit wird so verfahren wie zuvor beim Ergebnis mit nur einem Datensatz beschrieben.
Bei der Abfrage auf einen Vermittler (VKVO) gilt:
Die Abfrage kann keinen, einen oder mehrer Datensätze liefern. Wird kein Datensatz geliefert, hat der Vermittler keinen Kunden mit einem gültigen Vertrag und damit alle seine Kunden Sicht 0, also keine Sicht. Auch dies ist nur eine theoretische Konstellation.
Wird genau ein Datensatz zurückgeliefert gilt der Wert aus SICHT_VKVO. Ist dieser Wert 0, liefert SICHT_VERTRIEBSWEG die gültige Sicht für den Kunden.
Bei mehreren Datensätzen wird wiederum der erste Datensatz betrachtet und wie zuvor bei genau einem Datensatz verfahren. Das Feld SICHT_P_NR hat bei der Abfrage auf einen Vermittler keine Relevanz.

1.2 Setzen der Sicht
Gesetzt wird die Sicht in der Tabelle MLS_SICHT_ZUORDNUNG

für einen Kunden:
INSERT INTO MLS_SICHT_ZUORDNUNG (ID, SICHT_KZ, P_NR, KOMMENTAR) VALUES (DEFAULT, [SICHT], [Kundennummer], '[Kommentar]');

für einen Vermittler:
INSERT INTO MLS_SICHT_ZUORDNUNG (ID, SICHT_KZ, VKVO, KOMMENTAR) VALUES (DEFAULT, [SICHT], [VKVO], '[Kommentar]');

Zu beachten ist, daß die Felder alle angegeben werden müssen. Also Kommentar kann der Benutzer einen kurzen Freitext angeben; Idealerweise wird der aktuelle Zeitstempel angefügt. Das Kommentarfeld ist nur beschreibend und wird derzeit nicht ausgewertet. Dennoch sollte es immer gefüllt sein.
Das Schreiben der Daten mit INSERT geschieht nur, wenn noch keine Daten hinterlegt waren. gibt es zu einem Kunden oder zu einem Vermittler bereits eine Zugeordnete Sicht, so wird diese nach Änderung durch den Benutzer mittels UPDATE aktualisiert. Es darf immer nur maximal einen Datensatz pro Vermittler und genau ein Datensatz pro Kunde in der Tabelle MLS_SICHT_ZUORDNUNG vorhanden sein.

1.3 Umsetzung
In einer Maske gibt der Benutzer entweder eine Kundennummer oder eine VKVO an. Wird beides angegeben, wird nur nach der Kundennummer gesucht. Wurden Daten gefunden (siehe 1.1) Werden die Informationen aus VKVO und Sicht in die Maske übernommen, der Benutzer kann die Sicht anpassen und dann seine Einstellung speichern, wodurch die Informationen in der Datenbank aktualisiert werden. Der Benutzer erhält eine entsprechende Meldung, die Daten aus den Eingabefeldern werden gelöscht und es kann bei Bedarf gleich eine neue Abfrage ausgeführt werden. Entsprechend sollten Suche, Dateneingabe und Ergebnis auf genau einer Seite durchgeführt werden; für Suche und Dateneingabe sollten die gleichen Felder verwendet werden. Die Sicht kann der Benutzer nicht frei eingeben, sondern wählen aus
1 - Eingeschränkte Sicht
2 - Volle Sicht
3 - Volle Sicht mit Aktionen

Sollte die Abfrage (siehe 1.1) den Wert 0 liefern, wird als Voreinstellung 1 (Eingeschränkte Sicht) gesetzt; ansonsten wird der ermittelte Wert als Voreinstellung übernommen.

2. Ändern eines Benutzernamens in myInfoPoint

Benutzer können über myInfoPoint eine Änderung Ihres Benutzernamens (= ihrer Emailadresse) beantragen. Dies bedeutet derzeit, daß die Emailadresse in VWS geändert durch Versicherungsbetrieb geändert wird und für myInfoPoint selbst durch IT geändert wird.

2.1 Prüfen des Benutzernamens

Um zu prüfen, ob der Benutzer in myInfoPoint existiert, nutzt man auf WEBSERVICES eine der folgenden Abfragen:

SELECT * FROM ASPNET_USERS au WHERE au.USERNAME = '[alter Username]';
SELECT * FROM ASPNET_USER_EXTENSIONS aue INNER JOIN ASPNET_USERS au ON aue.USERID = au.USERID WHERE aue.PNR = [Kundennummer];

[alter Username] bzw. [Kundennummer] sind durch die entsprechende Information zu ersetzen.

Die Abfrage sollte genau einen Datensatz zurückliefern. Wird kein Datensatz gefunden, so ist der Benutzer unter diesem Namen / mit dieser Kundennummer nicht in myInfoPoint registriert.

2.2 Ändern des Benutzernamens / der Emailadresse

Die Änderung für myInfoPoint betrifft sowohl den Benutzernamen als auch die Emailadresse. Obwohl beide Informationen identisch sind, werden sie getrennt verwaltet und sind entsprechend auch einzeln in WEBSERVICES zu ändern:

UPDATE ASPNET_USER_EXTENSIONS aue SET aue.MAIL = '[neuer Username]' WHERE aue.USERID = (SELECT au.USERID FROM ASPNET_USERS au WHERE au.USERNAME = '[alter Username]');
UPDATE ASPNET_USERS aue SET aue.USERNAME = '[neuer Username]' WHERE aue.USERID = (SELECT au.USERID FROM ASPNET_USERS au WHERE au.USERNAME = '[alter Username]');
[alter Username] bzw. [neuer Username] sind durch die entsprechende Information zu ersetzen.

2.3 Umsetzung
In einer Maske gibt der Benutzer wahlweise einen Benutzernamen oder eine Kundennummer ein und erhält den aktuell hinterlegten Benutzernamen in myInfoPoint. 
Wurde kein Eintrag gefunden, erhält der Benutzer eine entsprechende Meldung und kann dann seine Angaben ggf. Ändern und eine neue Abfrage durchführen.
Wurde ein Eintrag gefunden, hat der Benutzer die Möglichkeit, einen neuen Benutzernamen einzugeben und die Änderung durchführen zu lassen. der Wert wird dann sowohl als Benutzername als auch als Emailadresse übernommen. Die beiden Felder sind identisch zu führen; es werden entsprechend keine getrennten Eingabefelder hierfür angeboten.

3. Technische Umsetzung

Für die Umsetzung sind im folgenden technischen Aspekte zu beachten.

3.1 Zugriff auf die Datenbank
Der Zugriff erfolgt sowohl lesend als auch schreibend ausschließlich über EntityFramework. Für die Abfrage (siehe 1.1) ist der Zugriff auf Daten der Bestandsdatenbank (GUTINGIA, @BSTDB) notwendig, welche im Webkontext nicht erreichbar ist. Entsprechend ist hierfür eine View auf WEBDATA zu erstellen. Analog zu bisherigen Datenbankobjekte für myInfoPoint und zur schnelleren Einordnung beginnt der Name der View mit 'MLS_'.

3.2 Umsetzung als Services
Die Aktionen (Lesen der Daten, Schreiben von Daten) sind als REST-Services zu implementieren welche die Entities (3.1) verwenden.

3.3 Umsetzung der Webseite
Punkt 1 und Punkt 2 der Anforderungen sind in getrennten Seiten und jeweils als eine einzelne Seite umzusetzen. Die Seiten nutzen die REST-Services (3.2) um Daten abzufragen und zu schreiben.

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.