Integrating .NET Aspire with Dapr for Cloud-Native Apps

Table of Contents

  1. Introduction
  2. .NET Aspire Overview
  3. Dapr Overview
  4. Setting Up the Project
  5. Understanding Dapr Building Blocks
    • Pub-Sub
    • HTTP Invocation
    • State Management
    • Actor Model
  6. Overview of apphost.cs in .NET Aspire
  7. Advantages of Combining .NET Aspire and Dapr
  8. 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

  1. A publisher sends a message to a topic.
  2. Dapr routes the message to all subscribed services.
  3. 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?

  1. A service calls another via Dapr’s service invocation API.
  2. Dapr resolves the service name dynamically and forwards the request.
  3. 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

  1. A service stores data using Dapr’s state management API.
  2. Dapr persists the data in the configured state store.
  3. 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

  1. Redis for caching.
  2. Dapr State Store for persistence.
  3. Dapr Pub-Sub for messaging.
  4. Dapr Service Invocation for secure API calls.
  5. Dapr Actors (optional) for stateful workflows.
  6. 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.

Up Next
    Ebook Download
    View all
    Learn
    View all