Modern development with .NET Aspire and AWS

Introduction

Modern distributed applications rely on a complex ecosystem of services to function effectively. From Redis for caching to OpenTelemetry for monitoring, and database options like PostgreSQL or MongoDB, managing these components presents significant challenges, especially when deploying to cloud platforms like AWS or Azure. To face This article explores how .NET Aspire empowers developers to build and deploy cloud-native applications within the AWS ecosystem. We’ll demonstrate implementation using Visual Studio Code, though the principles apply equally to other development environments like Visual Studio or JetBrains.

What is .NET Aspire

.NET Aspire is not a new framework or a technology, it is a set of pillars that enable cloud-ready application development. It is delivered through a collection of NuGet packages that make it easier to develop, integrate, and orchestrate distributed applications comprising many small, interconnected services.

Aspire is based on the following three main pillars:

These three pillars of Aspire provide a simplified and enhanced productivity environment for building, orchestrating and managing complex distributed applications.

If you’re interested in learning more about .NET Aspire check this overview https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview.

Set up your environment

To be able to work with .NET Aspire and AWS, and follow all the info described in this article, you need to have:

Build your .NET Aspire project

First of all, we’ll proceed by creating a .NET Aspire project. Before that, we should make sure that we have the latest versions of .NET workloads. To do that, we can open the terminal and write:

dotnet workload update

Then we can install, or update, the latest Aspire project templates:

dotnet new install Aspire.ProjectTemplates

Now, we can go to our project folder and write the following command to create our first Aspire solution:

dotnet new Aspire

The above command will create a .NET solution with basically two projects:

A .NET Aspire project’s core component is the AppHost project. In this project, we are going to define all the components that will run in our application environment. For example, if we need a Redis cache, as mentioned before, we will write the following code into the Program.cs file:

var builder = DistributedApplication.CreateBuilder(args);
builder.AddRedis("cache");
builder.Build().Run();

This is the center of our application. We can instruct how the application network should work and the connection between every service inside the AppHost project, following the basis of IaC (Infrastructure as Code) principle. Now, we can proceed by adding the services that we want to run. For example, if we need to add a WebAPI, we should first create a project, and then add to the AppHost solution. Let’s see how we can do it by using dotnet CLI:

dotnet new webapi -o AspireOnAWS.ApiService -n AspireOnAWS.MyApiService
dotnet sln add AspireOnAWS.ApiService/AspireOnAWS.ApiService.csproj

Nothing new? That’s right. But, if we want to use the newly created project directly into the AppHost, we should add it as Project Reference:

dotnet add AspireOnAWS.AppHost/AspireOnAWS.AppHost.csproj reference AspireOnAWS.ApiService/AspireOnAWS.ApiService.csproj

Now we can use the project in the Program.cs. Let’s see the updated code:

var builder = DistributedApplication.CreateBuilder(args);

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

builder.AddProject<Projects.AspireOnAWS_ApiService>("api")
    .WithExternalHttpEndpoints()
    .WithReference(cache)
    .WaitFor(cache);

builder.Build().Run();

The above code adds two projects to the AppHost: the Redis cache, as showed before, and the ApiService. Since the ApiService project requires the Redis cache to run properly, we need to declare a reference by using the WithReference method, and a run dependency by using WaitFor. The latter means that we need to wait that the Redis cache service is ready before start the ApiService.

Run your .NET Aspire project

By simply executing dotnet run, we can now see the results:

What happens in the middle? .NET Aspire requests execution of a container based on Redis image. If this is not available locally, it tries to download from the registry (!). When the image is available, a container instance is built and run. Easy.

Ready for AWS

We had just seen how to create a .NET Aspire solution and connect it with a pre-built service. Now, we are ready to see how we can interact and use AWS to run our solution. Following the IaC rule, with .NET 9, AWS introduces the possibility of using the AWS Cloud Development Kit (CDK) in a .NET Aspire project. A big improvement that allows building applications by defining your AWS resources directly in code. CDK is a coding abstraction built on top of CloudFormation. You can define your infrastructure using your preferred language, and CDK will automatically generate a CloudFormation template. First of all, we should add the Aspire.Hosting.AWS package to our AppHost project:

dotnet add package Aspire.Hosting.AWS

Now we can start using AWS CDK to configure the services that we would like to use. For example, if we want to use a S3 Bucket, we can add it in our AppHost by using the following code:

var builder = DistributedApplication.CreateBuilder(args);

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

var stack = builder.AddAWSCDKStack("MyAWSStack");
var bucket = stack.AddS3Bucket("MyAspireBucket");

builder.AddProject<Projects.AspireOnAWS_ApiService>("api")
    .WithExternalHttpEndpoints()
    .WithReference(cache)
    .WaitFor(cache)
    .WithReference(bucket)
    .WaitFor(bucket);

builder.Build().Run();

As seen before, also in this case by using WithReference we are declaring that the ApiService project should be able to see and use the S3 bucket. Futhermore, the WaitFor indicates that ApiService can be executed only when the S3 bucket service will be ready. Resources created with these methods can be directly referenced by project resources and common properties like resource names, ARNs or URLs will be made available as configuration environment variables. The default config section will be AWS:Resources, this means that we can access to our bucket by using configuration["AWS:Resources:MyAspireBucket:BucketName"]. For example, in our ApiService, we can write the following code to retrieve the bucket name and comunicate with S3:

app.MapGet("/weatherforecast", async ([FromServices] IAmazonS3 s3client, IConfiguration configuration) =>
{
    // code omitted ...
    PutObjectRequest request = new PutObjectRequest
    {
        BucketName = configuration["AWS:Resources:MyAspireBucket:BucketName"],
        Key = "WhetherForecast.json",
        ContentType = "application/json",
        ContentBody = JsonConvert.SerializeObject(forecast)
    };

    await s3client.PutObjectAsync(request);

    // code omitted ...
})

Executing that code will show the following dashboard:

Test your code

We can now execute the Swagger UI and test the service by executing the GET /weatherservice. As we can see after execution, now the result of the API will be written in a S3 Bucket. If we open the AWS console, in Cloud Formation section we can now see the related, and just created, AWS CloudFormation Stack:

By selecting the MyAWSStack and navigating into the Resources, we can see the MyAspireBucket resource:

How is this possible? Well, the Aspire.Hosting.AWS also includes and runs the CDK code under the hood. So when you run your application, even locally, the package will create the required AWS infrastructure for you. And that’s a great thing!

Conclusion

In this first post we have seen what .NET Aspire is and how we can use it in combination with AWS resources by using the AWS CDK. In the next few posts, we will look at additional ways to use the AWS .NET Aspire integration in our solutions.

Enjoy!

Found a problem? Edit this post