DEV Community

Cover image for .NET Aspire and Dapr. Overlap or complementary?
Tommaso Stocchi
Tommaso Stocchi

Posted on

.NET Aspire and Dapr. Overlap or complementary?

Although .NET Aspire and Dapr are two different projects, they have some overlap in their goals. Both projects aim to simplify the development of distributed applications by providing a set of building blocks that developers can use. However, there are some key differences between the two projects that make them complementary rather than competing.

Let's take a closer look at the goals of each project and how they can work together to help developers build better distributed applications.

.NET Aspire

.NET Aspire aims to simplify the development of distributed applications, including architectural components such as cache, databases, and queues. These components - now called integrations - are designed to be easy to use and configure, so developers can focus on building their applications rather than worrying about the underlying infrastructure.

On top of that, Aspire offers the ability to inject the connection strings of both the integrations and the different services where needed. Let's take a look at the basic .NET Aspire template.
Let's run the following command:

dotnet new aspire-starter --use-redis-cache
Enter fullscreen mode Exit fullscreen mode

In the <Project_Name.AppHost> project that has been created for us, we can find the following code:

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

var apiService = builder.AddProject<Projects.DistributedDAB_ApiService>("apiservice");

builder.AddProject<Projects.DistributedDAB_Web>("webfrontend")
    .WithExternalHttpEndpoints()
    .WithReference(cache)
    .WithReference(apiService);

builder.Build().Run();
Enter fullscreen mode Exit fullscreen mode

As you can see, we are adding a Redis cache and a project called ApiService. We are also adding a project called WebFrontend that has external HTTP endpoints and references the cache and the ApiService. .NET Aspire is actually injecting the connection strings of the cache and the ApiService. In .NET, we can use the connection string like this:

builder.Services.AddHttpClient<WeatherApiClient>(client =>
    {
        client.BaseAddress = new("https+http://apiservice");
    });
Enter fullscreen mode Exit fullscreen mode

In other coding languages we would have read the connection string as an environment variable:

services__apiservice__http__0
Enter fullscreen mode Exit fullscreen mode

On top of that, Aspire offers observability via the Aspire Dashboard leveraging OpenTelemetry. Using the dashboard, developers can monitor the performance of their applications, read logs and traces, and troubleshoot issues in real-time. As a cherry on top, we can leverage the Azure Developer CLI to easily deploy our applications to Azure.

.NET Aspire actually follows the deployment of distributed applications from the IDE to the cloud.

Dapr

Dapr is a portable, event-driven runtime that makes it easy for developers to build resilient, microservices-based applications. Dapr provides a set of building blocks that developers can use to build their applications, including state management, pub/sub messaging, and service invocation. These building blocks are designed to be language-agnostic, so developers can use them with any programming language.

Dapr uses the sidecar pattern. Basically, our microservice doesn't talk to the Redis cache or the API service directly. Instead, it talks to the Dapr sidecar, which in turn talks to the cache or the API service. This allows Dapr to provide a consistent interface for all the building blocks, regardless of the underlying implementation.

What's awesome about Dapr is the ability to define a building block — say a pub/sub message — in a YAML file and then use it in any programming language. This is possible because Dapr is a sidecar that runs alongside the application, providing the building blocks as a service. These YAML files can be swapped out for different environments, making it easy to switch from a Redis Queue to an Azure Service Bus without changing any code!!!!.

Take a look at this example of an API that subscribes to a topic, and guess which pub/sub component I'm using:

builder.Services.AddDaprClient();

...

app.UseCloudEvents();
app.UseEndpoints(endpoints => endpoints.MapSubscribeHandler());

app.MapPost("/", [Topic("queue", "topic")] async (DaprClient daprClient) => 
{
    ...
Enter fullscreen mode Exit fullscreen mode

The answer is: it depends. Locally, it is using a Redis pub/sub running in a container, but when it's deployed to the cloud, it's using Azure Service Bus. The only thing that changes is the YAML file that defines the pub/sub component.

Moreover, Dapr offers the ability to implement service-to-service invocation via HTTP. If we wanted to call the previous WeatherApiClient from the ApiService, we would do it like this:

// Using generic http client
var client = new HttpClient();
var response = await client.GetAsync("http://localhost:<DAPR_SIDECAR_PORT>/v1.0/invoke/apiservice/method/weatherforecast");

// using Dapr client (SDK specific)
var response = await _daprClient.InvokeMethodAsync<WeatherForecast[]>("apiservice", "weatherforecast");
Enter fullscreen mode Exit fullscreen mode

Overlap or complementary?

Dapr can be integrated into .NET Aspire. We can define a Dapr building block in a YAML file and then use it in our .NET Aspire application.

Here's an example of how to add Dapr components to a .NET Aspire application, but for the full picture please refer to the official documentation.

var secretstore = builder.AddDaprComponent("secretstore", "secretstores.azure.keyvault", new DaprComponentOptions { LocalPath = "..\\.dapr\\components\\secretstore.yml"});
var pubsub = builder.AddDaprPubSub("pubsub", new DaprComponentOptions { LocalPath = ".\\.dapr\\components\\pubsub.yml"});
Enter fullscreen mode Exit fullscreen mode

Cloud component -> complementary

Aspire can define resources, inject their connection strings where needed, and also create those resources when deploying to the cloud via the Azure Developer CLI. Dapr can reference resources already created, defining endpoints and configurations in YAML files that can easily be swapped.

In situations where we are testing our application locally and don't want to leverage cloud resources, we could easily reference the local Redis cache container as a pub/sub handler and then switch to Azure Service Bus when deploying to the cloud. When we do this, we can still leverage the Aspire integration with the Azure Developer CLI to create all of our resources during deployment.

Service to service invocation -> overlap (?)

As I mentioned, .NET Aspire will provide us with the correct connection string for whichever service we need to invoke. Dapr can do the same thing, but it will do it via the sidecar. This is something we need to keep in mind when making our decision.

Here's a schema of how Dapr implements service-to-service invocation:

Dapr service-to-service invocation

  1. Service A makes an HTTP or gRPC call targeting Service B. The call goes to the local Dapr sidecar.
  2. Dapr discovers Service B’s location using the name resolution component, which is running on the given hosting platform.
  3. Dapr forwards the message to Service B’s Dapr sidecar. Note: All calls between Dapr sidecars go over gRPC for performance. Only calls between services and Dapr sidecars can be either HTTP or gRPC.
  4. Service B’s Dapr sidecar forwards the request to the specified endpoint (or method) on Service B. Service B then runs its business logic code.
  5. Service B sends a response to Service A. The response goes to Service B’s sidecar.
  6. Dapr forwards the response to Service A’s Dapr sidecar.
  7. Service A receives the response.

These extra steps can take a toll on performance, but it is platform agnostic. It doesn't matter which platform we are running our distributed application on: service-to-service invocation won't need to take that into consideration. Also, all these HTTP/gRPC requests are performed on localhost.

On the other hand, if we leverage the .NET Aspire and Azure Developer CLI integration, we can deploy our application to Azure Container Apps and always have the correct connection string injected.
When running the <Project_Name.AppHost> locally, all these endpoints will be http://localhost:<PORT>. When deploying to Azure Container Apps, all these endpoints will be those of the correct Container App instances. Our code won't change because we are referencing an injected environment variable and we don't need to know the actual value of the endpoints.

In conclusion, .NET Aspire and Dapr are two different projects that can be used together to build better distributed applications. .NET Aspire streamlines the process of going from local development to the cloud, providing all the tools needed for building modern and resilient distributed applications. Dapr can help make our applications flexible and as cloud-agnostic as possible. By using both projects together, developers can take advantage of the strengths of each project to build better distributed applications.

Top comments (0)