Skip to content

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.AspNetCore and Dapr.Client are 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 daprd into pods with the dapr.io/enabled: true annotation.
  • Operator — reconciles Component and Configuration CRDs.
  • 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.Secrets are 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 developmentdapr init and dapr run work 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-store into 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 sitesSaveStateAsync("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=true or 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