== 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}} {{warning}} Wenn `LogSuccessfulRequests = true` gesetzt, aber **kein** `ILogger` über DI registriert ist, wirft der `ReCClient`-Konstruktor eine `InvalidOperationException`. Stellen Sie sicher, dass ein Logging-Provider (z. B. `services.AddLogging(...)`) registriert ist, oder lassen Sie die Option auf `false`. {{/warning}} == 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. Komfort-APIs: statischer Provider und synchrone Wrapper == Neben dem empfohlenen DI-basierten Ansatz bietet **ReC.Client** absichtlich auch einen **statischen Komfort-Pfad** sowie **synchrone Wrapper** an. Diese Bestandteile sind nicht „veraltet im Sinne von eingefroren" – sie werden weiterhin gepflegt und bei Bedarf um neue Funktionen erweitert. Sie sind jedoch bewusst mit `[Obsolete]` markiert, damit Aufrufer sie nicht „aus Versehen" auswählen, sondern eine bewusste Entscheidung treffen. Hintergrund * In Projekten, die noch auf **.NET Framework 4.6.2** basieren, ist `Microsoft.Extensions.DependencyInjection` häufig nicht etabliert und die Einführung einer DI-Infrastruktur kostet Zeit. Damit Entwickler dort nicht ins Stocken geraten, gibt es einen statischen Einstieg, der **sofort einsatzbereit** ist. * Synchroner Code in älteren Codebasen kann nicht überall sofort auf `async/await` umgestellt werden. Für solche Stellen existieren die `Sync()`-Erweiterungen als pragmatische Brücke. Status * **Aktiv gepflegt**: Beide Pfade erhalten weiterhin Funktionalitäts- und Komfort-Updates. * **`[Obsolete]`-Markierung als Erinnerung**: Die Compiler-Warnung soll bewusst sichtbar bleiben, damit Teams die Übergangslösung nicht vergessen und mittelfristig zu DI + `async/await` migrieren. * **Kein Breaking-Change-Risiko**: Aufrufe bleiben kompilierbar und ausführbar. Wann der statische Pfad sinnvoll ist * Bestehender VB.NET-/C#-Code auf .NET Framework 4.6.2 ohne eigene DI-Infrastruktur. * Kleine Werkzeuge, Skripte oder Konsolenanwendungen, bei denen ein vollständiges Host-Setup übertrieben wäre. * Schneller Einstieg in die Bibliothek, um ein erstes Ergebnis zu sehen, bevor die endgültige Architektur entschieden wird. Wann besser DI verwenden * Lang laufende Prozesse, Serveranwendungen, Tests. * Sobald in der Anwendung ohnehin `IServiceCollection` / `IHostBuilder` vorhanden ist. * Wenn `HttpClient`-Lebenszyklen, Logging-Scopes oder Optionen sauber verwaltet werden sollen. === 6.1 Statischer Client mit BuildStaticClient / Create === `ReCClient.BuildStaticClient(...)` baut intern eine `IServiceCollection` auf, ruft `AddRecClient(...)` auf und legt einen **statischen, thread-safen `Lazy`** an. Der eigentliche `IServiceProvider` wird **erst beim ersten Aufruf von `ReCClient.Create()`** und genau einmal gebaut. Wichtig: * `BuildStaticClient` darf **nur einmal** beim Anwendungsstart aufgerufen werden. Ein zweiter Aufruf – egal welcher Overload – wirft `InvalidOperationException("Static Provider is already built.")`. * Die Konstruktion des `IServiceProvider` ist **threadsicher** (`Lazy` mit `LazyThreadSafetyMode.ExecutionAndPublication`). ==== 6.1.1 Empfohlene Form: BuildStaticClient mit StaticBuildConfiguration ==== Da der statische Pfad mehrere optionale Bestandteile hat (Basis-URL **oder** `HttpClient`-Konfiguration, `ReCClientOptions`, eigener `ILogger`, zusätzliche Service-Registrierungen), gibt es einen Callback-basierten Overload, der alle Optionen in einem `StaticBuildConfiguration`-Objekt bündelt: {{code language="vb.net"}} Imports Microsoft.Extensions.DependencyInjection Imports Microsoft.Extensions.Logging ReCClient.BuildStaticClient( Sub(cfg) cfg.BaseAddress = "https://ihre-rec-api-adresse.com/" cfg.ConfigureOptions = Sub(opt) opt.LogSuccessfulRequests = True End Sub ' Optional: eigene zusätzliche Registrierungen, z. B. Logging-Provider cfg.ConfigureServices = Sub(services) services.AddLogging(Sub(b) b.AddConsole()) End Sub End Sub) {{/code}} {{code language="csharp"}} using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; ReCClient.BuildStaticClient(cfg => { cfg.BaseAddress = "https://ihre-rec-api-adresse.com/"; cfg.ConfigureOptions = opt => { opt.LogSuccessfulRequests = true; }; // Optional: additional service registrations, e.g. logging cfg.ConfigureServices = services => { services.AddLogging(b => b.AddConsole()); }; }); {{/code}} Eigenschaften von `StaticBuildConfiguration`: * `BaseAddress` – Basis-URI der ReC.API. **Schließt sich gegenseitig** mit `ConfigureClient` aus. * `ConfigureClient` – Delegate zum direkten Konfigurieren des `HttpClient` (z. B. `BaseAddress` + `Timeout` + Header). Schließt sich gegenseitig mit `BaseAddress` aus. * `ConfigureOptions` – Optionaler Delegate für `ReCClientOptions` (z. B. `LogSuccessfulRequests`). * `Logger` – Optionale `ILogger`-Instanz, die als Singleton in die interne `IServiceCollection` registriert wird. * `ConfigureServices` – Optionaler Delegate, mit dem der Aufrufer beliebige zusätzliche Registrierungen auf der internen `IServiceCollection` vornehmen kann (z. B. `AddLogging(...)` oder eigene Abhängigkeiten). Validierung beim Aufruf: * `BuildStaticClient` wirft `ArgumentNullException`, wenn der `configure`-Callback `null` ist. * `BuildStaticClient` wirft `InvalidOperationException`, wenn weder `BaseAddress` noch `ConfigureClient` gesetzt sind, **oder** wenn beide gleichzeitig gesetzt sind. {{info}} Auch diese callback-basierte Variante ist mit `[Obsolete]` markiert — der Hinweistext lautet hier jedoch *"Use a local service collection instead of the static provider."* Damit wird klargestellt, dass innerhalb des statischen Pfades die `StaticBuildConfiguration`-Variante die empfohlene Form ist, der statische Pfad als Ganzes aber weiterhin bewusst als Komfort-API gekennzeichnet bleibt (siehe Einleitung von Kapitel 6). {{/info}} Variante mit `HttpClient`-Feinkonfiguration: {{code language="vb.net"}} ReCClient.BuildStaticClient( Sub(cfg) cfg.ConfigureClient = Sub(http) http.BaseAddress = New Uri("https://ihre-rec-api-adresse.com/") http.Timeout = TimeSpan.FromSeconds(30) End Sub End Sub) {{/code}} {{code language="csharp"}} ReCClient.BuildStaticClient(cfg => { cfg.ConfigureClient = http => { http.BaseAddress = new Uri("https://ihre-rec-api-adresse.com/"); http.Timeout = TimeSpan.FromSeconds(30); }; }); {{/code}} Nach dem `BuildStaticClient`-Aufruf liefert `ReCClient.Create()` Instanzen aus dem statischen Provider: {{code language="vb.net"}} Dim client As ReCClient = ReCClient.Create() Await client.RecActions.InvokeAsync(profilId, "batch-001") {{/code}} {{code language="csharp"}} var client = ReCClient.Create(); await client.RecActions.InvokeAsync(profileId, "batch-001"); {{/code}} ==== 6.1.2 Ältere Komfort-Overloads ==== Aus Bequemlichkeit existieren zwei weitere Overloads, die intern an die `StaticBuildConfiguration`-Variante delegieren. Sie sind als `Obsolete` markiert mit dem Hinweis, die `Action`-Variante zu verwenden, bleiben aber funktionsfähig: {{code language="vb.net"}} ' Variante mit Basis-URL als String ReCClient.BuildStaticClient("https://ihre-rec-api-adresse.com/") ' Mit Options-Callback und optionalem Logger ReCClient.BuildStaticClient( "https://ihre-rec-api-adresse.com/", Sub(opt) opt.LogSuccessfulRequests = True, myLogger) {{/code}} {{code language="csharp"}} // Variant with base URL string ReCClient.BuildStaticClient("https://ihre-rec-api-adresse.com/"); // With options callback and optional logger ReCClient.BuildStaticClient( "https://ihre-rec-api-adresse.com/", opt => opt.LogSuccessfulRequests = true, myLogger); {{/code}} === 6.2 Synchrone Wrapper über TaskSyncExtensions === `TaskSyncExtensions.Sync()` bzw. `Sync()` blockieren den aktuellen Thread, bis die `Task` abgeschlossen ist. Sie sind nützlich, wenn der umliegende Code (noch) nicht asynchron sein kann. {{code language="vb.net"}} Imports ReC.Client ' Blockiert bis die Task fertig ist recClient.RecActions.InvokeAsync(profilId, "batch-001").Sync() {{/code}} {{code language="csharp"}} using ReC.Client; // Blocks until the task completes recClient.RecActions.InvokeAsync(profileId, "batch-001").Sync(); {{/code}} Hinweis: In Umgebungen mit einem `SynchronizationContext` (z. B. WinForms, WPF oder bestimmte Test-Runner) kann blockierendes Warten zu Deadlocks führen. Für Konsolen- und Hintergrundprozesse ist das Risiko in der Regel gering. Wo immer möglich: `async/await` bevorzugen. === 6.3 Mittelfristige Empfehlung === * **`BuildStaticClient` / `Create`**: für Legacy-Einstiege okay; sobald `IServiceCollection` vorhanden ist, auf `services.AddRecClient(...)` und Konstruktor-Injektion umstellen. * **`TaskSyncExtensions.Sync`**: nur lokal kapseln. Wenn eine Methode bereits `async` sein darf, direkt `await` verwenden. * Die `[Obsolete]`-Warnung dient als **dauerhafter Reminder**, dass es sich um einen bewussten Komfort-Pfad handelt – sie ist kein Hinweis darauf, dass die APIs entfernt werden.