Files
ReC/docs/ReC.Client.xwiki
TekH 1fc395f495 Improve ReCClient static client and documentation
Updated `ReCClientOptions` to include a warning about the `LogSuccessfulRequests` option throwing an `InvalidOperationException` if no `ILogger` is registered via DI. Added validation and thread-safety to `BuildStaticClient` using `Lazy<IServiceProvider>`.

Introduced `StaticBuildConfiguration` for callback-based configuration and detailed its properties. Clarified usage patterns, added VB.NET and C# examples, and documented validation rules.

Marked older `BuildStaticClient` overloads as `[Obsolete]` while retaining functionality. Expanded context on static client use cases and synchronous wrappers. Improved documentation clarity and consistency.
2026-05-21 08:33:24 +02:00

553 lines
21 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
== 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}}
{{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<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. 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<IServiceProvider>`** 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<T>` 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.
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<StaticBuildConfiguration>`-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<TResult>()` 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.