== 1. Einleitung == **ReC.Client** ist eine .NET-Client-Bibliothek für den typisierten und bequemen Zugriff auf die **ReC.API**. Anstatt direkt mit `HttpClient` zu arbeiten, bietet die Bibliothek thematisch geordnete API-Klassen (z. B. `RecActionApi`, `ResultApi`, `ProfileApi`, `EndpointAuthApi`, `EndpointParamsApi`, `EndpointsApi`, `CommonApi`) und integriert sich nahtlos in **Microsoft.Extensions.DependencyInjection**. Die Bibliothek unterstützt sowohl **.NET 8** als auch **.NET Framework 4.6.2** (Multi-Targeting). === 1.1 Kernmerkmale === * **DI-orientiert**: Registrierung über `services.AddRecClient(...)`. * **Typisierte API-Klassen**: jede Domäne hat eine eigene API-Klasse als Eigenschaft auf `ReCClient`. * **Konsistente Fehlerbehandlung**: Bei HTTP-Fehlerstatus wird einheitlich eine `ReCApiException` geworfen, inklusive Statuscode, Methode, URI, Body usw. * **Flexibles Lesen**: GET-Endpunkte unterstützen sowohl typisierte (`GetAsync(...)`) als auch dynamische (`GetAsync(...)` ohne Typparameter, liefert `dynamic` / `JsonElement`) Abfragen. * **Optionen**: Logging und Verhalten lassen sich über `ReCClientOptions` steuern. == 2. Installation und Setup == === 2.1 Konfiguration mit Dependency Injection (empfohlen) === Registrieren Sie den Client in `Program.cs` / `Startup.cs` über `AddRecClient`. Sie können entweder eine Basis-URL als String oder einen Konfigurations-Delegate für den zugrunde liegenden `HttpClient` übergeben. {{code language="vb.net"}} Imports Microsoft.Extensions.Hosting Imports Microsoft.Extensions.DependencyInjection Imports ReC.Client Module Program Sub Main(args As String()) Dim builder = Host.CreateDefaultBuilder(args) builder.ConfigureServices( Sub(services) ' Variante A: Basis-URL als String services.AddRecClient("https://ihre-rec-api-adresse.com/") ' Variante B: HttpClient feinkonfigurieren ' services.AddRecClient(Sub(client) ' client.BaseAddress = New Uri("https://ihre-rec-api-adresse.com/") ' client.Timeout = TimeSpan.FromSeconds(30) ' End Sub) End Sub) Dim app = builder.Build() app.Run() End Sub End Module {{/code}} {{code language="csharp"}} using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; using ReC.Client; var builder = Host.CreateDefaultBuilder(args); builder.ConfigureServices(services => { // Variant A: base URL as string services.AddRecClient("https://ihre-rec-api-adresse.com/"); // Variant B: configure HttpClient explicitly // services.AddRecClient(client => // { // client.BaseAddress = new Uri("https://ihre-rec-api-adresse.com/"); // client.Timeout = TimeSpan.FromSeconds(30); // }); }); var app = builder.Build(); app.Run(); {{/code}} === 2.2 Konstruktor-Injektion === Sobald registriert, kann `ReCClient` per Konstruktor in jeden Dienst injiziert werden. {{code language="vb.net"}} Imports ReC.Client Public Class MeinDienst Private ReadOnly _recClient As ReCClient Public Sub New(recClient As ReCClient) _recClient = recClient End Sub End Class {{/code}} {{code language="csharp"}} using ReC.Client; public class MeinDienst { private readonly ReCClient _recClient; public MeinDienst(ReCClient recClient) { _recClient = recClient; } } {{/code}} === 2.3 Optionen über ReCClientOptions === Über `ReCClientOptions` lässt sich das Verhalten des Clients steuern, z. B. ob erfolgreiche Anfragen geloggt werden sollen. {{code language="vb.net"}} services.AddRecClient("https://ihre-rec-api-adresse.com/") services.Configure(Of ReCClientOptions)( Sub(opt) opt.LogSuccessfulRequests = True End Sub) {{/code}} {{code language="csharp"}} services.AddRecClient("https://ihre-rec-api-adresse.com/"); services.Configure(opt => { opt.LogSuccessfulRequests = true; }); {{/code}} == 3. Überblick über die API-Klassen == `ReCClient` bündelt mehrere thematische API-Klassen als Eigenschaften: * `RecActions` (`RecActionApi`) – Verwaltung und Auslösen von RecActions (CRUD + Invoke) * `Results` (`ResultApi`) – Lesen, Anlegen, Aktualisieren und Löschen von Result-Datensätzen * `Profiles` (`ProfileApi`) – Verwaltung der Profile * `EndpointAuth` (`EndpointAuthApi`) – Verwaltung der Endpoint-Authentifizierungsdaten * `EndpointParams` (`EndpointParamsApi`) – Verwaltung der Endpoint-Parameter * `Endpoints` (`EndpointsApi`) – Verwaltung der Endpoints * `Common` (`CommonApi`) – Gemeinsame Operationen, die nicht entitätsspezifisch sind Alle entitätsspezifischen Klassen erben von `BaseCrudApi` und bieten ein konsistentes CRUD-Schema. == 4. Verwendung == === 4.1 GET-Endpunkte: typisiert oder dynamisch === Die GET-Methoden in `RecActionApi`, `ProfileApi` und `ResultApi` existieren jeweils als **zwei Overloads**: * **Generisch**: `GetAsync(...)` – führt die Anfrage aus, liest den Response-Body **einmal** und deserialisiert ihn in den Typ `T`. * **Nicht-generisch**: `GetAsync(...)` – identische Parameterliste, gibt aber ein `dynamic` (in der Praxis `System.Text.Json.JsonElement`) zurück. Intern wird `GetAsync(...)` aufgerufen. Beide Overloads teilen sich Implementierung und Fehlerbehandlung: bei HTTP-Fehlerstatus wird **`ReCApiException`** geworfen. {{info}} Da der nicht-generische Overload eine andere Signatur als der generische besitzt (kein Typparameter), gibt es **keinen Konflikt**. Welcher Overload aufgerufen wird, hängt davon ab, ob Sie einen Typparameter angeben oder nicht. {{/info}} ==== 4.1.1 Typisiertes Lesen ==== {{code language="vb.net"}} Imports ReC.Application.Common.Dto ' Alle Actions für ein Profil als typisiertes Array Dim actions As RecActionViewDto() = Await recClient.RecActions.GetAsync(Of RecActionViewDto())(profileId:=42) For Each a In actions Console.WriteLine($"Action {a.Id} -> Endpoint {a.EndpointUri}") Next {{/code}} {{code language="csharp"}} using ReC.Application.Common.Dto; // All actions for a profile as a typed array var actions = await recClient.RecActions.GetAsync(profileId: 42); foreach (var a in actions!) { Console.WriteLine($"Action {a.Id} -> Endpoint {a.EndpointUri}"); } {{/code}} ==== 4.1.2 Dynamisches Lesen ==== Wenn das Schema flexibel ist oder Sie das Ergebnis nur weiterleiten möchten, können Sie den nicht-generischen Overload verwenden: {{code language="vb.net"}} Imports System.Text.Json Dim payload As Object = Await recClient.RecActions.GetAsync(profileId:=42) Dim element As JsonElement = CType(payload, JsonElement) If element.ValueKind = JsonValueKind.Array Then For Each item In element.EnumerateArray() Console.WriteLine(item.GetProperty("id").GetInt64()) Next End If {{/code}} {{code language="csharp"}} using System.Text.Json; dynamic? payload = await recClient.RecActions.GetAsync(profileId: 42); var element = (JsonElement)payload!; if (element.ValueKind == JsonValueKind.Array) { foreach (var item in element.EnumerateArray()) { Console.WriteLine(item.GetProperty("id").GetInt64()); } } {{/code}} === 4.2 Eine RecAction auslösen (Invoke) === `RecActionApi.InvokeAsync` startet die Stapelverarbeitung der Actions eines Profils. Es gibt zwei Overloads: einen mit `InvokeReferences`-Objekt und einen Komfort-Overload mit nur einer Batch-ID. {{code language="vb.net"}} Imports ReC.Client.Api Public Async Function FuehreProfilAktionenAus(recClient As ReCClient, profilId As Long) As Task Try Await recClient.RecActions.InvokeAsync( profilId, New InvokeReferences With {.BatchId = "batch-" & Guid.NewGuid().ToString("N")}) Catch ex As ReC.Client.ReCApiException ' Auswertung von ex.StatusCode, ex.Method, ex.RequestUri, ex.ResponseBody Throw End Try End Function {{/code}} {{code language="csharp"}} using ReC.Client; using ReC.Client.Api; public async Task ExecuteProfileActionsAsync(ReCClient recClient, long profileId) { try { await recClient.RecActions.InvokeAsync( profileId, new InvokeReferences { BatchId = $"batch-{Guid.NewGuid():N}" }); } catch (ReCApiException ex) { // Inspect ex.StatusCode, ex.Method, ex.RequestUri, ex.ResponseBody throw; } } {{/code}} Komfort-Overload nur mit Batch-ID: {{code language="vb.net"}} Await recClient.RecActions.InvokeAsync(profilId, "batch-001") {{/code}} {{code language="csharp"}} await recClient.RecActions.InvokeAsync(profileId, "batch-001"); {{/code}} === 4.3 Anlegen, Aktualisieren, Löschen (CRUD) === Alle CRUD-Operationen sind asynchron und werfen bei Fehlern eine `ReCApiException`. * `CreateAsync(payload)` – HTTP POST mit JSON-Body. * `UpdateAsync(id, payload)` – HTTP PUT auf `/{ResourcePath}/{id}` mit JSON-Body. * `DeleteAsync(payload)` – HTTP DELETE; das Payload wird in den **Query-String** serialisiert (die API bindet Delete-Parameter aus der URL). {{code language="vb.net"}} Imports ReC.Application.RecActions.Commands Imports ReC.Application.Common.Procedures.UpdateProcedure.Dto ' POST Await recClient.RecActions.CreateAsync(New InsertActionCommand With { .ProfileId = 1, .EndpointId = 1, .Active = True, .Sequence = 1 }) ' PUT Await recClient.RecActions.UpdateAsync(123, New UpdateActionDto With { .Active = False, .Sequence = 2 }) ' DELETE (Payload wird zu Query-String) Await recClient.RecActions.DeleteAsync(New DeleteActionCommand With { .Start = 100, .End = 110, .Force = False }) {{/code}} {{code language="csharp"}} using ReC.Application.RecActions.Commands; using ReC.Application.Common.Procedures.UpdateProcedure.Dto; // POST await recClient.RecActions.CreateAsync(new InsertActionCommand { ProfileId = 1, EndpointId = 1, Active = true, Sequence = 1 }); // PUT await recClient.RecActions.UpdateAsync(123, new UpdateActionDto { Active = false, Sequence = 2 }); // DELETE (payload becomes query string) await recClient.RecActions.DeleteAsync(new DeleteActionCommand { Start = 100, End = 110, Force = false }); {{/code}} === 4.4 Fehlerbehandlung mit ReCApiException === Sobald die API einen Statuscode außerhalb von 2xx zurückgibt, wirft die Bibliothek eine `ReCApiException`. Diese enthält folgende Informationen: * `StatusCode` – `HttpStatusCode` der Antwort (z. B. 404, 400, 500) * `ReasonPhrase` – Optionaler HTTP-Reason-Phrase * `ResponseBody` – Roher Response-Body als String (sofern lesbar) * `Method` – HTTP-Methode der ursprünglichen Anfrage (z. B. `GET`, `POST`) * `RequestUri` – Aufgerufene URI mit Pfad und Query {{code language="vb.net"}} Try Dim profile = Await recClient.Profiles.GetAsync(Of ProfileViewDto)(id:=42) Catch ex As ReCApiException If ex.StatusCode = Net.HttpStatusCode.NotFound Then ' Profil existiert nicht Else ' Allgemeiner Fehler Console.WriteLine($"{ex.Method} {ex.RequestUri} -> {ex.StatusCode}: {ex.ResponseBody}") Throw End If End Try {{/code}} {{code language="csharp"}} try { var profile = await recClient.Profiles.GetAsync(id: 42); } catch (ReCApiException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) { // Profile does not exist } catch (ReCApiException ex) { Console.WriteLine($"{ex.Method} {ex.RequestUri} -> {ex.StatusCode}: {ex.ResponseBody}"); throw; } {{/code}} == 5. Testen == Das Testprojekt verwendet `Microsoft.AspNetCore.Mvc.Testing`, um die `ReC.API` mit `WebApplicationFactory` **in-process** zu starten. Der `ReCClient` wird über DI konfiguriert und auf den in-process HTTP-Handler verdrahtet. So müssen Tests die API nicht extern starten. Empfehlungen: * Schreiben Sie Tests als `async Task` und verwenden Sie `await` – **vermeiden** Sie `GetAwaiter().GetResult()` oder `TaskSyncExtensions.Sync(...)`. * Verwenden Sie `Assert.ThrowsAsync(...)`, um Fehlerpfade zu prüfen, und werten Sie `StatusCode`, `Method` und `RequestUri` aus. * Für GET-Tests reicht eine einzelne Methode pro Verhalten (typisiert vs. dynamisch) statt redundanter Setups. == 6. Veraltete / statische APIs == Einige historische Hilfsmittel sind weiterhin vorhanden und funktionsfähig, jedoch mit `[Obsolete]` markiert. Sie sind kein Breaking Change – Sie können sie übergangsweise weiter benutzen, sollten aber langfristig migrieren. Betroffen: * `ReCClient.BuildStaticClient(string)` und `ReCClient.BuildStaticClient(Action)` * `ReCClient.Create()` * `TaskSyncExtensions.Sync(...)` und `TaskSyncExtensions.Sync(...)` Warum als `Obsolete` markiert? * **Statischer Provider**: Ein global gehaltener `IServiceProvider` erschwert die Verwaltung von `HttpClient`-Lebenszyklen, kann zu schwer reproduzierbaren Problemen in lang laufenden Prozessen führen und behindert Tests. * **Synchrone Blockade**: `Task.GetAwaiter().GetResult()` (was `TaskSyncExtensions.Sync` intern tut) kann in Umgebungen mit `SynchronizationContext` (z. B. WinForms, WPF, einige Test-Runner) zu Deadlocks führen und macht Fehler schwerer diagnostizierbar. Funktionieren sie noch? * Ja. Die Methoden bleiben kompilierbar und ausführbar. Sie erhalten lediglich eine Compiler-Warnung bei der Verwendung. Wann darf man sie übergangsweise nutzen? * In Legacy-Code, der nicht sofort auf DI + `async/await` umgestellt werden kann. * In sehr kurzen, isolierten Skripten oder Konsolenanwendungen ohne `SynchronizationContext`, wo das Risiko überschaubar ist. Beispiel – statischer Client (nicht empfohlen, aber möglich): {{code language="vb.net"}} ' Einmalig beim Anwendungsstart ReCClient.BuildStaticClient("https://ihre-rec-api-adresse.com/") ' Später irgendwo im Code Dim client As ReCClient = ReCClient.Create() Await client.RecActions.InvokeAsync(profilId, "batch-001") {{/code}} {{code language="csharp"}} // Once at application startup ReCClient.BuildStaticClient("https://ihre-rec-api-adresse.com/"); // Later somewhere in code var client = ReCClient.Create(); await client.RecActions.InvokeAsync(profileId, "batch-001"); {{/code}} Beispiel – synchrone Blockade (nicht empfohlen, aber möglich): {{code language="vb.net"}} Imports ReC.Client ' Achtung: kann zu Deadlocks führen recClient.RecActions.InvokeAsync(profilId, "batch-001").Sync() {{/code}} {{code language="csharp"}} using ReC.Client; // Warning: may deadlock depending on context recClient.RecActions.InvokeAsync(profileId, "batch-001").Sync(); {{/code}} Migrations-Tipps: * **`BuildStaticClient` / `Create`** ? ersetzen durch `services.AddRecClient(...)` und Konstruktor-Injektion. * **`TaskSyncExtensions.Sync`** ? den umliegenden Codepfad asynchron machen (`async Task`) und `await` verwenden. == 7. Migrations-Hinweise (jüngste Änderungen) == Folgende Änderungen sind zu beachten, falls Sie von einer früheren Version migrieren: * **GET-Methoden geben jetzt deserialisierte Werte zurück, nicht mehr `HttpResponseMessage`.** ** `GetAsync(...)` liest den Body **einmal** und gibt `T?` zurück. ** `GetAsync(...)` (ohne Typparameter) gibt `dynamic`/`JsonElement` zurück. ** Falls Sie zuvor `HttpResponseMessage` selbst behandelt haben (Status, Body lesen, deserialisieren), entfällt dieser Schritt. * **Einheitliche Fehlerbehandlung über `ReCApiException`.** ** Bei HTTP-Fehlern wird konsistent diese Exception geworfen – auch für GET. ** Sie müssen nicht mehr selbst auf `IsSuccessStatusCode` prüfen. * **`GetDynamicAsync` wurde umbenannt zu `GetAsync` (Overload).** ** Es gibt nun pro API-Klasse zwei `GetAsync`-Overloads: typisiert und dynamisch. Aufrufe von `GetDynamicAsync(...)` müssen zu `GetAsync(...)` geändert werden. * **`TaskSyncExtensions` und statische `ReCClient`-Helfer sind `[Obsolete]`.** == 8. FAQ == **Warum gibt `GetAsync` einen deserialisierten Wert statt `HttpResponseMessage` zurück?** Damit wird der Response-Body genau einmal gelesen, Fehler werden einheitlich über `ReCApiException` behandelt, und Aufrufer müssen weder selbst auf den Statuscode prüfen noch die Antwort manuell deserialisieren. **Wozu der nicht-generische `GetAsync(...)`-Overload?** Er ist ein Komfort-Aufruf für Fälle, in denen Sie das Schema nicht (oder noch nicht) typisieren möchten. Intern ruft er `GetAsync` auf und liefert ein `JsonElement` zurück, das Sie ad hoc inspizieren können. **Gibt es einen Konflikt zwischen dem generischen und dem nicht-generischen `GetAsync`?** Nein. Beide Methoden haben unterschiedliche Signaturen (ein Methodengeneric-Parameter ist Teil der Signatur). Der Compiler wählt anhand der Aufrufsyntax (`GetAsync(...)` vs. `GetAsync(...)`). **Soll ich die statischen `ReCClient.Create`-Helfer noch verwenden?** Nur in Legacy-Szenarien. Für neuen Code: DI mit `services.AddRecClient(...)`. **Sind synchrone Aufrufe via `Sync()` sicher?** Nicht generell. In Umgebungen mit `SynchronizationContext` riskieren sie Deadlocks. Verwenden Sie `async/await`.