Microservices architecture has revolutionized modern application development, offering unparalleled scalability, modularity, and ease of deployment. Yet, setting up a local development environment for microservices often feels overwhelming. Enter .NET Aspire—a game-changer for building .NET cloud-native apps.
.NET Aspire simplifies local development with a consistent, opinionated set of tools and patterns tailored for distributed applications. Key features include:
- Orchestration: Seamlessly run and connect multi-project applications and dependencies locally.
- Integrations: Prebuilt NuGet packages for common services like Redis and Postgres, ensuring consistent, hassle-free connectivity.
- Tooling: Streamlined project templates and tools for Visual Studio, Visual Studio Code, and the .NET CLI.
In this post, we’ll explore how .NET Aspire's orchestration empowers you to develop, test, and debug cloud-native apps with ease. While not a replacement for production systems like Kubernetes, it abstracts complexities like service discovery, environment variables, and container configurations—letting you focus on coding, not setup.
The code sample used for this article is available in this Git Repository
While Aspire Integrations provide opinionated NuGet packages for commonly used services with built-in resiliency and retry logic, developers still face the challenge of understanding and correctly implementing the necessary SDKs for databases, queues, and other components. This often involves significant manual effort, especially for smaller teams juggling multiple responsibilities. Here's a glimpse of Aspire's AppHost in action:
// Create a distributed application builder given the command line arguments.
var builder = DistributedApplication.CreateBuilder(args);
// Add a Redis server to the application.
var cache = builder.AddRedis("cache");
// Add a Service Bus client to the application.
var servicebus = builder.AddAzureServiceBus("servicebus");
// Microservices
builder.AddProject<Projects.MyMicroservice1>("mymicroservice1")
.WithReference(cache)
.WithReference(servicebus)
.WaitFor(cache);
As projects scale and teams grow, the learning curve and setup effort can become a bottleneck. This is where Dapr, the Distributed Application Runtime, steps in to transform the microservices landscape.
Dapr is an open-source, platform-agnostic runtime designed to simplify the development of resilient, event-driven applications. By leveraging a sidecar architecture, Dapr provides a unified API for state management, service invocation, pub/sub messaging, and more. This enables seamless infrastructure transitions—for example, switching from MongoDB to Postgres or RabbitMQ to Azure Service Bus—without altering application code.
In this blog, we’ll explore how combining Dapr's flexibility with Aspire’s orchestration can streamline microservice development, empowering teams to focus on innovation rather than infrastructure.
When developing microservices with Dapr locally, debugging in Visual Studio traditionally requires Docker Desktop, as Visual Studio's container tools only support Docker. This dependency brings challenges, including licensing costs and the need for extensive configuration. To debug Dapr microservices with Docker Desktop, developers must write YAML files for Dapr components and a docker-compose file to orchestrate microservices, sidecars, and dependencies—a tedious process.
.NET Aspire eliminates the dependency on Docker Desktop by supporting both Docker and Podman container runtimes. With Aspire, you can enjoy a YAML-free local development experience, letting you focus on building and debugging your services without worrying about complex infrastructure setup.
Setting Up Your Development Environment
Getting started with .NET Aspire is simple. Follow these steps to set up your development environment:
1. Install .NET Aspire SDK
- To work with .NET Aspire, you need the following installed locally .NET 8.0 or .NET 9.0.
- Visual Studio 2022 17.9 or higher includes the latest .NET Aspire SDK by default when you install the Web & Cloud workload.
2. Install Podman Desktop
Podman is an open-source, daemonless alternative to Docker for building and running OCI containers.
If both Docker and Podman are installed on your system, .NET Aspire defaults to Docker. To switch to Podman, set the container runtime with the following command:
[System.Environment]::SetEnvironmentVariable("DOTNET_ASPIRE_CONTAINER_RUNTIME", "podman", "User")
3. Install Dapr CLI
To run applications with Dapr sidecars in .NET Aspire, install the Dapr CLI (version 1.13 or later).
- Follow the instructions here: Install the Dapr CLI.
- After installation, initialize Dapr for Podman with:
dapr init --container-runtime podman
For more details, see Initialize Dapr with Podman.
.NET Aspire + Dapr: Microservice Application Example
To showcase the potential of combining .NET Aspire and Dapr, I’ve built a simple microservice application that highlights how Aspire orchestration simplifies local development. You can find the complete source code for this demo in my GitHub Repository.
This demo consists of three projects:
- Service-A: A minimal API service that publishes messages and do service-to-service invocations.
- Service-B: Another minimal API service that subscribes to messages and retrieves secrets from the Dapr secret store and interacts with a state store.
- Blazor Frontend: A web application that interacts with backend services.
To create the application with .NET Aspire support, create a .NET Aspire Empty App in the visual studio or use the following command in the CLI
dotnet new aspire --name AspireWithDapr
. This will generate a solution with two projects:
- Aspire AppHost – Responsible for orchestration.
- ServiceDefaults – Handles OpenTelemetry integration, health checks, and service discovery.
The application demonstrates the following core Dapr capabilities:
- Service-to-Service Invocation: Service-A communicates directly with Service-B via Dapr's service invocation API.
- Publish/Subscribe Messaging: Service-A publishes events, which Service-B consumes through a pub/sub mechanism.
- State Store: Service B use the state store to cache data, making them stateless and ensuring consistency.
- Secret Management: Service-B fetches configuration values securely from the secret store, showcasing Dapr’s secret integration.
This architecture not only simplifies the implementation of distributed systems but also ensures best practices for reliability, scalability, and modularity.
In the following sections, we'll explore how .NET Aspire enables YAML-free orchestration, making Dapr integration more accessible for developers of all skill levels.
The code below represents the .NET Aspire host project, where we declare services, configure Dapr components, and define their relationships with minimal or no YAML configuration files.
var builder = DistributedApplication.CreateBuilder(args);
var stateStore = builder.AddDaprStateStore("statestore");
var pubSub = builder.AddDaprPubSub("pubsub");
var secretStore = builder.AddDaprComponent("secretstore", "secretstores.local.file", new DaprComponentOptions
{
LocalPath = "..\\dapr\\components\\secretstore.yaml"
});
// Redis is used for pubsub and state store by default. So the redis needs to be running at the port 6379.
// Uncomment the below code to add redis as a container if it is not running
// builder.AddContainer("redis", "redis").WithEndpoint(port: 6379, targetPort: 6379);
var serviceA = builder.AddProject<Projects.AspireWithDapr_ServiceA>("service-a")
.WithDaprSidecar(options =>
options.WithAnnotation(
new EnvironmentCallbackAnnotation("APP_API_TOKEN", () => "secret-dapr-api-token"),
ResourceAnnotationMutationBehavior.Append))
.WithReference(stateStore)
.WithReference(pubSub)
.WithReference(secretStore)
.WithEnvironment("APP_API_TOKEN", "secret-dapr-api-token");
var serviceB = builder.AddProject<Projects.AspireWithDapr_ServiceB>("service-b")
.WithDaprSidecar(options =>
options.WithAnnotation(
new EnvironmentCallbackAnnotation("APP_API_TOKEN", () => "secret-dapr-api-token"),
ResourceAnnotationMutationBehavior.Append))
.WithReference(stateStore)
.WithReference(pubSub)
.WithReference(secretStore)
.WithEnvironment("APP_API_TOKEN", "secret-dapr-api-token");
builder.AddProject<Projects.AspireWithDapr_WebUI>("webfrontend")
.WithDaprSidecar(options =>
options.WithAnnotation(
new EnvironmentCallbackAnnotation("APP_API_TOKEN", () => "secret-dapr-api-token"),
ResourceAnnotationMutationBehavior.Append))
.WaitFor(serviceA)
.WaitFor(serviceB)
.WithEnvironment("APP_API_TOKEN", "secret-dapr-api-token");
builder.Build().Run();
In this code, you can see how Dapr components are configured. Two Dapr components, pubsub
and statestore
, are natively supported by .NET Aspire. Using the AddDaprPubSub()
and AddDaprStateStore()
extension methods, you can easily configure these components. The YAML configuration for built-in components, like state store and pub/sub, is automatically generated in a temporary folder, eliminating the need to create or manage configuration files manually.
The secretstore component is not natively supported by .NET Aspire at the moment. However, you can configure it by using the AddDaprComponent()
method and providing the necessary configuration details directly in the code. Below is the YAML configuration I used for the secret store:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: secretstore
spec:
type: secretstores.local.file
version: v1
metadata:
- name: secretsFile
value: "..\\dapr\\secret.json"
- name: nestedSeparator
value: ":"
In the code example, you can see the use of the WithDaprSidecar()
method provided by .NET Aspire to attach a Dapr sidecar to the service and how the components are referenced to the sidecar. The example also demonstrates how to inject environment variables into both the service and the Dapr sidecar to enable Dapr authentication.
...
.WithDaprSidecar(options =>
options.WithAnnotation(
new EnvironmentCallbackAnnotation("APP_API_TOKEN", () => "secret-dapr-api-token"),
ResourceAnnotationMutationBehavior.Append))
.WithReference(stateStore)
.WithReference(pubSub)
.WithReference(secretStore)
.WithEnvironment("APP_API_TOKEN", "secret-dapr-api-token");
The code snippet below shows how to use pub/sub, direct invocation, caching, and the secret store in your application. Make sure the names you use to set up these components match the names in the code for everything to work correctly.
service-a
invokes service-b
to retrieve data using Dapr's service-to-service invocation. It also publishes a message to a topic(PublishEventAsync("pubsub", "sample-topic", message)
), enabling consumption by any subscribed service.
app.MapGet("/get-data", async (DaprClient daprClient) =>
{
// Call ServiceB using Dapr Service Invocation
var response = await daprClient.InvokeMethodAsync<string>(HttpMethod.Get,"service-b", "get-data");
return Results.Ok(new { Message = "Data from ServiceB", Response = response });
});
app.MapPost("/publish", async (DaprClient daprClient, string message) =>
{
// Publish a message to the Pub/Sub topic
await daprClient.PublishEventAsync("pubsub", "sample-topic", message);
return Results.Ok("Message published!");
});
service-b
subscribes to the event(WithTopic("pubsub", "sample-topic")
) and accesses both the state store(statestore
) and secret store(secretstore
) for its operations.
app.MapGet("/get-data", async (DaprClient daprClient) =>
{
// Retrieve cached data from the state store
var state = await daprClient.GetStateAsync<string>("statestore", "cached-key");
if (string.IsNullOrEmpty(state))
{
state = "Data Form Service B";
await daprClient.SaveStateAsync("statestore", "cached-key", state);
}
return Results.Ok(state);
});
app.MapPost("/subscribe", async ([FromBody] string message, ILogger<Program> logger) =>
{
// Handle Pub/Sub messages
logger.LogInformation("Received message: {Message}", message);
return Results.Ok($"Message received: {message}");
})
.WithTopic("pubsub", "sample-topic")
.RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = "Dapr" });
app.MapGet("/get-secret", async (DaprClient daprClient) =>
{
// Retrieve a secret from the secret store
var secret = await daprClient.GetSecretAsync("secretstore", "my-secret");
return Results.Ok(secret);
});
The complete source code is available in my GitHub repository
With the code in place, it's time to launch the application. You can start the AppHost project. Both Visual Studio and Visual Studio Code will launch your app and the .NET Aspire dashboard automatically in your browser. If you run the app using the .NET CLI, simply copy the dashboard URL from the output and paste it into your browser.
The Aspire dashboard is used to monitor and inspect various aspects of your application, including logs, traces, and environment configurations. Designed to enhance the local development experience, this dashboard provides a comprehensive overview of the overall state and structure of your distributed app.
When you launch the app, you will see the required containers running in Podman Desktop, all managed seamlessly by .NET Aspire!.
If you navigate to the Traces menu in the Aspire Dashboard, you will see complete traces from the distributed app (if properly instrumented with the OpenTelemetry SDK – basic instrumentation is already provided by the Aspire Template and Dapr SDK).
When you invoke the API from the frontend application or initiate communication with pub/sub, you will be able to see complete traces.
Adding .NET Aspire to Existing Project
1. Adding a ServiceDefaults Project
Add Service Defaults to the solution to include health checks, logging, and other recommended features for both front-end and back-end projects.
Steps:
-
Add Service Defaults Project
In Visual Studio 2022 follow these steps:- Right-click on the solution and select Add > New Project.
- Select the
.NET Aspire Service Defaults
project template. - Name the project
ServiceDefaults
(or any name). - Click Next > Create. Select .Net Aspire Version 9.0
Configure Service Defaults
Add a reference to theServiceDefaults
project in all the projects including the backend services and the web ui-
Update Program.cs
In the services and frontend projects:-
Update
Program.cs
files of all projects by adding the following line immediately after thevar builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
-
Update the
Program.cs
files by adding the following line immediately after thevar app = builder.Build();
line:app.MapDefaultEndpoints();
-
2. Adding an AppHost Project
Add an AppHost Project to the solution to orchestrate the services.
Steps:
-
Add AppHost Project
In Visual Studio 2022 follow the steps- Right-click on the solution and select Add > New Project.
- Select the
.NET Aspire App Host
project template. - Name the project
AppHost
(any name would work). - Click Next > Create. Select .Net Aspire Version 9.0
-
Add project references
Add a reference to both the
services
andWebUI
projects in the newAppHost
project:
1. Right-click on theAppHost
project and select Add > Reference.
2. Check theSeviceA, ServiceB
andWebUI
projects and click OK. Orchestrate the Application
In theAppHost
project, update theProgram.cs
file to configure components/integration, add projects and sidecars as shown in the code example above.
Conclusion
Combining .NET Aspire with Dapr revolutionizes the local development experience by eliminating the need for manual YAML configuration files. This integration allows for seamless orchestration of microservices, sidecars, and networking details, simplifying the development process. The Aspire Dashboard provides a comprehensive overview of the distributed application, offering insights into component interactions and making it easier for teams to understand and manage the system. Additionally, .NET Aspire eliminates the dependency on Docker Desktop, enabling developers to focus solely on building and debugging locally with ease.
References
- .NET Aspire overview
- Use Dapr with .NET Aspire
- Aspire Container runtime
- Dapr documentation
- Dapr .NET SDK Development with .NET Aspire
- Aspire Code Samples
You can find the complete source code here.
Top comments (0)