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#.
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.
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 Dapr | How Dapr helps |
|---|---|
| Writing retry and timeout code for every call | Dapr adds retries and resilience for you |
| Locking your app to one specific database | Dapr lets you swap stores via a config file |
| Picking a message queue and learning its SDK | Dapr gives one pub/sub API for many brokers |
| Storing secrets safely | Dapr reads secrets from a secret store for you |
| Tracing requests across services | Dapr 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 block | What it does |
|---|---|
| Service invocation | Call another service safely with retries and discovery |
| State management | Save and read key-value data in a store |
| Publish and subscribe | Send and receive messages between services |
| Bindings | Trigger code from, or push data to, external systems |
| Secrets | Read passwords and keys from a secret store |
| Actors | Manage many tiny stateful objects |
| Workflow | Run long, multi-step business processes |
| Configuration | Read and watch app settings centrally |
How a Dapr call flows
Steps
Your C# code
You call a simple method
Dapr SDK
Turns it into a sidecar call
Local sidecar
Adds retries, talks to backend
Backend
Redis, queue, or service
Response
Result comes back to you
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:6379To 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.
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.
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
Steps
Caller app
Calls InvokeMethodAsync
Caller sidecar
Finds target, adds retries
Callee sidecar
Receives the secure call
Callee app
Runs the method, replies
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 filesYou 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.
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
SaveStateAsyncandGetStateAsync. - 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
- Dapr .NET SDK Docs
- Dapr for .NET Developers (Microsoft Learn)
- Dapr Overview (Official Docs)
- How-To: Invoke a service with Dapr
- Pub/Sub with ASP.NET Core (dapr/dotnet-sdk)
Related Posts
Getting Started With Dapr for Building Cloud-Native Microservices in .NET
A beginner-friendly guide to Dapr for .NET developers: learn sidecars, state, pub/sub, and service invocation to build cloud-native microservices.
.NET Aspire: A Game Changer for Cloud-Native Development
A beginner-friendly guide to .NET Aspire, the cloud-native stack that orchestrates your services, databases, and dashboards with one simple command.
Service Discovery in .NET Microservices with HashiCorp Consul
A beginner-friendly guide to service discovery in .NET microservices using HashiCorp Consul, with registration, health checks, and lookups explained simply.
How .NET Aspire Simplifies Service Discovery for Your Apps
Learn how .NET Aspire service discovery lets your services find each other by name, with no hardcoded URLs, ports, or environment headaches.
Structured Logging and Distributed Tracing for Microservices with Seq
Learn to add structured logging with Serilog and distributed tracing with OpenTelemetry to .NET microservices, then view it all in Seq with one trace ID.
How to Scale Long-Running API Requests in .NET: A Beginner's Guide
Learn how to handle slow, long-running API requests in .NET using the 202 Accepted pattern, background services, channels, and status polling.