Dapr for .NET
Key Points
- Dapr (Distributed Application Runtime) is a sidecar runtime for distributed application primitives — state, pub/sub, service-to-service invocation, secrets, bindings, actors, distributed lock, configuration, workflows — exposed to the app over HTTP/gRPC on localhost.
- The selling point: code against Dapr's uniform API; swap the underlying infrastructure (Redis vs Cosmos for state; Kafka vs Service Bus vs RabbitMQ for pub/sub; Key Vault vs AWS Secrets Manager for secrets) by changing a YAML component definition — no app code change.
- In .NET:
Dapr.AspNetCoreandDapr.Clientare the integration packages. Idiomatic ASP.NET Core programming model — middleware, attributes, DI; no Java-isms. - Where Dapr shines: polyglot teams (Java + .NET + Node behind the same primitives), multi-cloud / portable apps, complex state-machine workflows (Dapr Workflows), and actor patterns for stateful microservices.
- Where Dapr loses: a single .NET team in a single cloud (Azure or AWS) where
Microsoft.Extensions.*libraries already provide what Dapr abstracts. The sidecar's extra hop is real overhead. - Sidecar tax — every Dapr call is a localhost hop. For chatty inner loops, the latency adds up. For coarse-grained "save state once per request," it's negligible.
Concepts (deep dive)
What Dapr is, mechanically
Dapr is a separate process (the daprd binary, written in Go) that runs alongside your application — same pod in Kubernetes, same host on a VM, same machine in local dev. Your app talks to it over HTTP or gRPC on localhost (default ports 3500 / 50001):
┌─────────────────────────────────────────────────────┐
│ Pod / Host │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ Your .NET app │◄──►│ Dapr sidecar (daprd)│ │
│ │ (kestrel) │ HTTP/gRPC ▲ ▲ ▲ ▲ ▲ ▲ │ │
│ └─────────────────┘ localhost │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
└─────────────────────────────────┼─┼─┼─┼─┼─┼────┘ │
│ │ │ │ │ │
│ │ │ │ │ └─ Pub/sub (Kafka / Service Bus / Redis Streams)
│ │ │ │ └─── Secrets (Key Vault / AWS Secrets Manager / Vault)
│ │ │ └───── Bindings (Twilio / SendGrid / blob / queue)
│ │ └─────── State (Redis / Cosmos / DynamoDB / SQL)
│ └───────── Service (gRPC to other Dapr-enabled apps)
└─────────── Configuration (Azure App Configuration / etcd)
Dapr's components are defined in YAML files — one per backing service:
# State store component:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: state-store
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: redis:6379
- name: redisPassword
secretKeyRef:
name: redis-creds
key: password
The app talks to "the state store named state-store" — never directly to Redis. Switching to Cosmos DB or DynamoDB is a YAML edit, not a code change. That's the abstraction Dapr sells.
Building blocks (the Dapr API surface)
| Building block | What it does | Backing options |
|---|---|---|
| State management | Key/value persistence | Redis, Cosmos, DynamoDB, MongoDB, SQL Server, Postgres, others |
| Publish/subscribe | Topic-based messaging | Kafka, RabbitMQ, Service Bus, SNS, Redis Streams, NATS, Pulsar |
| Service invocation | Service-to-service calls (with mTLS, retries, tracing) | Built-in (other Dapr sidecars) |
| Bindings | Input/output triggers for external systems | Twilio, SendGrid, blob/queue triggers, Cron, HTTP |
| Secrets | Read secrets uniformly | Key Vault, AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault |
| Configuration | Read app config + subscribe to changes | Azure App Configuration, Redis, etcd, others |
| Distributed lock | Cross-instance mutual exclusion | Redis, Postgres |
| Workflows | Durable, stateful workflows | Built-in (uses state store under the hood) |
| Actors | Stateful turn-based virtual actors | Built-in (uses state store) |
| Crypto | Encryption operations | Key Vault, JWK, others |
Each building block is opt-in — you don't need to use all of them. A common starting point is state + pub/sub + secrets.
How a .NET app talks to Dapr
Two integration paths:
1. Dapr.Client — direct API:
using Dapr.Client;
builder.Services.AddDaprClient();
app.MapPost("/orders", async ([FromBody] Order order, DaprClient dapr, CancellationToken ct) =>
{
// Save to whichever state store is configured:
await dapr.SaveStateAsync("state-store", order.Id, order, cancellationToken: ct);
// Publish to whichever pub/sub broker is configured:
await dapr.PublishEventAsync("messagebus", "orders.placed", order, cancellationToken: ct);
// Read a secret:
var apiKey = (await dapr.GetSecretAsync("secrets-store", "twilio-key", cancellationToken: ct))["twilio-key"];
return Results.Accepted();
});
2. Dapr.AspNetCore — middleware + attributes:
builder.Services.AddControllers().AddDapr();
app.UseCloudEvents();
app.UseRouting();
app.UseAuthorization();
app.MapSubscribeHandler(); // serves /dapr/subscribe to tell Dapr which topics this app listens to
app.MapControllers();
// Controller:
public class OrdersController(DaprClient dapr) : ControllerBase
{
[Topic("messagebus", "orders.placed")]
[HttpPost("/orders/handle-placed")]
public async Task HandleOrderPlaced(Order order, CancellationToken ct)
{
// Dapr routes pub/sub messages to this endpoint
// ...
}
}
The [Topic] attribute tells Dapr "send messages from this topic on this pub/sub component to this endpoint." Dapr handles delivery, retries, CloudEvents envelope.
Dapr vs Microsoft.Extensions.* directly
The most-asked question for a .NET-only Azure team: "Why not just use Microsoft.Extensions.Caching.StackExchangeRedis and Azure.Messaging.ServiceBus directly?"
Real answer:
Direct SDK (e.g., Azure.Messaging.ServiceBus) | Dapr | |
|---|---|---|
| Lines of code per call | 1 SDK call | 1 Dapr call |
| Process count per service | 1 (your app) | 2 (your app + sidecar) |
| Latency per call | Same as backing service | Backing service + localhost hop (~300 µs) |
| Change Redis → Cosmos | Code change + redeploy | YAML edit + restart sidecar |
| Change Service Bus → Kafka | Code change + redeploy | YAML edit + restart sidecar |
| Cross-language uniformity | Different SDKs per language | Same HTTP API everywhere |
| Service mesh features (mTLS, retries) | Roll your own / use Polly | Built into the sidecar |
| Observability (distributed tracing) | Manual ActivitySource + OTel | Auto-emitted by sidecar |
| Ops overhead | App only | App + Dapr control plane |
For a single .NET service in a single Azure subscription, direct SDKs are the lower-overhead choice. For a polyglot product with five services in three languages and a stated multi-cloud goal, Dapr's abstraction starts paying off.
Dapr Workflows: durable state machines
Dapr Workflows (now stable) is the durable-execution feature — write a function, suspend on external events, resume on activity completion, survive crashes:
public class OrderWorkflow : Workflow<OrderRequest, OrderResult>
{
public override async Task<OrderResult> RunAsync(WorkflowContext ctx, OrderRequest input)
{
var inventory = await ctx.CallActivityAsync<bool>(nameof(ReserveInventory), input.Items);
if (!inventory) return new OrderResult { Status = "OutOfStock" };
var payment = await ctx.CallActivityAsync<bool>(nameof(ChargePayment), input.PaymentInfo);
if (!payment)
{
await ctx.CallActivityAsync(nameof(ReleaseInventory), input.Items);
return new OrderResult { Status = "PaymentFailed" };
}
await ctx.CallActivityAsync(nameof(NotifyCustomer), input.CustomerEmail);
// Wait up to 30 minutes for shipment confirmation:
var shipped = await ctx.WaitForExternalEventAsync<bool>(
"ShipmentConfirmed", TimeSpan.FromMinutes(30));
return new OrderResult { Status = shipped ? "Shipped" : "ShipmentTimeout" };
}
}
WorkflowContext is the magic — every awaited activity is checkpointed; the workflow can run for days; if the host crashes, it resumes from the last checkpoint on a different host. Equivalent in spirit to Durable Functions in Azure or Temporal the standalone product.
Use Dapr Workflows when you need durable orchestration but don't want to adopt Temporal or Azure Durable Functions specifically.
Dapr Actors: stateful virtual actors
The actor model: each actor is a tiny single-threaded entity identified by (type, id), with isolated state, message ordering guaranteed per actor. Dapr activates actors on demand, deactivates them on idle, persists their state through the configured state store.
public interface IShoppingCartActor : IActor
{
Task AddItem(CartItem item);
Task<List<CartItem>> GetItems();
Task Checkout();
}
public class ShoppingCartActor : Actor, IShoppingCartActor, IRemindable
{
public ShoppingCartActor(ActorHost host) : base(host) { }
public async Task AddItem(CartItem item)
{
var items = await StateManager.GetStateAsync<List<CartItem>>("items");
items.Add(item);
await StateManager.SetStateAsync("items", items);
}
// ...
}
When useful: per-entity state that needs serialized access (e.g., one cart per customer, one trading account per user, one IoT device per device ID). When not useful: stateless services; high-throughput per-key write workloads (actor's per-actor lock becomes the bottleneck).
The Dapr control plane
Dapr in production includes a control plane running in the cluster:
┌──────────────────────────────────────────────────────────┐
│ Kubernetes cluster │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Sidecar │ │ Sidecar │ │ Sidecar │ │ Operator │◄─┐
│ │ injector │ │ injector │ │ injector │ │ + Sentry │ ││
│ │ (per pod)│ │ (per pod)│ │ (per pod)│ │ (mTLS CA)│ ││
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││
│ ││
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ app A │ │ app B │ │ app C │ │ Placement│◄┘
│ │ + daprd │ │ + daprd │ │ + daprd │ │ (actors) │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘
└──────────────────────────────────────────────────────────┘
- Sidecar injector — Kubernetes mutating webhook that auto-injects
daprdinto pods with thedapr.io/enabled: trueannotation. - Operator — reconciles
ComponentandConfigurationCRDs. - Sentry — certificate authority for mTLS between Dapr sidecars.
- Placement — coordinates actor instance distribution across hosts.
For local dev, dapr init spins up a self-hosted control plane. For Azure, Azure Container Apps has built-in Dapr — you don't operate a control plane; Container Apps does. That's the easiest production on-ramp for Dapr in 2026.
Dapr in Azure Container Apps
Container Apps shipped first-class Dapr integration: you opt in per app, declare your components in the Container Apps environment, and the platform runs the sidecar for you. There's no daprd to manage, no operator to install, no Sentry CA to rotate. From the .NET side, integration is identical:
# Container App definition (Bicep):
resource app 'Microsoft.App/containerApps@2024-03-01' = {
properties: {
configuration: {
dapr: {
enabled: true
appId: 'orders-api'
appPort: 8080
appProtocol: 'http'
}
}
// ...
}
}
Components are defined at the Container Apps Environment level (one Service Bus or Cosmos can be reused across apps). For Azure-first .NET teams interested in Dapr, this is the path of least resistance.
When Dapr is the right call
- Polyglot teams: services in .NET, Java, Node, Python, Go all need the same primitives. Dapr abstracts the SDK-per-language tax.
- Multi-cloud / portability: a portable component model that swaps Service Bus for SQS by config.
- Workflows without Temporal: durable workflows without adopting a separate product.
- Actor patterns: virtual actors for per-entity serialized state.
- Container Apps in Azure: zero-cost on-ramp (no control plane to operate).
- Service mesh "lite": mTLS, retries, traces, without committing to Istio/Linkerd.
When Dapr is overkill
- Single .NET service, single cloud:
Azure.Messaging.ServiceBus+StackExchange.Redis+Azure.Security.KeyVault.Secretsare simpler and lower-latency. - Tight latency budgets (sub-millisecond): the sidecar hop adds ~300 µs minimum.
- Small teams: operating Dapr's control plane (if not on Container Apps) is non-trivial; the abstraction tax has to be paid for.
- One backend per primitive forever: if you'll never switch state stores or brokers, the portability you're buying is never used.
Dapr vs service mesh
Both deploy as sidecars; the overlap is real but partial:
| Dapr | Service mesh (Linkerd, Istio) | |
|---|---|---|
| Layer | App primitives (state, pub/sub, secrets) | Network (mTLS, traffic routing, retries) |
| App API | HTTP/gRPC the app calls explicitly | Transparent (the mesh intercepts) |
| Programming model | App-aware | App-unaware |
| Overlap | mTLS, retries, distributed tracing | mTLS, retries, distributed tracing |
They coexist — many teams run a service mesh and Dapr. Dapr handles the app-layer primitives; the mesh handles the transparent network layer. Most teams don't need both; start with one.
Operational realities
- Dapr's sidecar uses memory. Budget ~30–50 MB per app instance for the sidecar (more with workflows or large component sets).
- Component reloads — adding or changing a component requires a sidecar restart, but not an app restart.
- Per-component metadata in code — most things are config, but the names of components leak into your code (
SaveStateAsync("state-store", ...)). Refactor with care. - CloudEvents envelope — Dapr wraps pub/sub messages in CloudEvents. Non-Dapr publishers need to either send CloudEvents or pass
metadata.rawPayload=true. - Local development —
dapr initanddapr runwork well. The local Redis-backed defaults match nothing in production; explicitly point at the real components for staging-fidelity testing.
How it works under the hood
The sidecar's anatomy
daprd (the sidecar binary) is a Go process exposing two HTTP/gRPC servers:
- App API (default port 3500 HTTP / 50001 gRPC) — what your app calls. Translates
POST /v1.0/state/state-storeinto a Redis SET (or Cosmos upsert, or DynamoDB PutItem). - Internal API — used by other Dapr sidecars to invoke this service via Dapr's service invocation. mTLS between sidecars, with certificates from the Sentry control-plane CA.
The two servers share the same Component configuration; both go through the same component drivers (state, pub/sub, etc.).
Component lifecycle
A Component YAML is read by the Dapr operator (or dapr init for local). The sidecar fetches its assigned components on startup, instantiates the appropriate driver (e.g., state.redis, pubsub.servicebus), and connects to the backing service. The driver implementation lives inside daprd — Dapr ships with built-in drivers for dozens of services.
When you call SaveStateAsync("state-store", ...), the sidecar looks up "state-store" in its component table, dispatches to the Redis driver, and the driver issues a Redis SET. From the app's perspective, this is a single HTTP call to localhost.
Pub/sub delivery
When the sidecar starts, it queries the app at GET /dapr/subscribe:
[
{ "pubsubname": "messagebus", "topic": "orders.placed", "route": "/orders/handle-placed" },
{ "pubsubname": "messagebus", "topic": "orders.cancelled", "route": "/orders/handle-cancelled" }
]
The sidecar subscribes to the configured pub/sub broker (Kafka, Service Bus, etc.) on behalf of the app. When a message arrives, the sidecar POSTs it to the app's declared route. The app processes it; HTTP 200 ACKs the message, non-200 triggers retry.
This is why Dapr doesn't require an SDK for receiving messages — the app is a plain HTTP server; Dapr is the message-broker client.
mTLS between sidecars
Service invocation goes from app A → sidecar A → sidecar B → app B. The sidecar-to-sidecar hop is mTLS-encrypted; the certificates come from Sentry (Dapr's CA). The app sees plain HTTP/gRPC to its own sidecar. mTLS is "free" for any service-to-service call made via Dapr — no app code change to enable.
State concurrency: ETags
Dapr's state API supports optimistic concurrency via ETags:
var (current, etag) = await dapr.GetStateAndETagAsync<Order>("state-store", id);
current.Total += 10;
try
{
await dapr.SaveStateAsync("state-store", id, current, etag: etag);
}
catch (DaprException) { /* concurrency conflict; retry */ }
ETags map to the backing store's optimistic concurrency mechanism (Redis WATCH/MULTI/EXEC, Cosmos _etag, DynamoDB conditional writes). The abstraction is uniform across stores.
Actor placement
Actors are uniquely identified by (actor_type, actor_id). The placement service hashes the actor ID and assigns it to a specific Dapr host. When an actor is invoked, the calling Dapr sidecar routes the call to the host that owns that actor. State and processing are co-located. If that host fails, placement reassigns the actor to a new host; state is reloaded from the configured state store.
This is the "virtual actor" model — actors exist abstractly forever; they're activated when needed and deactivated when idle.
Code: correct vs wrong
❌ Wrong: hand-rolling per-cloud abstractions
// Service uses IMessageBus today (Service Bus); team plans multi-cloud:
public interface IMessageBus
{
Task PublishAsync<T>(string topic, T message, CancellationToken ct);
}
public class ServiceBusMessageBus : IMessageBus { /* ... */ }
public class SnsMessageBus : IMessageBus { /* ... */ }
public class KafkaMessageBus : IMessageBus { /* ... */ }
public class RedisStreamsMessageBus : IMessageBus { /* ... */ }
// ... and the same for state, secrets, ...
The team is re-inventing Dapr's component model, badly, in their own codebase. Each impl is a new dependency, a new test surface, a new failure mode. Dapr exists because this pattern is universal — adopt the prebuilt thing.
✅ Correct: Dapr if multi-cloud is real; direct SDK if it's not
// Multi-cloud requirement is real → Dapr:
await dapr.PublishEventAsync("messagebus", "orders.placed", order, ct);
// Single cloud forever → direct SDK:
await serviceBus.SendMessageAsync(new ServiceBusMessage(JsonSerializer.Serialize(order)));
The decision is upfront and binary: pay the abstraction cost when the abstraction value is real.
❌ Wrong: Dapr-everywhere in a single .NET service
// Inside a single in-process method:
var customer = await dapr.GetStateAsync<Customer>("state-store", customerId);
var product = await dapr.GetStateAsync<Product>("state-store", productId);
var pricing = await dapr.GetStateAsync<Pricing>("state-store", pricingId);
// 3 localhost hops, ~1ms total for what should be in-memory or a single DB query
For intra-service state, Dapr's sidecar hop is real overhead with no abstraction benefit (you don't change state stores per request). Use Dapr for cross-service concerns (pub/sub between services; portable state for actors).
✅ Correct: Dapr for cross-service primitives; direct DB for intra-service
// Cross-service event (Dapr — abstraction value is real):
await dapr.PublishEventAsync("messagebus", "orders.placed", order, ct);
// Intra-service query (direct EF Core — no abstraction needed):
var pricing = await _db.Pricings.FindAsync(productId);
❌ Wrong: components with secrets in plain YAML
spec:
type: state.redis
metadata:
- name: redisHost
value: redis:6379
- name: redisPassword
value: "P@ssw0rd!" # ← committed to git, visible in `kubectl get component`
The Component CRD is readable by any pod in the cluster.
✅ Correct: secret references
spec:
type: state.redis
metadata:
- name: redisHost
value: redis:6379
- name: redisPassword
secretKeyRef:
name: redis-creds
key: password
auth:
secretStore: kubernetes # or a Dapr secret store like Key Vault
The Component references a secret; the secret store handles rotation.
❌ Wrong: pub/sub with non-idempotent consumer
[Topic("messagebus", "orders.placed")]
public async Task Handle(Order order)
{
await SendConfirmationEmail(order.CustomerEmail); // ← run twice = two emails
await ChargeCard(order.PaymentMethod); // ← run twice = double charge
}
Dapr's pub/sub is at-least-once; the broker may retry. Non-idempotent handlers send two emails / double-charge cards.
✅ Correct: idempotency key + dedup
[Topic("messagebus", "orders.placed")]
public async Task Handle(Order order, [FromHeader(Name = "ce-id")] string eventId, CancellationToken ct)
{
if (await _processedEvents.HasBeenProcessedAsync(eventId, ct))
return; // dedup: already handled
await _processedEvents.MarkProcessedAsync(eventId, ct);
await SendConfirmationEmail(order.CustomerEmail);
await ChargeCard(order.PaymentMethod);
}
See Idempotency keys for the deep-dive.
Design patterns for this topic
Pattern 1 — "Dapr for cross-service, direct SDK for intra-service"
- Intent: pay the sidecar tax only where the abstraction earns it.
Pattern 2 — "Components in YAML, secrets via secret store"
- Intent: infrastructure-as-code that doesn't leak credentials into git.
Pattern 3 — "Container Apps + Dapr as the easy on-ramp"
- Intent: get Dapr's benefits without operating a control plane.
Pattern 4 — "Dapr Workflows instead of Temporal"
- Intent: durable orchestration without adopting a separate product.
Pattern 5 — "Actors for per-entity serialized state"
- Intent: state machines per customer / device / account with built-in concurrency control.
Pattern 6 — "Idempotent pub/sub consumers"
- Intent: Dapr is at-least-once; consumers must dedup.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Sidecar model | Polyglot uniformity; per-language SDK is just HTTP/gRPC; mTLS for free | Extra process per pod; localhost latency hop |
| Component model | Swap backing services by YAML; no app redeploy | The abstraction is "least common denominator" — some Service Bus / Cosmos features aren't exposed |
| Workflows | Durable orchestration without Temporal | Newer; smaller ecosystem than Temporal |
| Actors | Built-in state + concurrency per entity | Not always the right model; per-actor bottleneck |
| Container Apps integration | Zero control-plane operation; native | Azure-locked for the platform itself |
| Self-hosted Kubernetes | Full control | Operating the control plane is non-trivial |
When to use / when to avoid
- Use Dapr for polyglot or multi-cloud .NET workloads where uniform primitives across languages/clouds matter.
- Use Dapr Workflows when you need durable orchestration but don't want Temporal as a separate product.
- Use Dapr Actors for per-entity serialized state (carts, accounts, IoT devices).
- Use Container Apps + Dapr as the easy Azure on-ramp.
- Avoid Dapr in single-cloud single-language .NET shops where direct SDKs suffice — the sidecar tax isn't worth it.
- Avoid Dapr for high-throughput inner loops — the localhost hop accumulates.
- Avoid Dapr-everywhere within a single service — use it for cross-service concerns only.
Interview Q&A
Q1. What problem does Dapr solve? Polyglot and multi-cloud distributed-app uniformity. Apps call standard primitives (state, pub/sub, secrets, service invocation) via HTTP/gRPC on a localhost sidecar; the sidecar talks to whatever backing service is configured (Redis vs Cosmos, Service Bus vs Kafka). Swap by YAML, not by code.
Q2. How is Dapr different from a service mesh? A service mesh (Linkerd, Istio) operates at the network layer — transparent to the app, providing mTLS, retries, traffic shaping. Dapr operates at the app primitives layer — apps call a Dapr API explicitly. They overlap on mTLS and tracing; the rest is complementary.
Q3. How does .NET integrate with Dapr? Dapr.Client for direct API calls; Dapr.AspNetCore for middleware, [Topic] attribute-driven pub/sub subscriptions, and idiomatic ASP.NET Core integration. No Java-isms, no gRPC plumbing required in the app code.
Q4. What's the cost of the sidecar? ~30–50 MB memory per app instance; ~300 µs per localhost hop for HTTP, less for gRPC. For chatty inner-loop calls, the latency adds up; for coarse-grained "once per request" use, it's negligible.
Q5. When is Dapr overkill? Single .NET app, single cloud (Azure or AWS) where direct SDKs already cover the primitives. The abstraction value (swap backing services by config) is never used; the cost (sidecar process, latency hop) is constant.
Q6. What's the easiest production Dapr on-ramp in Azure? Azure Container Apps with Dapr enabled. The platform runs the control plane and the sidecar; you declare components at the environment level and opt in per app.
Q7. What are Dapr Workflows? Durable, stateful workflows — write a function that suspends on awaited activities and external events, surviving host crashes. Similar in spirit to Azure Durable Functions or Temporal, but built into Dapr. Useful when you want durable orchestration without adopting a separate product.
Q8. What are Dapr Actors? Virtual actors — small single-threaded entities identified by (type, id), with isolated state and serialized message processing. Activated on demand, persisted via the configured state store. Good for per-entity state machines (carts, accounts, devices).
Q9. How does pub/sub delivery work? The Dapr sidecar subscribes to the broker on the app's behalf. When a message arrives, the sidecar POSTs it to the app's declared route (from /dapr/subscribe). The app's HTTP 200 ACKs the message; non-200 triggers retry. Delivery is at-least-once — consumers must be idempotent.
Q10. How does Dapr handle secrets? Components reference secrets via secretKeyRef. The Dapr sidecar fetches the secret from the configured secret store (Key Vault, AWS Secrets Manager, Kubernetes Secrets) at component initialization. Secrets never appear in plain YAML or in the app code.
Q11. Can Dapr coexist with a service mesh? Yes. Many teams run both — the service mesh handles the transparent network layer; Dapr handles app primitives. Most teams don't need both; pick whichever solves the problem in front of you first.
Q12. What's the .NET-specific story for Dapr Workflows? The Dapr.Workflow package provides Workflow<TInput, TOutput> base class, WorkflowContext for awaiting activities and events, and DI registration via AddDaprWorkflow(...). The mental model is the same as Durable Functions: deterministic workflow code; activities encapsulate side effects.
Q13. Why might a team pick Dapr but skip Container Apps? Existing investment in AKS or another Kubernetes platform; cross-cluster portability requirements; specific cluster-level customisations the platform-as-service doesn't allow. AKS with Dapr Operator is the alternative; you operate the control plane yourself.
Gotchas / common mistakes
- ⚠️ Adopting Dapr for a single .NET app in a single cloud — pure overhead.
- ⚠️ Dapr inside a service for intra-service concerns — use direct DB/cache for in-process state; Dapr for cross-service.
- ⚠️ Non-idempotent pub/sub consumers — Dapr is at-least-once; design for retries.
- ⚠️ Secrets in plain Component YAML — anyone with cluster read access sees them.
- ⚠️ Component name leaking into many call sites —
SaveStateAsync("state-store-prod-2", ...)everywhere. Centralise the name as a constant. - ⚠️ Tight latency budgets and chatty sidecar use — the localhost hop adds up. Batch where possible.
- ⚠️ CloudEvents-wrapped vs raw payload mismatch — a non-Dapr publisher to a Dapr subscriber needs
metadata.rawPayload=trueor the consumer fails to deserialize. - ⚠️ Skipping Sentry/mTLS in production — the default certs work for dev; production should rotate them.
- ⚠️ Treating Dapr Workflows like sync code — non-deterministic code in workflow body (e.g.,
DateTime.Now, random GUIDs) breaks replay determinism. Wrap in activities. - ⚠️ Mismatched component versions — components written for an older Dapr API version may behave differently or fail; pin and test.
Further reading
- Dapr documentation
- Dapr .NET SDK (GitHub)
- Dapr Workflows for .NET
- Azure Container Apps — Dapr integration
- Dapr Components reference
- Migration Patterns — Strangler-Fig, Sidecar, ACL — the sidecar pattern Dapr embodies
- Azure Container Apps & AKS — the host platforms for Dapr in Azure
- Idempotency keys — for safe Dapr pub/sub consumers