maksit-dapr/README.md
2026-08-01 19:27:59 +02:00

17 KiB
Raw Permalink Blame History

MaksIT.Dapr

Line Coverage Branch Coverage Method Coverage

NuGet facade over Dapr for ASP.NET Core: pub/sub, state, invocation, bindings, secrets, configuration, cryptography, sidecar, lock, HA work leases, actors, and workflows — all with MaksIT.Results outcomes.

Table of Contents

Overview

MaksIT.Dapr wraps Dapr building blocks so application code depends on small IDapr*Service facades instead of raw DaprClient calls. Failures surface as MaksIT.Results outcomes that map cleanly to HTTP via ToActionResult().

Need Register Inject
Publish events RegisterPubSub() IDaprPubSubService
Key/value state RegisterStateStore() IDaprStateStoreService
Call another Dapr app RegisterInvocation() IDaprInvocationService
Trigger external systems (queues, cron, …) RegisterBinding() IDaprBindingService
Read secrets from a Component RegisterSecrets() IDaprSecretService
Dynamic config + subscribe RegisterConfiguration() IDaprConfigurationService
Encrypt/decrypt via Dapr crypto Component RegisterCryptography() IDaprCryptographyService
Wait for sidecar / health / metadata RegisterSidecar() IDaprSidecarService
Short-lived distributed mutex RegisterLock() IDaprLockService
Multi-replica exclusive work (leader/bootstrap) RegisterWorkLeases(storeName) IDaprWorkLeaseService
Virtual actors RegisterActors(...) IDaprActorService
Durable workflows RegisterWorkflows(...) IDaprWorkflowService

RegisterDaprClientFacades() registers all DaprClient-backed rows above (not actors, workflows, or work leases).

Result conventions

  • Success / failure: check result.IsSuccess. Map to HTTP with result.ToActionResult().
  • Cancellation: facades rethrow OperationCanceledException (do not wrap as a failed Result).
  • State misses: GetStateAsync / GetStateAndETagAsync return Ok(null) / Ok((null, null)) when the key is absent (including JetStream key not found surfaced as gRPC Internal). !IsSuccess means infrastructure failure.
  • Topic handlers: return Result via ToActionResult()Ok ACK, ServiceUnavailable retry, BadRequest drop.

Getting Started

Ensure that you have the following installed:

Installation

dotnet add package MaksIT.Dapr

Or in your .csproj:

<PackageReference Include="MaksIT.Dapr" Version="2.2.0" />

Registering services

using MaksIT.Dapr.Extensions;

var builder = WebApplication.CreateBuilder(args);

// All DaprClient-backed facades (pub/sub, state, invocation, binding, secrets, …)
builder.Services.RegisterDaprClientFacades();
// Or register individually: RegisterPubSub(), RegisterStateStore(), …

// HA coordination (leases via Dapr state Component — e.g. persistent NATS KV)
builder.Services.RegisterWorkLeases("maksit-cicd-state"); // storeName for LeasedBackgroundService

builder.Services.RegisterActors(options => {
  options.Actors.RegisterActor<MyActor>();
});
builder.Services.RegisterWorkflows();

var app = builder.Build();

// After authentication/authorization middleware
app.RegisterSubscriber();
app.RegisterActorsHandlers();

Services

Pub/Sub — IDaprPubSubService

When to use: fire-and-forget or at-least-once messaging between services (commands, domain events, fan-out). Prefer this over direct broker SDKs so the app stays Component-agnostic (Redis, NATS, Kafka, …).

When not to: request/response RPC → service invocation; long-running orchestrations → workflows; exclusive multi-replica jobs → work leases.

DI RegisterPubSub() (also in RegisterDaprClientFacades())
Pipeline app.RegisterSubscriber() + [Topic("pubsubName", "topic")] on controllers
API PublishEventAsync, PublishByteEventAsync, BulkPublishEventAsync
var result = await pubSub.PublishEventAsync("my-pubsub", "my-topic", command, cancellationToken: ct);
if (!result.IsSuccess) { /* handle */ }

[Topic("my-pubsub", "my-topic")]
[HttpPost("/my-endpoint")]
public IActionResult Receive([FromBody] MyCommand payload) =>
  Result.Ok().ToActionResult(); // ACK; ServiceUnavailable → retry; BadRequest → drop

State store — IDaprStateStoreService

When to use: shared key/value state (caches, documents, coordination metadata) through a Dapr state Component. Also the foundation for work leases.

When not to: primary transactional business data that belongs in your product database; use the apps own data layer for that.

DI RegisterStateStore()
API Set / Get / Delete, ETag OCC (GetStateAndETagAsync, TrySaveStateAsync, TryDeleteStateAsync), bulk, query, transactions
var save = await stateStore.SetStateAsync("my-store", "my-key", "my-value", cancellationToken: ct);
var get = await stateStore.GetStateAsync<string>("my-store", "my-key", cancellationToken: ct);
if (!get.IsSuccess) { /* infra failure */ }
var value = get.Value; // null when key is missing

Service invocation — IDaprInvocationService

When to use: synchronous HTTP-style calls to another Dapr app id (mTLS, retries, and discovery handled by the sidecars). Good for request/response between microservices without hard-coding URLs.

When not to: broadcast or decoupled events → pub/sub; calling non-Dapr external APIs → bindings or a normal HttpClient.

DI RegisterInvocation()
API InvokeAsync overloads (void / request / response / both) — HTTP POST via CreateInvokableHttpClient
var result = await invocation.InvokeAsync<OrderRequest, OrderResponse>(
  appId: "orders",
  methodName: "create",
  data: request,
  cancellationToken: ct);

Bindings — IDaprBindingService

When to use: invoke an output binding Component (send to a queue, call Twilio, write to blob storage, cron-triggered input handlers on the app side, etc.) without embedding vendor SDKs.

When not to: app-to-app messaging → pub/sub or invocation.

DI RegisterBinding()
API InvokeAsync (fire-and-forget or typed response)
var result = await bindings.InvokeAsync("my-smtp", "create", emailPayload, cancellationToken: ct);

Secrets — IDaprSecretService

When to use: load secrets at runtime from a Dapr secret store Component (Kubernetes secrets, Azure Key Vault, local files, …) when the host should not bind every secret into IConfiguration up front.

When not to: replace standard ASP.NET appsettings / appsecrets.json for ordinary host config — MaksIT hosts still prefer configuration binding for app settings; use this facade for Component-backed secret reads from application code.

DI RegisterSecrets()
API GetAsync, GetBulkAsync
var secret = await secrets.GetAsync("my-secret-store", "connection-string", cancellationToken: ct);

Configuration — IDaprConfigurationService

When to use: read or subscribe to keys from a Dapr configuration Component (feature flags, dynamic settings that change without redeploy).

When not to: static startup configuration already covered by IConfiguration / Options — keep using the host configuration stack for that.

DI RegisterConfiguration()
API GetAsync, SubscribeAsync, UnsubscribeAsync

Cryptography — IDaprCryptographyService

When to use: encrypt/decrypt payloads with keys managed by a Dapr cryptography Component (keys stay in the sidecar/Component, not in app memory as raw key material).

When not to: simple password hashing or app-local crypto libraries when you do not need Dapr-managed keys.

DI RegisterCryptography()
API EncryptAsync, DecryptAsync

Sidecar — IDaprSidecarService

When to use: gate startup or background work until the sidecar is ready; health/outbound checks; inspect metadata; request graceful sidecar shutdown.

Typical scenario: call WaitForSidecarAsync before the first work-lease race or state call on cold start so replicas do not fail while the sidecar is still connecting.

DI RegisterSidecar()
API CheckHealthAsync, CheckOutboundHealthAsync, WaitForSidecarAsync, GetMetadataAsync, ShutdownAsync
var ready = await sidecar.WaitForSidecarAsync(ct);
if (!ready.IsSuccess) { /* abort startup path */ }

Distributed lock — IDaprLockService

When to use: a short-lived distributed mutex on a Dapr lock Component — e.g. protect a critical section across processes for seconds, then unlock. Check TryLockResponse.Success; dispose the response (or call UnlockAsync) when finished. Expiry is in seconds via the Dapr API.

When not to: multi-replica leader election, bootstrap, or long-running exclusive sweeps → prefer HA work leases (state-backed TTL, auto-renew, generation fencing, LeasedBackgroundService / bootstrap helpers). Per-entity serialized logic → actors.

DI RegisterLock() (also in RegisterDaprClientFacades())
API LockAsyncTryLockResponse, UnlockAsyncUnlockResponse
Component Dapr lock store (separate from the state Component used by work leases)
var locked = await locks.LockAsync("my-lock-store", "invoice-42", runtimeInstance.InstanceId, expiryInSeconds: 15, ct);
if (!locked.IsSuccess)
  return; // infra failure

await using var handle = locked.Value; // DisposeAsync unlocks
if (!handle.Success)
  return; // another owner holds the lock

/* critical section */

Or unlock explicitly with UnlockAsync when you are not disposing the TryLockResponse.

HA work leases — IDaprWorkLeaseService

When to use: only one replica among many should run a piece of work (DB migrate/bootstrap, periodic sweep, leader-style job). Leases live in a Dapr state Component (e.g. persistent NATS JetStream KV) — broker-agnostic, ETag concurrency, optional auto-renew and generation fencing.

When not to: short critical sections with an explicit unlock → distributed lock; per-entity serialized state machines → actors; multi-step durable processes → workflows.

Namespace MaksIT.Dapr.Services.WorkLease
DI RegisterWorkLeases() or RegisterWorkLeases("state-component-name")
Also registered IDaprStateStoreService, IDaprRuntimeInstanceId (POD_NAME in Kubernetes)
Helpers TryHoldAsyncDaprWorkLeaseHold, DaprWorkLeaseBootstrap.RunBootstrapUnderLeaseAsync, LeasedBackgroundService
using MaksIT.Dapr.Services.WorkLease;

builder.Services.RegisterWorkLeases("maksit-cicd-state");

await using var hold = (await leases.TryHoldAsync(
  storeName: "maksit-cicd-state",
  workKey: "bootstrap",
  holderId: runtimeInstance.InstanceId,
  ttl: TimeSpan.FromMinutes(5),
  autoRenew: true,
  cancellationToken: ct)).Value;

if (hold is null)
  return; // another replica holds the lease

var stillHeld = await hold.EnsureStillHeldAsync(ct);
if (stillHeld is { IsSuccess: true, Value: true }) {
  /* exclusive work; hold.Generation is a fencing token */
}

// Bootstrap: leader runs under lease; followers poll until ready
await DaprWorkLeaseBootstrap.RunBootstrapUnderLeaseAsync(
  leases,
  "maksit-cicd-state",
  "bootstrap",
  runtimeInstance.InstanceId,
  TimeSpan.FromMinutes(5),
  bootstrap: async ct => { /* migrate / seed */ return Result.Ok(); },
  isReady: async ct => Result<bool>.Ok(await db.IsMigratedAsync(ct)),
  cancellationToken: ct);

For recurring exclusive sweeps, subclass LeasedBackgroundService and implement ExecuteLeasedAsync (uses IDaprWorkLeaseOptions.StoreName from RegisterWorkLeases(storeName)). Before lease races on cold start, call IDaprSidecarService.WaitForSidecarAsync.

Actors — IDaprActorService

When to use: virtual-actor patterns — single-threaded access to an entity id (cart, device, session), turn-based concurrency, reminders/timers. Define actor interfaces and implementations in the product; this facade creates proxies and invokes methods with Result outcomes.

When not to: cluster-wide singleton jobs → work leases; multi-step saga across many services → workflows.

DI RegisterActors(options => options.Actors.RegisterActor<T>())
Pipeline app.RegisterActorsHandlers()
API Create / Create<TActor>, InvokeAsync overloads
builder.Services.RegisterActors(o => o.Actors.RegisterActor<CounterActor>());
app.RegisterActorsHandlers();

var created = actors.Create<ICounterActor>("cart-42", nameof(CounterActor));
if (!created.IsSuccess)
  return created.ToResult().ToActionResult();

var count = await created.Value.IncrementAsync();

Workflows — IDaprWorkflowService

When to use: durable, multi-step business processes (order pipeline, approval flow) with history, wait-for-external-event, suspend/resume, and replay. Author Workflow / WorkflowActivity types in the product (auto-discovered on Dapr SDK 1.18+).

When not to: simple fire-and-forget messages → pub/sub; single exclusive background job → work leases.

DI RegisterWorkflows(configure?)
API ScheduleAsync, get/wait state, RaiseEventAsync, terminate/suspend/resume/purge, list IDs, history, RerunFromEventAsync
builder.Services.RegisterWorkflows();

var scheduled = await workflows.ScheduleAsync(
  workflowName: nameof(OrderProcessingWorkflow),
  input: order,
  instanceId: order.Id,
  cancellationToken: ct);

if (!scheduled.IsSuccess)
  return scheduled.ToResult().ToActionResult();

var completed = await workflows.WaitForCompletionAsync(scheduled.Value, cancellationToken: ct);

Choosing a coordination pattern

Scenario Prefer
One replica runs migrate/bootstrap/sweep Work leases
Short critical section across processes Distributed lock
Per-entity single-threaded logic Actors
Long-running multi-step process with history Workflows
Decoupled async events Pub/sub
Sync call to another Dapr app Invocation

Contributing

See CONTRIBUTING.md for build/test (RepoUtils), commit format, and release scripts.

Contact

License

See LICENSE.md.