Updated `RecActions.InvokeAsync(...).Sync()` to align with migration guidelines, marking `Sync()` as `[Obsolete]` and recommending `async/await` for asynchronous patterns. Enhanced `BuildStaticClient` methods to include an optional `configureOptions` parameter for flexible `ReCClientOptions` configuration. Added conditional compilation for nullable reference type compatibility across .NET Framework and modern .NET versions. Updated `Services.AddRecClient` calls to support `configureOptions`. Retained `[Obsolete]` on static helpers to encourage dependency injection (`services.AddRecClient(...)`) for new code. Revised migration notes to emphasize deprecation of synchronous methods, static helpers, and the importance of adopting modern async and DI patterns. Clarified changes to `GetAsync` methods, error handling with `ReCApiException`, and deserialization behavior.
434 lines
14 KiB
Plaintext
434 lines
14 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.
|