Introduction to Dapr for .NET Developers

Introduction to Dapr for .NET Developers

10 min read ·

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:

Dapr sidecar diagram with the building blocks and services.
Source: Dapr

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:

  1. Service Invocation: Enables reliable service-to-service communication with automatic service discovery, load balancing, and retries.
  2. State Management: Provides a unified way to store and retrieve state with features like concurrency control and transactions.
  3. Pub/Sub: Implements asynchronous messaging between services, allowing for loosely-coupled, event-driven architectures.
  4. Workflows: Enables you to define long running, persistent processes that span multiple microservices.
  5. Bindings: Connects your applications to external systems, either for triggering your app from external events or invoking external services.
  6. Actors: Implements the virtual actor pattern, making it easy to build stateful microservices with encapsulated state and behavior.
  7. Secrets: Offers secure access to sensitive configuration like connection strings and API keys from various secret stores.
  8. Configuration: Centralizes application settings with support for dynamic updates across multiple services.
  9. Distributed Lock: Provides mutually exclusive access to shared resources in a distributed environment.
  10. Cryptography: Offers encryption and decryption operations while handling key management.
  11. Jobs: Allows you to schedule and orchestrate jobs (e.g., schedule batch processing jobs to run every business day)
  12. 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:

Dapr components diagram with the base building blocks and services.
Source: Dapr

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
Diagram showing how the service invocation flow looks like with Dapr.
Source: Dapr

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 calls InvokeMethodAsync using the DaprClient to send a request to the order 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
Diagram showing how the publish subscribe flow looks like with Dapr.
Source: Dapr

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/subscribing
  • spec.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:

A distributed trace from the Aspire dashboard showing a message flow from the checkout service to the order service.

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.

Dapr university learning platform.

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.


Whenever you're ready, there are 4 ways I can help you:

  1. (NEW) Pragmatic REST APIs: You will learn how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  2. Pragmatic Clean Architecture: Join 3,900+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 1,700+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Patreon Community: Join a community of 1,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

Become a Better .NET Software Engineer

Join 64,000+ engineers who are improving their skills every Saturday morning.