Get every Dometrain Course at 40% off! Dometrain is an educational courses platform by Nick Chapsas. With 79 courses, 20 authors and more than 55.000 students, there is no better place to invest in your learning as a .NET engineer. Use code MILAN03 to get 40% off your first year of Dometrain Pro. Get every course 40% off!
Archera is unique among cloud cost management products for a simple reason: they insure your cloud commitments. That means if you underutilize, say, an AWS Savings Plan or Microsoft Azure Consumption Commitment, they will write you an actual check to cover the difference. They also allow you to achieve the cost savings of native cloud savings plans with a commitment as low as 30 days, instead of 1 to 3 years. Check out their Azure offerings here.
Building distributed systems has never been more important—or more challenging. As .NET developers, we're constantly juggling service discovery, state management, messaging, resilience patterns, and various infrastructure SDKs. Our business logic gets buried under mountains of plumbing code, and we become tightly coupled to specific technologies.
What if there was a better way?
Enter Dapr (Distributed Application Runtime), an open-source project that handles the complex infrastructure challenges so you can focus on what matters most: your application's business logic.
In this article, we'll explore how Dapr transforms microservice development for .NET developers by:
- Abstracting away infrastructure-specific code behind consistent APIs
- Providing standardized building blocks for common distributed system patterns
- Enabling you to use the same code from local development to production
- Integrating seamlessly with .NET and ASP.NET Core applications
Whether you're building your first microservice or evolving a complex system, Dapr offers a simpler path forward. Let's explore how it works.
What is Dapr?
Dapr is a portable, event-driven runtime that simplifies building microservices. As a graduated project within the Cloud Native Computing Foundation (CNCF), Dapr has proven its value in production environments.
At its core, Dapr provides standardized building blocks that abstract away the complexity of common microservice patterns. Rather than wrestling with infrastructure-specific code, you can focus on your business logic while Dapr handles the rest.
Before Dapr, building a microservice architecture in .NET might require direct integration with multiple infrastructure components:
// Pre-Dapr approach - direct infrastructure dependencies
builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = "redis:6379"; });
builder.Services.AddSingleton<IMessageBroker>(provider => new KafkaMessageBroker("kafka:9092"));
builder.Services.AddSingleton<ISecretManager>(provider =>
new AzureKeyVaultClient(new Uri("https://myvault.vault.azure.net")));
This approach tightly couples your application to specific technologies.
With Dapr, you gain flexibility through standardized APIs:
// Dapr approach - simple, consistent APIs
builder.Services.AddDaprClient();
// Later in code:
// State management (could be Redis, Cosmos DB, etc.)
await daprClient.SaveStateAsync("statestore", "customer-123", customerData);
// Pub/sub (could be Kafka, RabbitMQ, etc.)
await daprClient.PublishEventAsync("pubsub", "orders", orderData);
// Secrets (could be Azure Key Vault, HashiCorp Vault, etc.)
var secret = await daprClient.GetSecretAsync("secretstore", "api-keys");
The underlying providers can be swapped without code changes - just by updating Dapr component configuration files.
The Sidecar Pattern: How Dapr Works
Dapr uses the sidecar architectural pattern, where it runs as a separate process alongside your application:
Your application communicates with the Dapr sidecar through HTTP or gRPC, and Dapr handles communication with infrastructure services. This separation brings several benefits:
- Language Agnostic: Dapr works with any programming language, including all .NET variants
- Cross-cutting Concerns: Security, observability, and resiliency are handled by Dapr, not your code
- Infrastructure Abstraction: Your application remains decoupled from specific technologies
- Simplified Development: Clean, maintainable code focused on business logic
- Production Ready: Built-in features that improve reliability in production environments
Building Blocks: Dapr's Core Capabilities
Dapr offers several building blocks that solve common microservice challenges. Each provides a standardized API that abstracts away infrastructure complexity:
- Service Invocation: Enables reliable service-to-service communication with automatic service discovery, load balancing, and retries.
- State Management: Provides a unified way to store and retrieve state with features like concurrency control and transactions.
- Pub/Sub: Implements asynchronous messaging between services, allowing for loosely-coupled, event-driven architectures.
- Workflows: Enables you to define long running, persistent processes that span multiple microservices.
- Bindings: Connects your applications to external systems, either for triggering your app from external events or invoking external services.
- Actors: Implements the virtual actor pattern, making it easy to build stateful microservices with encapsulated state and behavior.
- Secrets: Offers secure access to sensitive configuration like connection strings and API keys from various secret stores.
- Configuration: Centralizes application settings with support for dynamic updates across multiple services.
- Distributed Lock: Provides mutually exclusive access to shared resources in a distributed environment.
- Cryptography: Offers encryption and decryption operations while handling key management.
- Jobs: Allows you to schedule and orchestrate jobs (e.g., schedule batch processing jobs to run every business day)
- Conversation: Lets you supply prompts to converse with different large language models (LLMs). Includes prompt caching and PII obfuscation.
Here's an overview of Dapr's building blocks and the most popular services they interact with:
Let's explore the most commonly used building blocks in depth.
Service Invocation
The service invocation building block enables reliable service-to-service communication with automatic mTLS encryption, retries, and observability.
This solves several challenging microservice problems:
- Service Discovery: Finding where services are located
- Resilient Communication: Handling transient failures gracefully
- Load Balancing: Distributing requests across multiple instances
- Observability: Tracking requests across service boundaries
- Security: Encrypting traffic between services
Here's a simple example of invoking a service using Dapr's .NET SDK:
// Client application making a request
using Dapr.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
app.MapGet("/checkout/{itemId}", async (int itemId, DaprClient daprClient) =>
{
// Create order data
var orderData = new OrderData(itemId, DateTime.UtcNow);
// Invoke the order-processing service
var result = await daprClient.InvokeMethodAsync<OrderData, string>(
"order-processor",
"process-order",
orderData);
return Results.Ok(new { Message = $"Order {itemId} processed: {result}" });
});
await app.RunAsync();
public record OrderData(int ItemId, DateTime OrderedAt);
And the corresponding service handling the request:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/process-order", (OrderData order) =>
{
Console.WriteLine($"Processing order {order.ItemId} placed at {order.OrderedAt}");
return $"Order {order.ItemId} confirmation: #{Guid.NewGuid().ToString()[..8]}";
});
await app.RunAsync();
public record OrderData(int ItemId, DateTime OrderedAt);
What's happening here:
- The
checkout
service callsInvokeMethodAsync
using theDaprClient
to send a request to theorder
service - The Dapr sidecar for the checkout service receives this request
- The Dapr sidecar looks up the location of the order service
- The request is forwarded to the Dapr sidecar of the order service
- The order service's Dapr sidecar forwards the request to the order service
- The response follows the reverse path
This process provides automatic service discovery, encryption, retries, and distributed tracing without any additional code.
Publish & Subscribe
The publish and subscribe building block provides asynchronous messaging between services with at-least-once delivery guarantees. This pattern is essential for building resilient, loosely-coupled microservices that can:
- Process operations asynchronously without blocking the user
- Continue functioning when downstream services are unavailable
- Scale independently based on workload
Pub/Sub in Dapr follows this flow:
- A publisher service sends a message to a topic via the Dapr sidecar
- The publisher Dapr sidecar converts the message to the CloudEvent format and forwards it to the configured message broker
- Subscriber services receive the message through their Dapr sidecars
- The subscriber application processes the message
Dapr uses component configuration files to define the pub/sub message broker. Here's a typical Redis pub/sub component:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: order-events
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ''
The key parts of this configuration are:
metadata.name
: The component name (order-events
) that your application will reference when publishing/subscribingspec.type
: The type of component (pubsub.redis
in this case)spec.metadata
: Configuration specific to the component type
This file should be placed in a components
directory where Dapr can discover it.
When running locally, this is typically ./components/
relative to your application.
What if you want to switch from Redis to RabbitMQ?
You'd replace the spec.type
with pubsub.rabbitmq
and update the metadata
section accordingly.
This change doesn't require any code modifications in your application.
Isn't this flexibility amazing?
Here's how to publish events using Dapr:
// Publisher service
using Dapr.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
app.MapPost("/create-order", async (OrderRequest request, DaprClient daprClient) =>
{
var orderEvent = new OrderCreatedEvent(
request.OrderId,
request.CustomerId,
request.Items,
DateTime.UtcNow
);
// Publish event to "orders" topic
await daprClient.PublishEventAsync("order-events", "orders", orderEvent);
return Results.Accepted();
});
await app.RunAsync();
public record OrderRequest(Guid OrderId, string CustomerId, List<string> Items);
public record OrderCreatedEvent(Guid OrderId, string CustomerId, List<string> Items, DateTime CreatedAt);
And here's how a subscriber would handle these events:
// Subscriber service
using Dapr;
using Microsoft.AspNetCore.OutputCaching;
var builder = WebApplication.CreateBuilder(args);
// Add Dapr event handling
builder.Services.AddDapr();
builder.Services.AddControllers();
var app = builder.Build();
// Required for Dapr pub/sub
app.UseCloudEvents();
app.MapSubscribeHandler();
// Subscribe to "orders" topic
app.MapPost("/events/orders", [Topic("order-events", "orders")] async (OrderCreatedEvent orderEvent) =>
{
Console.WriteLine($"Processing order {orderEvent.OrderId} for customer {orderEvent.CustomerId}");
await ProcessOrderAsync(orderEvent);
return Results.Ok();
});
await app.RunAsync();
async Task ProcessOrderAsync(OrderCreatedEvent orderEvent)
{
// Process the order
await Task.Delay(100); // Simulate work
}
public record OrderCreatedEvent(Guid OrderId, string CustomerId, List<string> Items, DateTime CreatedAt);
The key components:
- The publisher uses Dapr to send events to a topic
- Dapr handles the interaction with the message broker (Kafka, Redis, etc.)
- The subscriber decorates endpoints with
[Topic]
attributes - Dapr delivers the messages to the appropriate subscribers
Note that the name
defined in the component file (order-events
in our example) must match the first parameter used in
PublishEventAsync("order-events", ...)
and [Topic("order-events", ...)]
.
If these names don't match exactly, messages won't flow correctly between services.
Dapr and .NET Aspire: Better Together
Dapr works seamlessly with .NET Aspire, Microsoft's new cloud-ready stack for building distributed applications. While Aspire focuses on .NET-specific application orchestration, Dapr provides language-agnostic building blocks.
Here's how to integrate Dapr with a .NET Aspire application:
using CommunityToolkit.Aspire.Hosting.Dapr;
// Program.cs in the Aspire AppHost project
var builder = DistributedApplication.CreateBuilder(args);
// Add Aspire service and configure Dapr
var orderService = builder.AddProject<Projects.OrderService>("orderservice")
.WithDaprSidecar(new DaprSidecarOptions
{
AppId = "order-api",
Config = "./dapr/config.yaml",
ResourcesPaths = ["./dapr/components"]
});
// Add another service that can communicate with the order service via Dapr
var checkoutService = builder.AddProject<Projects.CheckoutService>("checkoutservice")
.WithDaprSidecar(new DaprSidecarOptions
{
AppId = "checkout-api",
Config = "./dapr/config.yaml",
ResourcesPaths = ["./dapr/components"]
})
// Reference the order service by its Dapr app ID
.WithReference(orderService);
builder.Build().Run();
Note that I'm using the CommunityToolkit.Aspire.Hosting.Dapr
package, which is the official Dapr integration for .NET Aspire.
The Aspire.Hosting.Dapr
library is now deprecated.
Here's an example of how a message flow might look in the Aspire dashboard:
Learning with Dapr University
If you're looking for a structured way to learn Dapr, I highly recommend checking out Dapr University. You can run the hands-on lessons completely for free.
As someone who started with limited Dapr experience, I found the "Dapr 101" course particularly valuable. It provides hands-on exercises for State Management, Service Invocation, and Pub/Sub—exactly what you need to get started quickly.
Conclusion
Dapr simplifies microservice development for .NET developers by providing standardized building blocks that handle infrastructure complexity. With its sidecar pattern, Dapr lets you focus on business logic while it manages cross-cutting concerns. As you build distributed applications, consider how Dapr can help you:
- Accelerate development with ready-made patterns
- Build more resilient systems with fewer lines of code
- Avoid vendor lock-in through abstraction (building blocks)
- Improve production reliability with built-in best practices
Ready to dive deeper? Check out Dapr University for comprehensive courses and hands-on learning.
That's all for today. Hope this was helpful.