Update ReC.Client docs with DI, async, and API changes

Comprehensively updated the documentation for the ReC.Client
library to improve usability and align with modern .NET
development practices. Key changes include:

- Added an introduction to the library, its purpose, and
  supported frameworks (.NET 8 and .NET Framework 4.6.2).
- Documented core features like DI support, typed APIs,
  consistent error handling, and flexible GET methods.
- Provided installation and setup instructions with examples
  in VB.NET and C#.
- Explained usage patterns for GET endpoints (typed and
  dynamic), CRUD operations, and invoking RecActions.
- Highlighted error handling via ReCApiException with examples.
- Added a section on testing with in-process API testing
  recommendations.
- Marked static APIs and synchronous helpers as [Obsolete],
  explaining limitations and providing migration tips.
- Provided migration guidance for recent API changes, such as
  `GetAsync<T>` returning deserialized values and unified
  error handling.
- Addressed FAQs about new patterns and deprecated methods.

These updates aim to modernize the library, promote best
practices, and simplify adoption for developers.
This commit is contained in:
2026-05-20 14:04:05 +02:00
parent afd5cd5fbd
commit 983f3f76ad

470
docs/ReC.Client.xwiki Normal file
View File

@@ -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<T>(...)`) 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<ReCClientOptions>(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<T>(...)` 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<object>(...)` 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<RecActionViewDto[]>(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<ProfileViewDto>(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<Program>` **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<ReCApiException>(...)`, 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<HttpClient>)`
* `ReCClient.Create()`
* `TaskSyncExtensions.Sync(...)` und `TaskSyncExtensions.Sync<TResult>(...)`
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<T>(...)` 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<T>` 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<object>` 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<T>(...)` 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`.