Table of Contents
- Introduction
- .NET Aspire Overview
- Dapr Overview
- Setting Up the Project
- Understanding Dapr Building Blocks
- Overview of
apphost.csin .NET Aspire - Advantages of Combining .NET Aspire and Dapr
- Conclusion
1. Introduction
Building scalable microservices comes with challenges like managing state, handling inter-service communication, and ensuring resilience. This article explores how .NET Aspire and Dapr simplify these concerns by providing a seamless integration of cloud-native features.
Source code here
2. .NET Aspire Overview
.NET Aspire is a cloud-native development stack that helps developers build distributed applications with built-in observability, service discovery, and lifecycle management. It provides:
- Service Composition – Easily compose microservices with service defaults.
- Observability – Native support for logs, metrics, and traces.
- Integrated Hosting – Applications can run locally with cloud-compatible configurations.
.NET Aspire is particularly useful for microservices that need to scale efficiently in Kubernetes or cloud-based environments.
3. Dapr Overview
Dapr (Distributed Application Runtime) is an open-source, event-driven runtime designed for microservices. It provides:
- Building Blocks for Microservices, including:
- Pub-Sub Messaging – Asynchronous event-driven architecture.
- Service Invocation – Secure service-to-service communication.
- State Management – Store data across microservices.
- Actor Model – Encapsulate logic in stateful, distributed actors.
- Platform Agnostic – Can run on Kubernetes, VMs, or even locally.
- Cloud and Language Agnostic – Works with any cloud provider and supports multiple programming languages.
Why Combine .NET Aspire with Dapr?
- .NET Aspire focuses on simplifying cloud-native application development.
- Dapr focuses on simplifying microservices communication and state management.
- Together, they provide a powerful and scalable foundation for microservices.
4. Setting Up the Project
The repository is structured as follows:
- AspireAndDaprClientVerify – Publishes messages to the Pub-Sub topic.
- AspireAndDaprVerify.AppHost – Contains API endpoints and subscriber logic.
- AspireAndDaprVerify.ServiceDefaults – Defines reusable configurations.
Before running the project, ensure you have:
- .NET 8 or later installed
- Dapr CLI installed (
dapr --version) - Docker running (for local Dapr components)
5. Understanding Dapr Building Blocks
5.1. Pub-Sub Messaging – Asynchronous Event-Driven Architecture
The Publish-Subscribe (Pub-Sub) pattern enables event-driven communication between microservices. It decouples services, allowing them to communicate asynchronously without knowing each other’s details.
Why Use Pub-Sub?
- Loose Coupling – Publishers and subscribers don’t need to be aware of each other.
- Scalability – Events can be consumed by multiple subscribers.
- Resilience – If a service is down, it can process events later when it recovers.
Dapr Pub-Sub Flow
- A publisher sends a message to a topic.
- Dapr routes the message to all subscribed services.
- Subscribers process the message asynchronously.
Dapr Pub-Sub Implementation
Publishing a Message
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
private readonly DaprClient _daprClient;
public WeatherForecastController(ILogger<WeatherForecastController> logger, DaprClient daprClient)
{
_logger = logger;
_daprClient = daprClient;
}
[HttpGet(Name = "GetWeatherForecast")]
public async Task<IEnumerable<WeatherForecast>> Get()
{
var result= await _daprClient.InvokeMethodAsync<IEnumerable< WeatherForecast>>(httpMethod: HttpMethod.Get, "aspireanddaprverify", "weatherforecast");
await _daprClient.PublishEventAsync("servicebus-pubsub", "ganeshmahadev", result.First());
return result;
}
}
Subscribing to a Topic
[Route("subscribe")]
[ApiController]
public class SubscribeController : ControllerBase
{
[HttpPost]
[Topic("azure-servicebus-subscription", "ganeshmahadev")]
public IActionResult SubscribeToQueue([FromBody] WeatherForecast message)
{
// Process the message
Console.WriteLine($"Received message: {message}");
return Ok();
}
}
The subscriber listens for messages without needing to know the publisher’s details.
Pub-Sub Component Configuration (azure-servicebus-subscription.yaml)
apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
name: azure-servicebus-subscription
spec:
topic: ganeshmahadev
route: /subscribe # Route defined in the controller
pubsubname: servicebus-pubsub
servicebus-pubsub.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: servicebus-pubsub
namespace: default
spec:
type: pubsub.azure.servicebus
version: v1
metadata:
- name: connectionString
value: ""
Register your dependencies in App.Host project in program.cs
var serviceBus = builder.AddDaprPubSub("servicebus-pubsub", new DaprComponentOptions
{
LocalPath = "component/servicebus-pubsub.Yaml"
});
var sb1 = builder.AddDaprPubSub("azure-servicebus-subscription", new DaprComponentOptions
{
LocalPath = "component/azure-servicebus-subscription.yaml"
});
2. Service Invocation – Secure Service-to-Service Communication
Service-to-service communication is crucial in microservices, but managing service discovery, retries, and load balancing can be complex.
Why Use Dapr Service Invocation?
- No Hardcoded URLs – Services invoke each other by name.
- Automatic Retries & Load Balancing – Handles transient failures.
- Secure Communication – Supports mTLS for encrypted communication.
How Service Invocation Works?
- A service calls another via Dapr’s service invocation API.
- Dapr resolves the service name dynamically and forwards the request.
- The target service processes the request and sends a response.
Calling a Service Using Dapr
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
private readonly DaprClient _daprClient;
public WeatherForecastController(ILogger<WeatherForecastController> logger, DaprClient daprClient)
{
_logger = logger;
_daprClient = daprClient;
}
[HttpGet(Name = "GetWeatherForecast")]
public async Task<IEnumerable<WeatherForecast>> Get()
{
var result= await _daprClient.InvokeMethodAsync<IEnumerable< WeatherForecast>>(httpMethod: HttpMethod.Get, "aspireanddaprverify", "weatherforecast");
await _daprClient.PublishEventAsync("servicebus-pubsub", "ganeshmahadev", result.First());
return result;
}
}
This service automatically receives requests via Dapr.
Register your dependencies in App.Host Project
var appservice = builder.AddProject<Projects.AspireAndDaprVerify>("aspireanddaprverify")
.WithExternalHttpEndpoints()
.WithDaprSidecar(sidecar =>
{
sidecar.WithOptions(new DaprSidecarOptions
{
AppId = "aspireanddaprverify",
AppPort = 5281,
DaprHttpPort = 3502,
DaprGrpcPort = 50001,
});
}).WithReference(redis).WaitFor(redis).WithReference(stateStore).WithReference(sb1);
builder.AddDapr(x =>
{
x.EnableTelemetry = true;
});
builder.AddProject<Projects.AspireAndDaprClientVerify>("aspireanddaprclientverify")
.WithExternalHttpEndpoints()
.WithDaprSidecar(sidecar =>
{
sidecar.WithOptions(new DaprSidecarOptions
{
AppId = "aspireanddaprclientverify",
AppPort = 5027,
DaprHttpPort = 3501,
DaprGrpcPort = 50002,
});
}).WithReference(appservice).WaitFor(appservice)
.WithReference(serviceBus);
3. State Management – Store Data Across Microservices
Dapr provides stateful microservices without requiring complex databases. It supports various state stores like Redis, PostgreSQL, CosmosDB, and DynamoDB.
Why Use Dapr State Management?
- Built-in State Persistence – No need for external databases for transient state.
- Scalable and Resilient – Stores data across multiple microservices.
- Easy to Integrate – Works with any supported backend.
Dapr State Management Workflow
- A service stores data using Dapr’s state management API.
- Dapr persists the data in the configured state store.
- Any microservice can later retrieve or update the stored data.
Saving State
[HttpGet(Name = "GetWeatherForecast")]
public async Task<IEnumerable<WeatherForecast>> Get()
{
var weather = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
});
await _daprClient.SaveStateAsync("statestore", "weather1", weather);
return weather;
}
"statestore"is the configured Dapr state store component.-
"weather1"is the state key. - The value is persisted across restarts.
Retrieving State
var value = await _daprClient.GetStateAsync<IEnumerable<WeatherForecast>>("statestore", "weather1");
Register your dependencies in App.Host project
var redis = builder.AddRedis("cache"); // This line should work now
var stateStore = builder.AddDaprStateStore("statestore");
4. Actor Model – Encapsulate Logic in Stateful, Distributed Actors
Dapr implements actors, a concurrency model where each actor is isolated and manages its own state.
Why Use Dapr Actors?
- Concurrency & Isolation – Each actor instance runs independently.
- Stateful Processing – Each actor remembers its state across calls.
- Automatic Garbage Collection – Dapr manages idle actors.
Use Case Example
- Managing shopping carts for each user.
- Processing IoT device updates.
- Storing long-running workflows.
Defining an Actor Interface
public interface ISampleActor:IActor
{
Task<IEnumerable<WeatherForecast>> GetWeatherForecast();
}
Actors expose methods just like microservices.
Implementing an Actor
public class SampleActor : Actor, ISampleActor
{
private readonly DaprClient _daprClient;
public SampleActor(ActorHost host,DaprClient daprClient) : base(host)
{
_daprClient = daprClient;
}
public async Task<IEnumerable<WeatherForecast>> GetWeatherForecast()
{
var weatherForecast = await _daprClient.
GetStateAsync<IEnumerable<WeatherForecast>>("statestore", "weather1");
if (weatherForecast != null) return weatherForecast;
return default;
}
}
Each actor instance operates independently.
Invoking an Actor
[Route("api/[controller]")]
[ApiController]
public class ActorController : ControllerBase
{
private readonly ActorProxyFactory actorProxy;
public ActorController(ActorProxyFactory actorProxy)
{
this.actorProxy = actorProxy;
}
[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
var actor=actorProxy.
CreateActorProxy<ISampleActor>(new ActorId(Guid.NewGuid().ToString()), "SampleActor");
var weatherforecast=await actor.GetWeatherForecast();
return weatherforecast;
}
}
Dapr ensures each actor instance is isolated and stateful.
Register your dependencies in calling application
builder.Services.AddActors(options =>
{
options.Actors.RegisterActor<SampleActor>();
});
builder.Services.AddSingleton<ActorProxyFactory>();
Overview of apphost.cs
The apphost.cs file is the entry point of the Aspire + Dapr application.
It defines:
- Redis for caching.
- Dapr State Store for persistence.
- Dapr Pub-Sub for messaging.
- Dapr Service Invocation for secure API calls.
- Dapr Actors (optional) for stateful workflows.
- Telemetry for monitoring.
5. Platform Agnostic – Kubernetes, VMs, or Locally
Dapr can run anywhere:
- On-Premise – Deploy on VMs or bare-metal servers.
- Kubernetes – Seamless scaling and cloud-native orchestration.
- Local Development – Run Dapr services locally using Docker.
Example: Running Locally
dapr run --app-id myapp --app-port 5000 -- dotnet run
Dapr runs as a sidecar, intercepting requests and handling state management.
6. Cloud and Language Agnostic
Dapr is not tied to any cloud or programming language:
- Cloud Providers: Works on Azure, AWS, GCP, or private clouds.
- Languages Supported:
- .NET
- Java
- Python
- Go
- Node.js
- Rust
7. Advantages of Combining .NET Aspire and Dapr
| Feature | .NET Aspire | Dapr |
|---|---|---|
| Service Discovery | ✅ Built-in | ✅ Sidecar-based |
| State Management | ❌ Not provided | ✅ Supports Redis, PostgreSQL, etc. |
| Pub-Sub Messaging | ❌ Not provided | ✅ Supports multiple brokers (Kafka, Azure Service Bus, RabbitMQ) |
| Actor Model | ❌ Not provided | ✅ Virtual Actors for stateful workflows |
| Cloud Agnostic | ✅ Works on Azure | ✅ Works on any cloud |
| Observability | ✅ Built-in | ✅ Distributed Tracing |
Key Benefits
- Simplifies microservices development.
- Enhances resilience and scalability.
- Supports hybrid and multi-cloud architectures.
- Decouples microservices communication via Pub-Sub and Actors.
Conclusion
Dapr provides a powerful, cloud-native abstraction layer for microservices.
By using Pub-Sub, Service Invocation, State Management, and Actors, developers can build scalable, resilient, and platform-independent applications.