Henara.Grpc.Client.DakotaSlim 2.2.0
Henara.Grpc.Client.DakotaSlim
Slim Dakota gRPC client for a native host that owns its own Keycloak login + session (written for the SmartVO Windows client). It adapts the host's access token onto the Dakota clients — it has no interactive login and no token refresh of its own. The host's session manager stays the single token authority.
The host must perform the interactive login before any Dakota call, on its UI thread, so a session exists by the time the clients are used. The per-RPC token resolver only reads/refreshes that session — it must not try to log in, because it runs inside the gRPC call-credentials interceptor on a background thread where opening the host's WPF login window throws. See Registration.
Do not also call
AddNativeKeycloakAuth(fromHenara.Grpc.Client.Common). That starts a second refresh-token rotation against the same Keycloak session and the two invalidate each other (invalid_grant).AddSlimNativeDakotaClientenforces this — it throws at registration if anyICallTokenProvideror native Keycloak provider is already registered.
Endpoints
Two environment variables, read and validated at registration (throws if missing):
| Variable | Value |
|---|---|
URL_DAKOTA_PIPELINE |
Dakota pipeline gateway gRPC URL |
URL_DAKOTA_REPORTER |
Dakota reporting gRPC URL |
http:// targets are served as plaintext h2c (call credentials are still attached); use https://
in production.
Registration
services.AddSlimNativeDakotaClient((sp, ct) => /* return a valid raw access token, no "Bearer " prefix */);
The callback is invoked per RPC and must return a valid (auto-refreshed) access token, doing no
UI work. In SmartVO it resolves ISessionManager and reads its current token:
services.AddSlimNativeDakotaClient((sp, ct) =>
sp.GetRequiredService<ISessionManager>().GetValidAccessTokenAsync(ct));
GetValidAccessTokenAsync only reads/refreshes an existing session and throws "Not signed in." when
Current is null. So the host must sign in once, on its UI thread, before the first Dakota call,
which establishes Current:
var tokens = await keycloakAuthService.LoginAsync(ct);
await sessionManager.ConsumeNewLoginToken(tokens, ct);
Do not call LoginAsync/ConsumeNewLoginToken from inside the resolver: the resolver runs in the
gRPC call-credentials interceptor on a background thread, and opening the WPF login/PIN windows there
throws.
This registers two clients — IDakotaGatewayClient (pipeline) and IDakotaReporterClient
(reporting). Inject them wherever you need them.
Pipeline — IDakotaGatewayClient
Submits Dakota processing jobs and queries retry state.
IAsyncEnumerable<DakotaDispatchResult> DispatchDakotaProcess(
List<DakotaPairRequest> pairs, CancellationToken ct = default);
Task<Result<bool, Failure>> IsPipelineStepRetryDispatched(Guid tickerId);
DispatchDakotaProcess streams one ESOL/AUF file pair per DakotaPairRequest and yields one result
per pair. The intake gate authorises each pair by the sender (praxis) IK carried in the file; a
rejected pair returns IsDispatched == false with a Message.
var pairs = new List<DakotaPairRequest>
{
new(
FilePair: new FileRequestInfoDto(esolBytes, aufBytes, "file.con"),
CustomerId: 20131386,
Channel: ChannelTypeEnum.Production,
IsEsolPreprocessed: false,
OverwriteRecipient: null)
};
await foreach (var r in gateway.DispatchDakotaProcess(pairs, ct))
Console.WriteLine(r.IsDispatched
? $"dispatched {r.FileName} -> {r.PipelineId}"
: $"rejected {r.FileName}: {r.Message}");
IsPipelineStepRetryDispatched returns true if a retry is already queued for a step ticker;
otherwise a Failure.
Reporting — IDakotaReporterClient
Reads process statuses and files. The server scopes all results to the signed-in user's customer id(s); Henara admins see everything.
IAsyncEnumerable<DakotaProcessInfoDto> GetDakotaStati(DakotaFilter? filter = null);
Task<(IReadOnlyList<DakotaFileRowDto> Rows, long Total)> GetFilesPaged(int page, int pageSize, string search);
IAsyncEnumerable<SingleIlFileInfoDto> DownloadFiles(List<(int oid, DownloadFileTypeEnum type)> filesToDownload);
GetDakotaStati— streams process-status aggregates. TheDakotaFilter(all members optional) narrows by customer id, receiver IK, time range, pipeline step, status, or file name;nullreturns everything the caller may see.GetFilesPaged— server-paged, searchable file list;searchmatches file name / receiver IK (empty matches all). Returns the page rows and the total count.DownloadFiles— streams raw file contents for the given object ids. Dakota file types areDownloadFileTypeEnum.DakotaEsolandDakotaAuf.
var filter = new DakotaFilter { CustomerId = new MatchesIntFilterDto(20131386) };
await foreach (var p in reporter.GetDakotaStati(filter))
Console.WriteLine($"{p.CreatedAt:o} {p.ReceiverIk} {p.FileName}");
var (rows, total) = await reporter.GetFilesPaged(page: 0, pageSize: 100, search: "");
var ids = rows.Select(r => (r.DataOid, DownloadFileTypeEnum.DakotaEsol)).ToList();
await foreach (var f in reporter.DownloadFiles(ids))
await File.WriteAllBytesAsync(f.FileName, f.FileData, ct);
DTO reference
| Type | Shape |
|---|---|
DakotaPairRequest |
(FileRequestInfoDto FilePair, int CustomerId, ChannelTypeEnum Channel, bool IsEsolPreprocessed, string? OverwriteRecipient) |
FileRequestInfoDto |
(byte[] EsolFile, byte[] AufFile, string FileName) |
ChannelTypeEnum |
Test, Staging, Production |
DakotaDispatchResult |
(bool IsDispatched, Guid? PipelineId, string FileName, string? Message) |
DakotaProcessInfoDto |
ProcessId, CustomerId, FileName, ReceiverIk, CreatedAt, EsolFileId/AufFileId, FileVersions, per-step Stages |
DakotaFileRowDto |
(int DataOid, DateTimeOffset CreatedAt, string FileName, string FileType, Guid ProcessId, string ReceiverIk, int Step) |
SingleIlFileInfoDto |
(Guid ProcessId, byte[] FileData, string FileType, string FileName, int Oid) |
DownloadFileTypeEnum |
DakotaEsol, DakotaAuf (plus IlResult, IlVerordnung — not Dakota) |
Identity
| Setting | Value |
|---|---|
| Keycloak realm | henara @ https://auth.praxx.me |
| Client id | henara-main-sso |
No packages depend on Henara.Grpc.Client.DakotaSlim.
.NET 10.0
- Henara.Grpc.Client.Dakota (>= 2.2.0)