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.
471 lines
17 KiB
Plaintext
471 lines
17 KiB
Plaintext
== 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`.
|