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:

.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:

Why Combine .NET Aspire with Dapr?

4. Setting Up the Project

The repository is structured as follows:

Before running the project, ensure you have:

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?

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?

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?

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;
    }

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?

Use Case Example

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:

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:

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

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.