Dapr in .NET: Sidecar Architecture for Cloud-Native Microservices
Build cloud-native .NET microservices with Dapr. Covers service invocation, pub/sub, state management, secret stores, bindings, the actor model, and running Dapr locally and on Kubernetes.
What is Dapr?
Dapr (Distributed Application Runtime) is a sidecar-based runtime that provides common microservice capabilities as HTTP/gRPC APIs ā regardless of your language or framework. Your .NET app talks to localhost:3500; Dapr handles the rest.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Your Pod ā
ā āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā .NET App āāāāāŗā Dapr Sidecar ā ā
ā ā :5000 ā ā :3500 (HTTP) ā ā
ā āāāāāāāāāāāāāāāā ā :50001 (gRPC) ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāWhat Dapr solves:
- Service-to-service calls with retries and mTLS ā without writing Polly policies
- Pub/sub over any broker (RabbitMQ, Kafka, Azure Service Bus) ā one API, swap the broker in config
- State management over any store (Redis, Cosmos DB, PostgreSQL) ā one API, swap the store
- Secret management from any vault ā one API, swap the provider
- Distributed actors without Orleans or Akka
Setup
# Install Dapr CLI
winget install Dapr.CLI
# Initialise Dapr (installs Redis + Zipkin containers locally)
dapr init
# Install .NET SDK
dotnet add package Dapr.AspNetCore
dotnet add package Dapr.ClientService Invocation
Call another Dapr-enabled service by its app-id. Dapr handles discovery, retries, mTLS, and tracing.
// Inject DaprClient
builder.Services.AddDaprClient();
// Call another service
public class OrderService
{
private readonly DaprClient _dapr;
public OrderService(DaprClient dapr) => _dapr = dapr;
public async Task<Product> GetProductAsync(string productId, CancellationToken ct)
{
// Calls http://localhost:3500/v1.0/invoke/product-service/method/products/{id}
// Dapr resolves "product-service" ā actual URL via service discovery
return await _dapr.InvokeMethodAsync<Product>(
HttpMethod.Get,
appId: "product-service",
methodName: $"products/{productId}",
cancellationToken: ct);
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
{
return await _dapr.InvokeMethodAsync<CreateOrderRequest, Order>(
HttpMethod.Post,
appId: "order-service",
methodName: "orders",
data: request,
cancellationToken: ct);
}
}Receiving Invocations
Your service just exposes normal ASP.NET Core endpoints ā Dapr routes to them:
[ApiController]
[Route("products")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public async Task<Product> GetProduct(string id) => await _repo.GetAsync(id);
}Pub/Sub
Publish and subscribe to events across services. Swap the broker by changing a YAML component file ā your code doesn't change.
Publisher
public class OrderService
{
private readonly DaprClient _dapr;
public async Task CreateOrderAsync(Order order, CancellationToken ct)
{
await _repo.SaveAsync(order, ct);
// Publish to "orders" topic on "pubsub" component
await _dapr.PublishEventAsync(
pubsubName: "pubsub",
topicName: "order-created",
data: new OrderCreatedEvent(order.Id, order.CustomerId, order.Total),
cancellationToken: ct);
}
}Subscriber
// Register Dapr subscriptions
app.MapSubscribeHandler();
[ApiController]
public class OrderEventsController : ControllerBase
{
[Topic("pubsub", "order-created")] // Dapr attribute ā subscribes this endpoint
[HttpPost("order-created")]
public async Task<IActionResult> HandleOrderCreated(
OrderCreatedEvent @event,
CancellationToken ct)
{
await _emailService.SendConfirmationAsync(@event.CustomerId, @event.OrderId, ct);
return Ok();
}
}Pub/Sub Component (Redis locally, swap for prod)
# components/pubsub.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis # swap to pubsub.rabbitmq / pubsub.azure.servicebus / pubsub.kafka
version: v1
metadata:
- name: redisHost
value: localhost:6379State Management
Store and retrieve state from any backend using one API. Dapr handles serialisation, ETag-based optimistic concurrency, and TTL.
public class CartService
{
private readonly DaprClient _dapr;
private const string StoreName = "statestore";
// Save state
public async Task AddToCartAsync(string userId, CartItem item, CancellationToken ct)
{
var cart = await GetCartAsync(userId, ct) ?? new Cart(userId);
cart.AddItem(item);
await _dapr.SaveStateAsync(
storeName: StoreName,
key: $"cart-{userId}",
value: cart,
cancellationToken: ct);
}
// Get state
public async Task<Cart?> GetCartAsync(string userId, CancellationToken ct)
=> await _dapr.GetStateAsync<Cart>(StoreName, $"cart-{userId}", ct);
// Delete state
public async Task ClearCartAsync(string userId, CancellationToken ct)
=> await _dapr.DeleteStateAsync(StoreName, $"cart-{userId}", ct);
// Transactional state (multiple operations atomically)
public async Task CheckoutAsync(string userId, CancellationToken ct)
{
var ops = new StateTransactionRequest[]
{
new($"cart-{userId}", null, StateOperationType.Delete),
new($"order-{Guid.NewGuid()}", JsonSerializer.SerializeToUtf8Bytes(new Order()), StateOperationType.Upsert)
};
await _dapr.ExecuteStateTransactionAsync(StoreName, ops, cancellationToken: ct);
}
}Secret Management
Read secrets from any vault ā Key Vault, HashiCorp Vault, Kubernetes secrets ā using one API.
// Read a single secret
var secret = await _dapr.GetSecretAsync(
storeName: "secretstore",
key: "db-connection-string");
string connectionString = secret["db-connection-string"];
// Use in configuration
builder.Configuration.AddDaprSecretStore("secretstore", new DaprClientBuilder().Build());
// Then access normally: configuration["db-connection-string"]# components/secretstore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: secretstore
spec:
type: secretstores.azure.keyvault # or secretstores.kubernetes / secretstores.hashicorp.vault
version: v1
metadata:
- name: vaultName
value: my-key-vaultActors
Dapr virtual actors: an actor is created on first message, deactivated when idle, and reactivated on demand. Dapr handles placement, single-threaded execution, and reminder persistence.
// Define the interface
public interface IOrderActor : IActor
{
Task<OrderState> GetStateAsync();
Task SubmitAsync();
Task CancelAsync(string reason);
}
// Implement
[Actor(TypeName = "OrderActor")]
public class OrderActor : Actor, IOrderActor, IRemindable
{
public OrderActor(ActorHost host) : base(host) { }
public async Task<OrderState> GetStateAsync()
=> await StateManager.GetStateAsync<OrderState>("state");
public async Task SubmitAsync()
{
var state = await StateManager.GetStateAsync<OrderState>("state");
state.Status = "Submitted";
await StateManager.SetStateAsync("state", state);
// Register a reminder ā fires even if actor is deactivated
await RegisterReminderAsync("payment-check", null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
{
if (reminderName == "payment-check")
await CheckPaymentStatusAsync();
}
}
// Register
builder.Services.AddActors(options =>
{
options.Actors.RegisterActor<OrderActor>();
});
app.MapActorsHandlers();
// Call an actor from another service
var proxy = ActorProxy.Create<IOrderActor>(
new ActorId(orderId.ToString()),
"OrderActor");
await proxy.SubmitAsync();Running with Dapr
# Run a single service with Dapr sidecar
dapr run \
--app-id order-service \
--app-port 5001 \
--components-path ./components \
-- dotnet run --project OrderFlow.Orders
# Run multiple services (dapr.yaml)
dapr run -f dapr.yaml# dapr.yaml
version: 1
apps:
- appID: order-service
appDirPath: ./src/OrderFlow.Orders
appPort: 5001
command: ["dotnet", "run"]
- appID: product-service
appDirPath: ./src/OrderFlow.Products
appPort: 5002
command: ["dotnet", "run"]Dapr on Kubernetes
# Annotate your deployment ā Dapr injects the sidecar
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
template:
metadata:
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "order-service"
dapr.io/app-port: "8080"
dapr.io/config: "dapr-config" # optional: tracing config
dapr.io/log-level: "info"# Install Dapr on Kubernetes
helm repo add dapr https://dapr.github.io/helm-charts/
helm install dapr dapr/dapr --namespace dapr-system --create-namespaceObservability
Dapr emits OpenTelemetry traces automatically for all service invocations and pub/sub:
# dapr-config.yaml
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: dapr-config
spec:
tracing:
samplingRate: "1"
zipkin:
endpointAddress: http://zipkin:9411/api/v2/spansInterview Questions
Q: What is Dapr and why use it instead of writing client libraries directly? Dapr is a sidecar runtime providing distributed system capabilities (service invocation, pub/sub, state, secrets) as HTTP/gRPC APIs. Instead of each team writing Polly retry logic, service discovery, and broker clients, Dapr provides these once. The broker, state store, and secret provider are swappable via config ā your code never changes when you move from RabbitMQ to Kafka.
Q: How does Dapr pub/sub differ from using RabbitMQ/MassTransit directly?
With MassTransit, your code references RabbitMQ types. Switching brokers requires code changes. With Dapr, your code calls _dapr.PublishEventAsync("pubsub", "topic", data). The broker is configured in a YAML component file. Swap pubsub.rabbitmq for pubsub.azure.servicebus with no code changes.
Q: What is a Dapr actor and how does it differ from a regular service? A Dapr virtual actor represents a single entity (one order, one user session). It has single-threaded execution (no concurrency bugs), persistent state, and is deactivated when idle. A regular service handles many requests concurrently and manages its own state. Use actors when you need per-entity state isolation and serialised access ā e.g., game sessions, shopping carts, order workflows.
Q: What are Dapr reminders vs timers? Both schedule work for an actor. Timers fire relative to the current activation ā they're lost if the actor deactivates. Reminders are persisted ā they fire even after the actor deactivates and reactivates. Use reminders for durable scheduled work (payment retries, expiry checks).
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.