Dapr, the Distributed Application Runtime, is a graduated CNCF project that puts the plumbing every microservice needs (service calls, messaging, state, secrets, actors and durable workflows) behind HTTP and gRPC APIs served by a sidecar that runs next to your app. This guide shows .NET developers how Dapr works, how to use each building block with the current Dapr .NET SDK (1.18), how to run it locally with the Dapr CLI and Aspire, how to host it on Azure Container Apps and Kubernetes, and when calling the Azure or Redis SDKs directly is the better choice.

What Is Dapr and Why Use It with .NET?#

Dapr turns distributed-systems patterns into APIs. Instead of referencing Service Bus, Redis and Key Vault clients in every service, your code asks a local sidecar to publish an event, save a key, read a secret or call another service. The sidecar translates each call for whatever infrastructure a YAML component points it at, so swapping Redis for Cosmos DB or RabbitMQ for Service Bus becomes a deployment change, as long as the new component supports the features you use.

Runtime 1.18 shipped in June 2026 (patch 1.18.4 followed in September), and the 1.18 SDK packages target .NET 8, 9 and 10. Per the SDK docs, .NET 8 and 9 support ends with the first Dapr release after their November 2026 end of life, so plan a move to .NET 10 LTS. Maturity varies by building block:

Building blockStatus (runtime 1.18)Main .NET entry point
Service invocationStableHttpClient with InvocationHandler
Publish and subscribeStableDaprClient.PublishEventAsync, WithTopic, Dapr.Messaging
State managementStableDaprClient state methods, Dapr.StateManagement
BindingsStableDaprClient.InvokeBindingAsync, input binding endpoints
SecretsStableDaprClient.GetSecretAsync, AddDaprSecretStore
ConfigurationStableDapr.Extensions.Configuration
ActorsStableDapr.Actors, Dapr.Actors.AspNetCore
WorkflowStableDapr.Workflow
JobsStableDapr.Jobs
Distributed lockAlphaDapr.DistributedLock
CryptographyAlphaDapr.Cryptography
Conversation (LLMs)AlphaDapr.AI, Dapr.AI.Microsoft.Extensions

The Conversation API is worth watching for AI workloads: Dapr.AI.Microsoft.Extensions exposes it as a DaprChatClient that implements IChatClient from Microsoft.Extensions.AI, moving the choice of LLM provider into a component file. It is alpha and does not support streaming yet, so treat it as an experiment.

How Dapr Works: The Sidecar Architecture#

Every app instance gets its own daprd sidecar: a process when you run locally, a container in the same pod on Kubernetes. Your code calls it on localhost (HTTP port 3500 and gRPC port 50001 by default), and the sidecar calls back into your app's port to deliver messages, input binding events and actor calls. The local hop is cheap compared with the network call behind it, but not free: each request is serialized twice and each replica carries an extra container.

The sidecar learns about infrastructure from components: YAML resources of kind: Component with a type such as state.redis, optionally scoped to app IDs and pulling credentials from a secret store. Resiliency resources declare timeouts, retries and circuit breakers per target app, actor type or component.

A small control plane supports the sidecars: placement tracks which instance hosts each actor, the scheduler stores jobs, workflow timers and actor reminders, and Sentry issues certificates for mutual TLS. Kubernetes adds an operator and a sidecar injector. Dapr also propagates W3C Trace Context across invocation and pub/sub and exports spans over OTLP, which fits neatly with OpenTelemetry in your .NET code.

YAML
# components/statestore.yaml: Redis state store that also backs actors
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
    - name: redisHost
      value: localhost:6379
    - name: actorStateStore
      value: "true"
scopes:
  - orders
  - carts
---
# components/resiliency.yaml: one retry layer, owned by the platform
apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
  name: default-resiliency
spec:
  policies:
    timeouts:
      general: 5s
    retries:
      transient:
        policy: exponential
        maxInterval: 15s
        maxRetries: 5
    circuitBreakers:
      downstream:
        maxRequests: 1
        timeout: 30s
        trip: consecutiveFailures >= 5
  targets:
    apps:
      inventory:
        timeout: general
        retry: transient
        circuitBreaker: downstream

Getting Started: Your First Dapr .NET Service#

Install the Dapr CLI, then run dapr init. It installs the daprd binary, starts local containers for Redis, Zipkin, placement and the scheduler, and writes default components under ~/.dapr. The --slim flag skips the containers and default components when Docker is not available.

Bash
dapr init

# Web project with the ASP.NET Core integration (brings in Dapr.Client)
dotnet new web -n Orders
dotnet add Orders package Dapr.AspNetCore

# Run the app with its sidecar; --resources-path replaces the old --components-path
dapr run --app-id orders --app-port 5080 --resources-path ./components \
  -- dotnet run --project Orders --urls http://localhost:5080

The Dapr.AspNetCore package adds AddDaprClient, CloudEvents middleware and subscription endpoints. DaprClient is thread-safe and registered as a singleton by default, so inject it wherever you need it.

C#
using Dapr.Client;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();

var app = builder.Build();

app.UseCloudEvents();      // unwrap CloudEvents so handlers bind to the payload
app.MapSubscribeHandler(); // serves /dapr/subscribe for programmatic subscriptions

app.MapPost("/orders", async (Order order, DaprClient dapr, CancellationToken ct) =>
{
    // Two independent writes: see the outbox section for an atomic alternative
    await dapr.SaveStateAsync("statestore", order.Id, order, cancellationToken: ct);
    await dapr.PublishEventAsync("pubsub", "orders.created", order, ct);
    return Results.Accepted($"/orders/{order.Id}", order);
});

app.MapGet("/orders/{id}", async (string id, DaprClient dapr, CancellationToken ct) =>
    await dapr.GetStateAsync<Order?>("statestore", id, cancellationToken: ct) is { } order
        ? Results.Ok(order)
        : Results.NotFound());

app.Run();

public sealed record Order(string Id, string CustomerId, decimal Total);

Service Invocation in .NET: Use HttpClient, Not InvokeMethodAsync#

Service invocation adds name-based discovery, mTLS, retries and tracing to service-to-service calls. The key change for .NET developers: SDK 1.18 marks the whole InvokeMethodAsync and InvokeMethodGrpcAsync family [Obsolete], formalizing guidance from 2023 to use native HTTP or gRPC clients. The factories are not deprecated: DaprClient.CreateInvokeHttpClient(appId) returns a configured HttpClient, and DaprClient.CreateInvocationInvoker(appId) returns a gRPC CallInvoker for generated clients.

CreateInvokeHttpClient builds a new HttpClient per call, so reuse it or, better, plug the SDK's InvocationHandler into IHttpClientFactory. The handler rewrites http://inventory/api/stock/42 to http://localhost:3500/v1.0/invoke/inventory/method/api/stock/42 and reads the sidecar address and API token from the standard Dapr environment variables.

C#
using System.Net.Http.Json;
using Dapr.Client;

builder.Services
    .AddHttpClient<InventoryClient>(client =>
        client.BaseAddress = new Uri("http://inventory")) // host name = target app ID
    .AddHttpMessageHandler(() => new InvocationHandler());

public sealed class InventoryClient(HttpClient http)
{
    public Task<StockLevel?> GetStockAsync(string sku, CancellationToken ct) =>
        http.GetFromJsonAsync<StockLevel>($"/api/stock/{Uri.EscapeDataString(sku)}", ct);

    public async Task ReserveAsync(Reservation reservation, CancellationToken ct)
    {
        using var response = await http.PostAsJsonAsync("/api/reservations", reservation, ct);
        response.EnsureSuccessStatusCode();
    }
}

public sealed record StockLevel(string Sku, int Available);
public sealed record Reservation(string OrderId, string Sku, int Quantity);

Decide where retries live. If the Dapr resiliency policy above retries calls to inventory, do not also attach a retrying Polly or standard resilience handler to the same client, or one failure multiplies into dozens of attempts.

Publish and Subscribe with CloudEvents#

Pub/sub decouples producers from consumers through a broker component. Dapr wraps every message in a CloudEvents 1.0 envelope, uses the app ID as the consumer group so each message goes to one instance of each subscribing app, and guarantees at-least-once delivery. That last point shapes your handlers: duplicates will happen, so every subscriber must be idempotent.

The subscriber's HTTP response controls redelivery. A 2xx response acknowledges the message unless the body carries a status of RETRY or DROP. A 404 logs an error and drops the message, and any other status code causes a retry. Pair that with a dead-letter topic so poison messages are parked rather than lost.

C#
using Dapr;

app.MapPost("/events/orders-created", async (
        Order order, IOrderProjection projection, CancellationToken ct) =>
    {
        if (order.Total <= 0)
        {
            return Results.Ok(new { status = "DROP" }); // invalid forever: do not retry
        }

        if (await projection.HasProcessedAsync(order.Id, ct))
        {
            return Results.Ok(); // duplicate delivery: acknowledge and move on
        }

        await projection.ApplyAsync(order, ct); // an exception here returns 500, so Dapr retries
        return Results.Ok();
    })
    .WithTopic(new TopicOptions
    {
        PubsubName = "pubsub",
        Name = "orders.created",
        DeadLetterTopic = "orders.created.deadletter",
    });

public interface IOrderProjection
{
    Task<bool> HasProcessedAsync(string orderId, CancellationToken ct);
    Task ApplyAsync(Order order, CancellationToken ct);
}

Controllers use the [Topic("pubsub", "orders.created")] attribute instead of WithTopic. Both rely on MapSubscribeHandler, which tells the sidecar about your programmatic subscriptions at startup. Dapr also supports declarative subscriptions in YAML and streaming subscriptions that pull messages over a gRPC stream without an inbound endpoint; the Dapr.Messaging package wraps the streaming model for .NET.

State Management with ETags and Transactions#

The state API is a key/value store with optional strong consistency, optimistic concurrency through ETags, TTLs, bulk reads and multi-key transactions. Without an ETag, writes are last-write-wins; supplying one gives you first-write-wins semantics and a clean way to detect conflicting updates. By default, keys are prefixed with the app ID, so two services never share state by accident. Remember that feature support depends on the component: transactions, queries and ETags behave differently from store to store.

C#
using System.Text.Json;
using Dapr.Client;

public sealed class InventoryStore(DaprClient dapr)
{
    private const string Store = "statestore";
    private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
    private static readonly StateOptions FirstWrite = new()
    {
        Concurrency = ConcurrencyMode.FirstWrite,
        Consistency = ConsistencyMode.Strong,
    };

    // Read-modify-write with optimistic concurrency; retry when another writer wins
    public async Task<bool> TryReserveAsync(string sku, int quantity, CancellationToken ct)
    {
        for (var attempt = 1; attempt <= 3; attempt++)
        {
            var (stock, etag) = await dapr.GetStateAndETagAsync<StockItem?>(
                Store, sku, cancellationToken: ct);
            if (stock is null || stock.Available < quantity)
            {
                return false;
            }

            var updated = stock with { Available = stock.Available - quantity };
            if (await dapr.TrySaveStateAsync(Store, sku, updated, etag, FirstWrite,
                    cancellationToken: ct))
            {
                return true;
            }
        }

        throw new InvalidOperationException($"Concurrent updates kept conflicting for {sku}.");
    }

    // All-or-nothing write of several keys
    public Task CommitAsync(StockItem stock, string stockEtag, Reservation reservation,
        CancellationToken ct)
    {
        List<StateTransactionRequest> operations =
        [
            new(stock.Sku, JsonSerializer.SerializeToUtf8Bytes(stock, Json),
                StateOperationType.Upsert, etag: stockEtag, options: FirstWrite),
            new($"reservation-{reservation.OrderId}",
                JsonSerializer.SerializeToUtf8Bytes(reservation, Json),
                StateOperationType.Upsert),
        ];

        return dapr.ExecuteStateTransactionAsync(Store, operations, cancellationToken: ct);
    }
}

public sealed record StockItem(string Sku, int Available);

Transactions are also the fix for the dual write in the getting-started sample. When a state store component sets outboxPublishPubsub and outboxPublishTopic, Dapr implements the transactional outbox pattern: a state transaction commits and the matching event is published, or neither happens. Newer code can also use the Dapr.StateManagement package, which offers a dedicated client and source-generated typed stores and can coexist with DaprClient while you migrate.

Bindings and Secrets#

Bindings connect your app to external systems that are not message brokers. An input binding triggers your app: the sidecar sends a POST to a route named after the component, for example on a cron schedule, when a blob lands or when a queue receives a message. An output binding calls out with an operation such as create, get or delete, with metadata keys specific to each component. For plain HTTP calls between your own services, use service invocation instead.

The secrets API reads from Key Vault, Kubernetes secrets, AWS Secrets Manager and other stores through the same call. Components can reference secrets with secretKeyRef, and the Dapr.Extensions.Configuration package can load secrets into IConfiguration at startup.

C#
using Dapr.Client;

var builder = WebApplication.CreateBuilder(args);

// Load secrets into IConfiguration; waits up to 10 seconds for the sidecar to start
builder.Configuration.AddDaprSecretStore(
    "secretstore", new DaprClientBuilder().Build(), TimeSpan.FromSeconds(10));
builder.Services.AddDaprClient();

var app = builder.Build();

// Input binding: a bindings.cron component named "nightly-export" POSTs here
app.MapPost("/nightly-export", async (DaprClient dapr, CancellationToken ct) =>
{
    var secret = await dapr.GetSecretAsync("secretstore", "partner-api-key",
        cancellationToken: ct);
    var report = new ExportReport(DateOnly.FromDateTime(DateTime.UtcNow), secret.Count);

    // Output binding: bindings.azure.blobstorage component named "exports"
    var metadata = new Dictionary<string, string>
    {
        ["blobName"] = $"{report.Day:yyyy-MM-dd}.json",
    };
    await dapr.InvokeBindingAsync("exports", "create", report, metadata, ct);
    return Results.Ok();
});

app.Run();

public sealed record ExportReport(DateOnly Day, int SecretCount);

Dapr Actors in .NET#

Dapr actors implement the virtual actor model popularized by Orleans. An actor is identified by a type and an ID, activated on first use, garbage-collected after an idle period (60 minutes by default) and given turn-based, single-threaded access, so its code needs no locks. The placement service routes each actor ID to exactly one instance, and the state store component must set actorStateStore to "true". Timers are in-memory and die with the activation, while reminders are persisted and fire even after the actor was deactivated.

The .NET strongly typed proxy serializes arguments with the Data Contract Serializer, so mark records with [DataContract] and [DataMember], as the Dapr docs recommend.

C#
using System.Runtime.Serialization;
using Dapr.Actors;
using Dapr.Actors.Client;
using Dapr.Actors.Runtime;

public interface ICartActor : IActor
{
    Task AddItemAsync(CartItem item);
    Task<CartItem[]> GetItemsAsync();
}

[DataContract]
public sealed record CartItem(
    [property: DataMember] string Sku,
    [property: DataMember] int Quantity);

public sealed class CartActor(ActorHost host) : Actor(host), ICartActor, IRemindable
{
    private const string ItemsKey = "items";
    private const string ExpiryReminder = "expire-cart";

    public async Task AddItemAsync(CartItem item)
    {
        var items = await StateManager.GetOrAddStateAsync(ItemsKey, new List<CartItem>());
        items.Add(item);
        await StateManager.SetStateAsync(ItemsKey, items);

        // Persisted reminder: fires in 24 hours even if the actor was deactivated
        await RegisterReminderAsync(ExpiryReminder, [], TimeSpan.FromHours(24),
            Timeout.InfiniteTimeSpan);
    }

    public async Task<CartItem[]> GetItemsAsync() =>
        [.. await StateManager.GetOrAddStateAsync(ItemsKey, new List<CartItem>())];

    public Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime,
        TimeSpan period) =>
        reminderName == ExpiryReminder
            ? StateManager.RemoveStateAsync(ItemsKey)
            : Task.CompletedTask;
}

// Program.cs
builder.Services.AddActors(options => options.Actors.RegisterActor<CartActor>());
var app = builder.Build();
app.MapActorsHandlers();

app.MapPost("/carts/{cartId}/items", async (
    string cartId, CartItem item, IActorProxyFactory proxies) =>
{
    var cart = proxies.CreateActorProxy<ICartActor>(new ActorId(cartId), nameof(CartActor));
    await cart.AddItemAsync(item);
    return Results.NoContent();
});

Reentrancy is off by default, so a call chain that loops back into an actor blocks until it times out; enable ReentrancyConfig deliberately or keep call graphs acyclic. Compared with Microsoft Orleans, Dapr actors can be called from any language, while Orleans offers richer .NET-native features such as streams, transactions and custom placement. The SDK also ships Dapr.Actors.Next, a rewrite built on source generators whose API is still settling.

Dapr Workflow: Durable Orchestration in C#

Dapr Workflow is a durable execution engine: you write orchestration as ordinary async C#, and the runtime checkpoints each step and replays history after a crash. It covers chaining, fan-out and fan-in, approvals with timeouts, sagas and child workflows. SDK 1.17 rebuilt Dapr.Workflow without the DurableTask dependency and added versioning; SDK 1.18 added source-generated registration and history propagation, and runtime 1.18 previews cryptographically signed workflow history.

The rule that matters most is determinism. The workflow body is replayed many times, so it must not perform I/O, read the clock or generate random values directly. Put side effects in activities, use context.CurrentUtcDateTime and context.NewGuid(), and log through context.CreateReplaySafeLogger.

C#
using Dapr.Workflow;

public sealed record CheckoutRequest(string OrderId, string CustomerId, decimal Total);
public sealed record PaymentResult(bool Succeeded, string? Reason);

public sealed class CheckoutWorkflow : Workflow<CheckoutRequest, string>
{
    public override async Task<string> RunAsync(WorkflowContext context, CheckoutRequest order)
    {
        var logger = context.CreateReplaySafeLogger<CheckoutWorkflow>();
        var retry = new WorkflowTaskOptions(new WorkflowRetryPolicy(
            maxNumberOfAttempts: 5,
            firstRetryInterval: TimeSpan.FromSeconds(2),
            backoffCoefficient: 2.0,
            maxRetryInterval: TimeSpan.FromMinutes(1)));

        await context.CallActivityAsync(nameof(ReserveStockActivity), order, retry);
        var payment = await context.CallActivityAsync<PaymentResult>(
            nameof(ChargePaymentActivity), order, retry);

        if (!payment.Succeeded)
        {
            // Saga compensation: undo the reservation
            await context.CallActivityAsync(nameof(ReleaseStockActivity), order, retry);
            logger.LogWarning("Payment failed for {OrderId}: {Reason}",
                order.OrderId, payment.Reason);
            return "Cancelled";
        }

        try
        {
            // Durable wait: costs nothing while idle and survives restarts
            await context.WaitForExternalEventAsync<bool>(
                "ShipmentConfirmed", TimeSpan.FromDays(3));
            return "Completed";
        }
        catch (TaskCanceledException)
        {
            await context.CallActivityAsync(nameof(EscalateActivity), order);
            return "Escalated";
        }
    }
}

public interface IPaymentGateway
{
    Task<PaymentResult> ChargeAsync(string customerId, decimal amount, string idempotencyKey);
}

// Activities are normal DI-enabled classes and may perform I/O.
// ReserveStockActivity, ReleaseStockActivity and EscalateActivity follow the same shape.
public sealed class ChargePaymentActivity(IPaymentGateway payments)
    : WorkflowActivity<CheckoutRequest, PaymentResult>
{
    public override Task<PaymentResult> RunAsync(WorkflowActivityContext context,
        CheckoutRequest order) =>
        payments.ChargeAsync(order.CustomerId, order.Total, context.TaskExecutionKey);
}

// Program.cs: explicit registration works in every 1.x SDK; 1.18 can discover types itself
builder.Services.AddDaprWorkflow(options =>
{
    options.RegisterWorkflow<CheckoutWorkflow>();
    options.RegisterActivity<ReserveStockActivity>();
    options.RegisterActivity<ChargePaymentActivity>();
    options.RegisterActivity<ReleaseStockActivity>();
    options.RegisterActivity<EscalateActivity>();
});

app.MapPost("/checkout", async (CheckoutRequest order, DaprWorkflowClient workflows) =>
{
    var id = await workflows.ScheduleNewWorkflowAsync(
        nameof(CheckoutWorkflow), $"checkout-{order.OrderId}", order);
    return Results.Accepted($"/checkout/{id}", new { id });
});

app.MapPost("/checkout/{id}/shipped", async (string id, DaprWorkflowClient workflows) =>
{
    await workflows.RaiseEventAsync(id, "ShipmentConfirmed", true);
    return Results.NoContent();
});

Changing a workflow while instances are in flight is the classic trap: replayed history no longer matches the code. Since SDK and runtime 1.17 you can wrap a change in if (context.IsPatched("add-fraud-check")) so old instances keep the old path, or register a new workflow name for larger rewrites. WorkflowTaskOptions also accepts a TargetAppId, which lets one workflow call activities hosted by other Dapr apps.

Running Dapr Locally and with .NET Aspire#

For a handful of services, the CLI's multi-app run is enough: a dapr.yaml file lists each app's ID, directory, port and command, and dapr run -f . starts them all with sidecars. For a richer inner loop, .NET Aspire can orchestrate sidecars next to your projects and show their logs and traces in the dashboard.

The original Aspire.Hosting.Dapr package is deprecated; NuGet points to CommunityToolkit.Aspire.Hosting.Dapr instead. The integration shells out to the Dapr CLI (dapr run), so the CLI must be installed, and actors and workflows still need the placement and scheduler containers that dapr init starts. When you add a state store or pub/sub resource without a file path, the integration generates in-memory components, which is ideal for tests but not for anything durable.

C#
// AppHost/Program.cs (package: CommunityToolkit.Aspire.Hosting.Dapr)
using CommunityToolkit.Aspire.Hosting.Dapr;

var builder = DistributedApplication.CreateBuilder(args);

var stateStore = builder.AddDaprStateStore("statestore",
    new DaprComponentOptions { LocalPath = "../components/statestore.yaml" });
var pubSub = builder.AddDaprPubSub("pubsub"); // in-memory component for local runs

builder.AddProject<Projects.Inventory>("inventory")
    .WithDaprSidecar("inventory");

builder.AddProject<Projects.Orders>("orders")
    .WithDaprSidecar(sidecar => sidecar.WithOptions(new DaprSidecarOptions
    {
        AppId = "orders",
        EnableApiLogging = true,
    }))
    .WithReference(stateStore)
    .WithReference(pubSub);

builder.Build().Run();

Dapr on Azure Container Apps and Kubernetes#

Azure Container Apps offers managed Dapr: enable it per app with an app ID, port and protocol, and define components per environment, scoped to app IDs and authenticated with a managed identity through azureClientId instead of connection strings. Check the support matrix first. Microsoft's January 2026 documentation lists invocation, state, pub/sub, bindings, actors, secrets and configuration as generally available, yet states that Dapr's actor and workflow SDK packages are not supported. Custom Configuration resources and sidecar annotations are unavailable, Container Apps jobs cannot use Dapr, and actor reminders need minReplicas of at least 1.

Bash
cat > pubsub.yaml <<'EOF'
componentType: pubsub.azure.servicebus.topics
version: v1
metadata:
  - name: namespaceName
    value: "contoso-orders.servicebus.windows.net"
  - name: azureClientId        # user-assigned managed identity, no secrets
    value: "{identity-client-id}"
scopes:
  - orders
  - shipping
EOF

az containerapp env dapr-component set --name orders-env --resource-group rg-orders \
  --dapr-component-name pubsub --yaml ./pubsub.yaml

az containerapp create --name orders --resource-group rg-orders \
  --environment orders-env --image contoso.azurecr.io/orders:1.4.2 \
  --ingress internal --target-port 8080 \
  --enable-dapr --dapr-app-id orders --dapr-app-port 8080 --dapr-app-protocol http

Kubernetes gives you full control and every building block. Install the control plane with dapr init -k --enable-ha=true or with the dapr/dapr Helm chart and global.ha.enabled=true, which runs three replicas of each control plane service. You then opt pods in with annotations, and the sidecar injector adds daprd to each pod. See Running .NET on Kubernetes for probes, resource limits and rollout strategy.

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders
spec:
  replicas: 3
  selector:
    matchLabels:
      app: orders
  template:
    metadata:
      labels:
        app: orders
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "orders"
        dapr.io/app-port: "8080"
        dapr.io/config: "tracing"
        dapr.io/log-level: "info"
    spec:
      containers:
        - name: orders
          image: contoso.azurecr.io/orders:1.4.2
          ports:
            - containerPort: 8080

Best Practices#

  • Upgrade the runtime and SDK together. Keep the sidecar and Dapr.* packages on the same minor version, and track the supported-versions table so production does not drift out of support.
  • Scope everything. Limit components, secrets and pub/sub topics to the app IDs that need them. Unscoped components are loaded by every app in the namespace or environment.
  • Make subscribers and activities idempotent. At-least-once delivery and workflow retries both produce duplicates; dedupe with a processed-ID store or a natural idempotency key.
  • Choose one retry layer per call path. Let either Dapr resiliency policies or in-process Polly pipelines retry a given call, never both.
  • Prefer managed identity in components. Use azureClientId or workload identity rather than connection strings, and keep any remaining credentials in a secret store referenced through secretKeyRef.
  • Wait for the sidecar in workers. Background services that call Dapr immediately at startup should call WaitForSidecarAsync first.
  • Run the control plane in HA mode. Placement and scheduler outages stall actors, reminders and workflows even when your pods are healthy.

Common Pitfalls#

  • Treating Dapr as exactly-once. Neither pub/sub nor bindings deduplicate for you; a handler that charges a card twice is your bug, not Dapr's.
  • Keeping obsolete invocation code. InvokeMethodAsync still compiles with warnings, and creating a fresh HttpClient per request through CreateInvokeHttpClient wastes sockets. Move to InvocationHandler with IHttpClientFactory.
  • Forgetting actorStateStore. Actors and workflows fail to start if no state store sets actorStateStore to "true".
  • Non-deterministic workflows. DateTime.UtcNow, Guid.NewGuid(), HTTP calls or DI services used inside the workflow body break replay. Use context APIs and activities.
  • Assuming components are interchangeable. Transactions, ETags, TTL and query support differ per store. Test against the production component, not only the in-memory or Redis one.
  • Stacking retries. Broker redelivery, Dapr resiliency and Polly retries multiply. Draw the call path and count the attempts.
  • Expecting full Dapr on every platform. Azure Container Apps limits workflow, actor SDK and configuration features. Confirm support before designing around them.

Dapr vs Direct SDKs: Trade-offs and When to Use Dapr#

Dapr is an abstraction, and abstractions trade depth for breadth. The table compares Dapr building blocks with calling each service's own .NET SDK, such as Azure.Messaging.ServiceBus or StackExchange.Redis.

ConcernDapr building blocksDirect SDKs
Portability across clouds and brokersHigh: swap components through YAMLLow: code is tied to each client library
Provider-specific featuresCommon subset plus component metadataFull feature set, such as sessions or change feed
Latency and resource costExtra local hop and a sidecar per replicaDirect network call, nothing extra to run
Service-to-service securitymTLS and app-ID access policies built inYou configure TLS, identity and policies
Resilience and tracingDeclarative policies, W3C tracing in the sidecarPolly and OpenTelemetry configured in each app
Polyglot teamsSame API from every languageEach language uses its own SDK
OperationsControl plane to install, upgrade and monitorNo additional platform
DebuggingTwo processes on every call pathSingle process

Choose Dapr when you run many services in several languages, need portability across environments, or want actors, workflows and pub/sub under one operational model. Choose direct SDKs when a service depends on a platform-specific feature such as Service Bus sessions, when latency budgets are tight, or when an all-.NET team already gets discovery, configuration and telemetry from Aspire. Mixing is fine: use Dapr pub/sub and workflows while calling Cosmos DB directly.

Frequently Asked Questions#

Is Dapr a service mesh?#

No. The Dapr FAQ describes it as a developer-facing runtime rather than a service mesh. Both use sidecars and both can provide mTLS and tracing, but a mesh manages network traffic transparently, while Dapr gives application code APIs for state, messaging, actors and workflows. They can run together if you need the traffic management a mesh provides.

What replaced InvokeMethodAsync in the Dapr .NET SDK?#

Native HTTP and gRPC clients. SDK 1.18 marks the InvokeMethodAsync and InvokeMethodGrpcAsync overloads as obsolete. Use InvocationHandler with IHttpClientFactory, the DaprClient.CreateInvokeHttpClient factory, or DaprClient.CreateInvocationInvoker for generated gRPC clients.

Should I use Dapr actors or Orleans?#

Use Orleans when your system is .NET-only and you want the richest actor feature set, including streams, transactions, custom placement and in-process performance. Use Dapr actors when actors must be callable from several languages or when you already run Dapr for other building blocks. Both implement the virtual actor model, so the programming concepts transfer.

Can I use Dapr with .NET Aspire?#

Yes, through the CommunityToolkit.Aspire.Hosting.Dapr package, which replaced the deprecated Aspire.Hosting.Dapr. Call WithDaprSidecar on a project and add components with AddDaprStateStore, AddDaprPubSub or AddDaprComponent. The Dapr CLI must be installed because the integration launches sidecars through dapr run.

Does Dapr Workflow run on Azure Container Apps?#

Check the current Container Apps documentation before you plan on it. As of January 2026, Microsoft lists the core building blocks as generally available but states that Dapr's workflow and actor SDK packages are not supported there. If durable workflows are central to your design, run Dapr on Kubernetes or evaluate Durable Functions.

Summary#

  • Dapr exposes distributed-systems building blocks through a sidecar, so .NET code calls simple local APIs while YAML components choose the infrastructure.
  • Use HttpClient with InvocationHandler for service invocation, idempotent handlers for at-least-once pub/sub, ETags and transactions for state, and the outbox for atomic state-plus-event writes.
  • Actors and workflows are stable and powerful, but workflows demand deterministic code and a versioning strategy.
  • Develop locally with dapr init, multi-app run or the Community Toolkit Aspire integration. Deploy to Kubernetes for full control, or to Azure Container Apps for managed simplicity with a narrower feature set.
  • Weigh portability and uniform operations against sidecar cost and lost provider-specific features. Adopting Dapr for some building blocks only is a valid choice.

Further Reading#