diff --git a/docs/ReC.Client.xwiki b/docs/ReC.Client.xwiki new file mode 100644 index 0000000..dc28b75 --- /dev/null +++ b/docs/ReC.Client.xwiki @@ -0,0 +1,470 @@ +== 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`.