Skip to main content
SEMastery
DevOpsbeginner

Introduction to Dapr for .NET Developers: A Beginner Guide

A warm, beginner-friendly introduction to Dapr for .NET developers, covering sidecars, building blocks, state, pub/sub, and service invocation in plain C#.

13 min readUpdated March 31, 2026

When you build one small program that runs on one machine, life is simple. But modern apps are often made of many small programs called services. One service handles login. Another handles orders. Another sends email. These services must talk to each other over a network. And networks are messy. Messages get lost. One service is slow. A database connection drops. Every team ends up writing the same boring plumbing code to deal with this, over and over.

Dapr was built to stop that pain. In this guide you will meet Dapr, see why .NET developers enjoy it, and write some simple C# to use its main features. We will keep the words plain and the steps small, so a beginner can follow along.

A simple everyday analogy

Imagine you run a small shop. You are great at selling things, but you are not great at every other job. So you hire a smart office assistant who sits at a desk right next to you.

When you need to send a parcel, you do not learn how the courier company works. You just hand the parcel to your assistant and say, "Send this." When you need to look up a customer's phone number, you ask the assistant, and they fetch it from the filing cabinet. When a message arrives for you, the assistant brings it to your desk. You never touch the courier, the cabinet, or the postman directly.

Your assistant speaks all the outside languages so you do not have to. You focus on selling. The assistant handles the messy outside world.

Dapr is that assistant for your app. It sits right next to your service as a small helper. Your code asks the helper to save data, send a message, or call another service. The helper deals with the actual database, the actual message queue, and the actual network.

What is Dapr, really?

Dapr stands for Distributed Application Runtime. It is free and open source, and it is a graduated project in the Cloud Native Computing Foundation (CNCF). That means it is mature and trusted by many companies.

The key idea is the sidecar. A sidecar is a separate small process that runs beside your app. Your app and the Dapr sidecar are two programs, but they live together like a motorbike and its side-car. Your app talks to the sidecar using plain HTTP or gRPC over localhost. The sidecar then does the heavy lifting.

Your app talks only to its local Dapr sidecar, never directly to the infrastructure.

Because your app only ever talks to the local sidecar, your code does not care what database or message queue is behind it. That detail lives in a small configuration file. Swap the file, and the same code works with a different backend.

Why .NET developers like Dapr

Here are the main reasons Dapr feels good in a .NET project.

Problem without DaprHow Dapr helps
Writing retry and timeout code for every callDapr adds retries and resilience for you
Locking your app to one specific databaseDapr lets you swap stores via a config file
Picking a message queue and learning its SDKDapr gives one pub/sub API for many brokers
Storing secrets safelyDapr reads secrets from a secret store for you
Tracing requests across servicesDapr emits traces and metrics automatically

Another nice point about licensing. Some popular .NET libraries, such as MediatR and MassTransit, have recently moved to commercial (paid) licenses for many uses. Dapr is different. It is open source and free under the Apache 2.0 license, so there is no licence fee to worry about.

The building blocks

Dapr groups its features into building blocks. A building block is one focused job, exposed through a simple API. You only use the ones you need. The most common ones are listed below.

Building blockWhat it does
Service invocationCall another service safely with retries and discovery
State managementSave and read key-value data in a store
Publish and subscribeSend and receive messages between services
BindingsTrigger code from, or push data to, external systems
SecretsRead passwords and keys from a secret store
ActorsManage many tiny stateful objects
WorkflowRun long, multi-step business processes
ConfigurationRead and watch app settings centrally

How a Dapr call flows

Your C# code
Dapr SDK
Local sidecar
Backend
Response

Steps

1

Your C# code

You call a simple method

2

Dapr SDK

Turns it into a sidecar call

3

Local sidecar

Adds retries, talks to backend

4

Backend

Redis, queue, or service

5

Response

Result comes back to you

A request from your code travels through the sidecar to the real backend and back.

Let us now look at three building blocks with real C# code: state, pub/sub, and service invocation.

Setting up the Dapr .NET SDK

First, install the Dapr CLI and run dapr init once on your machine. That sets up the local pieces Dapr needs. Then, in your ASP.NET Core project, add the SDK package.

// Run in your project folder:
//   dotnet add package Dapr.AspNetCore
 
var builder = WebApplication.CreateBuilder(args);
 
// Register the Dapr client so we can inject it anywhere.
builder.Services.AddDaprClient();
builder.Services.AddControllers().AddDapr();
 
var app = builder.Build();
 
app.MapControllers();
app.Run();

The DaprClient is your main door into all the building blocks. You inject it into a controller or service, and call simple methods on it. You never open a database connection or a queue connection yourself.

Building block 1: State management

State management lets you save and read data as simple key-value pairs. Behind the scenes the data might live in Redis, Azure Cosmos DB, PostgreSQL, or many other stores. Your code does not care which one.

Imagine a shopping cart. We want to save it and read it back later.

[ApiController]
[Route("cart")]
public class CartController : ControllerBase
{
    private const string StoreName = "statestore";
    private readonly DaprClient _dapr;
 
    public CartController(DaprClient dapr) => _dapr = dapr;
 
    // Save a cart for a user.
    [HttpPost("{userId}")]
    public async Task<IActionResult> Save(string userId, Cart cart)
    {
        await _dapr.SaveStateAsync(StoreName, userId, cart);
        return Ok();
    }
 
    // Read a cart back.
    [HttpGet("{userId}")]
    public async Task<Cart?> Get(string userId)
        => await _dapr.GetStateAsync<Cart>(StoreName, userId);
}
 
public record Cart(string[] Items, decimal Total);

Notice the route uses cart/{userId}. In prose we always wrap such routes in backticks, like POST /cart/{userId}, so the page renders correctly. The string "statestore" is the name of a component, not a database brand. The brand is decided in a YAML file:

// File: components/statestore.yaml  (this is YAML, shown here as a code block)
//
// apiVersion: dapr.io/v1alpha1
// kind: Component
// metadata:
//   name: statestore
// spec:
//   type: state.redis
//   version: v1
//   metadata:
//     - name: redisHost
//       value: localhost:6379

To use a different store, you change type: state.redis to something else and update the settings. Your C# does not change at all. That is the portability promise in action.

The same SaveStateAsync call can point at very different stores.

Building block 2: Publish and subscribe

Pub/sub lets one service send a message without knowing who will read it. Other services subscribe to a topic and get the message. This is great for things like "an order was placed" where many services may care.

Here is the publisher. It sends an OrderPlaced message to a topic.

[ApiController]
[Route("orders")]
public class OrderController : ControllerBase
{
    private const string PubSubName = "pubsub";
    private readonly DaprClient _dapr;
 
    public OrderController(DaprClient dapr) => _dapr = dapr;
 
    [HttpPost]
    public async Task<IActionResult> Place(Order order)
    {
        // Publish to the "orders" topic. We do not know or care who reads it.
        await _dapr.PublishEventAsync(PubSubName, "orders", order);
        return Accepted();
    }
}
 
public record Order(string Id, string Customer, decimal Amount);

And here is a subscriber in a different service. Dapr calls this endpoint whenever a new message lands on the topic.

[ApiController]
public class EmailController : ControllerBase
{
    // This attribute tells Dapr: send "orders" topic messages here.
    [Topic("pubsub", "orders")]
    [HttpPost("/on-order")]
    public IActionResult OnOrder(Order order)
    {
        // Pretend we send a confirmation email here.
        Console.WriteLine($"Emailing {order.Customer} about order {order.Id}");
        return Ok();
    }
}

When the subscriber app starts, its sidecar asks the app which topics it wants. Then it routes matching messages to the right method. You wrote no queue code at all.

One publisher, two independent subscribers, all decoupled by a topic.

Because the publisher does not know its subscribers, you can add a new listener (say, an analytics service) later without touching the order service. That loose coupling keeps a system easy to grow.

Building block 3: Service invocation

Sometimes one service must call another and wait for an answer. For example, the order service may ask the inventory service, "Do we have this item?" Service invocation handles finding the other service, retrying on failure, and securing the call.

public class OrderProcessor
{
    private readonly DaprClient _dapr;
 
    public OrderProcessor(DaprClient dapr) => _dapr = dapr;
 
    public async Task<bool> CanFulfill(string sku)
    {
        // Call the "inventory" app's GET endpoint and read a typed result.
        var stock = await _dapr.InvokeMethodAsync<StockInfo>(
            HttpMethod.Get,
            "inventory",          // the other app's Dapr id
            $"stock/{sku}");      // its route, like GET /stock/{sku}
 
        return stock.Available > 0;
    }
}
 
public record StockInfo(string Sku, int Available);

You pass the app id of the target service, not a URL with a host and port. Dapr finds the right instance for you. If the inventory service has moved or scaled to many copies, your code stays the same. This is service discovery and resilience, handed to you for free.

Service invocation steps

Caller app
Caller sidecar
Callee sidecar
Callee app

Steps

1

Caller app

Calls InvokeMethodAsync

2

Caller sidecar

Finds target, adds retries

3

Callee sidecar

Receives the secure call

4

Callee app

Runs the method, replies

How one service reaches another through both sidecars.

Running it locally

On your laptop, you start your app together with its sidecar using the Dapr CLI. The command looks like this.

// Start one service with a Dapr sidecar next to it:
//   dapr run --app-id orders --app-port 5001 --resources-path ./components -- dotnet run
//
// --app-id        the name other services use to reach this one
// --app-port      the port your ASP.NET Core app listens on
// --resources-path  the folder with your component YAML files

You run a similar command for each service. Each one gets its own sidecar. When you are ready for production, the same app and components run on Kubernetes, where Dapr injects the sidecar for you automatically. Your code does not change between laptop and cluster.

A quick word on observability

Because every call passes through a sidecar, Dapr can see the whole journey of a request. It produces distributed traces and metrics without you adding much code. You can send these to tools like Zipkin, Jaeger, or Prometheus and watch how a request hops from service to service. For a beginner, the win is simple: you get useful insight into a tricky distributed system almost for free.

Secrets, the safe way

Hard-coding a password or an API key in your code is risky. If that code leaks, the secret leaks too. Dapr offers a secrets building block so your app reads sensitive values from a proper secret store, such as Azure Key Vault, HashiCorp Vault, or even a local file during development.

public class PaymentService
{
    private readonly DaprClient _dapr;
 
    public PaymentService(DaprClient dapr) => _dapr = dapr;
 
    public async Task<string> GetApiKeyAsync()
    {
        // Ask Dapr for a secret named "stripe-key" from the "secretstore".
        var secret = await _dapr.GetSecretAsync("secretstore", "stripe-key");
        return secret["stripe-key"];
    }
}

Your code never holds the raw password in source control. It asks the sidecar at runtime, and the sidecar fetches it from the trusted store. As with state, swapping the secret store is a config change, not a code change.

How the pieces fit together

It can help to picture a small system. Below, an order service, an inventory service, and an email service each run with their own Dapr sidecar. They share a state store and a message broker, but no service talks to that infrastructure directly.

A small Dapr system where every service has its own sidecar.

This shape stays the same as the system grows. Add a new service, give it a sidecar, and it can join the conversation right away. The infrastructure stays hidden behind the sidecars.

When should you use Dapr?

Dapr shines when you have several services that need to talk, store data, and send messages. It is also a kind teacher for distributed patterns, because it nudges you toward loose coupling and resilience.

You may not need Dapr for a single small app with no other services. In that case the sidecar is extra weight you do not use. As always, pick the tool that fits the job. Start small, add building blocks only as the need appears.

Quick recap

  • Dapr means Distributed Application Runtime. It is free, open source, and a graduated CNCF project.
  • It runs as a sidecar, a small helper process beside your app, like a smart office assistant.
  • Your code talks only to the local sidecar, so the real database or queue is swappable via a YAML component file.
  • Building blocks are focused features: state, pub/sub, service invocation, bindings, secrets, actors, workflow, and configuration.
  • State management saves and reads key-value data with SaveStateAsync and GetStateAsync.
  • Pub/sub lets a publisher send to a topic while subscribers receive, fully decoupled.
  • Service invocation calls another service by app id, with discovery and retries built in.
  • You keep writing normal C# and ASP.NET Core. Dapr handles the hard distributed plumbing.

References and further reading

Related Posts