DEV Community

Cover image for Getting Started With Dapr for Building Cloud-Native Microservices in .NET
Anton Martyniuk
Anton Martyniuk

Posted on Originally published at antondevtips.com

Getting Started With Dapr for Building Cloud-Native Microservices in .NET

Building microservices is hard.

Microservices don't just come with scalability, they come with mandatory complexity you pay for upfront and forever.

You need to handle service-to-service communication, message brokers, state management, secrets, and more.
Each of these concerns requires its own library, configuration, and expertise.

What if there were a runtime that provided all these capabilities out of the box, regardless of the infrastructure you use?

That is exactly what Dapr does.

Dapr (Distributed Application Runtime) is a portable, event-driven runtime that makes it easier to build resilient microservices.
It provides a set of building block APIs for common distributed systems challenges: pub/sub messaging, service invocation, state management, secrets, and more.

The best part? Dapr is infrastructure-agnostic.
You can swap RabbitMQ for Kafka, or Redis for PostgreSQL, without changing a single line of application code.

Dapr is a free and open-source project; you can see its GitHub repo here.

And when you combine Dapr with .NET Aspire, you get a powerful local development experience where containers, sidecars, and services are orchestrated for you with a single command.

In this post, we will explore:

  • What is Dapr and How It Simplifies Microservice Development?
  • Understanding Dapr Building Blocks
  • How Dapr Components Work
  • How to set up Dapr with .NET Aspire
  • How to use Dapr to Publish Events to a Message Queue with Pub/Sub
  • How to use Dapr to Consume Events from a Message Queue
  • How to use Dapr to Call REST Services with Service Invocation
  • How to use Dapr to Manage State with State Management

Let's dive in.


👉 Read original article on my newsletter: https://antondevtips.com/blog/getting-started-with-dapr-for-building-cloud-native-microservices-in-dotnet

What is Dapr and How It Simplifies Microservice Development?

When you build microservices, you quickly face a set of recurring challenges:

  • How do services discover and call each other reliably?
  • How do you publish and consume events across services?
  • How do you store state without coupling to a specific database?
  • How do you manage secrets securely?
  • How do you handle retries, timeouts, and circuit breakers?

Without Dapr, you solve each problem independently.
You install a RabbitMQ client library for messaging, a Redis library for caching, a Vault client for secrets, and write custom retry logic.
Each library has its own API, configuration, and behavior.

You can use messaging libraries such as MassTransit, Wolverine, Rebus or Brighter, but they solve only the async messaging flows.
You still need to implement caching, secrets, synchronous communication, service discovery, and others.

Dapr solves all of these by providing a unified set of APIs that abstract these concerns.
Your application talks to Dapr, and Dapr talks to the infrastructure.

If you need to switch from RabbitMQ to Kafka for messaging, you change a configuration file (in production).
Your application code stays exactly the same.

How the Sidecar Architecture Works

Dapr runs as a sidecar alongside your application.
This means Dapr is a separate process (or container) that runs next to your service.

Your application communicates with the Dapr sidecar over HTTP or gRPC on localhost.
The sidecar handles the actual interaction with external infrastructure: message brokers, databases, secret stores, and other services.

Here is how it works:

Screenshot_3

In a Kubernetes environment, Dapr runs as a companion container within the same pod as your application.
In local development, it runs as a separate process on your machine.

This sidecar architecture provides several benefits:

  1. Language agnostic: Dapr works with any language that can make HTTP or gRPC calls. Whether you write in C#, Go, Python, or Java, you use the same Dapr APIs.

  2. No SDK lock-in: While Dapr provides SDKs for popular languages (including .NET), you can always fall back to raw HTTP calls.

  3. Independent lifecycle: Your application and the Dapr sidecar can be updated independently. Upgrading Dapr does not require recompiling your application.

  4. Consistent behavior: Retry policies, timeouts, and circuit breakers are configured at the sidecar level. Every service in your system automatically receives the same resilience guarantees.

Each service in your system has its own Dapr sidecar.
When Service A wants to call Service B, it tells its local Dapr sidecar.
The sidecar handles service discovery, routes the request to Service B's sidecar, which then forwards it to Service B.

Screenshot_4

This means your services never communicate directly with each other.
All communication goes through the Dapr sidecars, which gives you observability, security, and resilience for free.

What about an overhead when talking to the sidecar on each request?
This overhead is minimal, as all requests are done on the localhost.

And the benefits you get outpass this small disadvantage.

Understanding Dapr Building Blocks

Dapr organizes its capabilities into building blocks.
Each building block is an independent API that addresses a specific challenge in distributed systems.

Screenshot_1

You can use any combination of building blocks.

Here are the main building blocks:

Service Invocation

Enables services to call each other reliably.
Dapr handles service discovery, retries, and distributed tracing automatically.

Instead of hardcoding URLs or using a service registry, you reference services by their App ID:

// Call the "hotels-api" service via Dapr
var rooms = await daprClient.InvokeMethodAsync<RoomAvailabilityResponse>(
    HttpMethod.Get,
    "hotels-api",
    "hotels/1/rooms/available");
Enter fullscreen mode Exit fullscreen mode

Dapr resolves the address of hotels-api service, routes the request through the sidecar network, and returns the response.
If the target service is temporarily unavailable, Dapr can retry the request based on your configured policies.

Publish and Subscribe

Allows services to communicate asynchronously through queues and topics.
Publishers send events to a topic, and subscribers receive them.

The pub/sub building block supports at-least-once delivery guarantees.
It works with any supported message broker: RabbitMQ, Kafka, Azure Service Bus, Redis Streams, and more.

await daprClient.PublishEventAsync("pubsub", "booking-created", bookingEvent);
Enter fullscreen mode Exit fullscreen mode

Subscribers receive events through HTTP endpoints that Dapr calls automatically:

app.MapPost("/dapr/booking-created",
    [Topic("pubsub", "booking-created")]
    async (BookingCreatedEvent message) =>
    {
        // Process the event
        return Results.Ok();
    });
Enter fullscreen mode Exit fullscreen mode

This model is fundamentally different from traditional background service consumers.
Dapper Sidecar subscribes to a message queue, receives your message and pushes it to your application via HTTP POST requests.

State Management

Provides key/value state storage with pluggable backends.
You can use Redis, PostgreSQL, MongoDB, Azure Cosmos DB, or any other supported state store.

// Save state
await daprClient.SaveStateAsync("statestore", "user-123", userData);

// Get state
var user = await daprClient.GetStateAsync<UserData>("statestore", "user-123");

// Delete state
await daprClient.DeleteStateAsync("statestore", "user-123");
Enter fullscreen mode Exit fullscreen mode

The state management building block also supports:

  • Concurrency control with ETags for optimistic locking
  • TTL (Time-to-Live) for automatic expiration
  • Transactions for atomic operations across multiple keys

Secrets Management

Retrieves secrets from various secret stores without hardcoding credentials in your application.
Supported stores include Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, Kubernetes secrets, and local file-based stores.

var secrets = await daprClient.GetSecretAsync("secretstore", "payment-gateway");
var apiKey = secrets["api-key"];
Enter fullscreen mode Exit fullscreen mode

Bindings

Creates connections to external systems.
Input bindings trigger your application when an external event occurs.
Output bindings let your application invoke external systems.

For example, you can use an output binding to write files to local storage, send emails, or trigger a cloud function:

await daprClient.InvokeBindingAsync("email-output", "create", emailContent);
Enter fullscreen mode Exit fullscreen mode

Distributed Lock

Provides mutual exclusion for scenarios where multiple service instances might access the same resource concurrently:

var lockResponse = await daprClient.Lock(
    "statestore", "room-lock:101", "owner-id", expiryInSeconds: 30);

if (lockResponse.Success)
{
    // Exclusive access to the resource
    await daprClient.Unlock("statestore", "room-lock:101", "owner-id");
}
Enter fullscreen mode Exit fullscreen mode

Other Building Blocks

Dapr also provides building blocks for:

  • Workflows: Long-running, persistent processes that span multiple services
  • Actors: Virtual actor model for stateful computations with single-threaded execution
  • Configuration: Dynamic application configuration retrieval and subscription
  • Cryptography: Encryption and decryption operations
  • Jobs: Scheduled and triggered batch processing

👉 Read original article on my newsletter: https://antondevtips.com/blog/getting-started-with-dapr-for-building-cloud-native-microservices-in-dotnet

Top comments (0)