Table of Contents
- Introduction
- .NET Aspire Overview
- Dapr Overview
- Setting Up the Project
- Understanding Dapr Building Blocks
- Pub-Sub
- HTTP Invocation
- State Management
- Actor Model
- Overview of apphost.cs in .NET Aspire
- Advantages of Combining .NET Aspire and Dapr
- Conclusion
1. Introduction
Building scalable microservices comes with challenges like managing the 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 the App.Host project in the 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"
});
5.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 the 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);
5.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");
5.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 the 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.