Aspire?—?.NET’s Goodbye to Cloud-Native Complexity!
Shalinda Silva
Senior Software Engineer | .NET Core, Azure, and Full-Stack Development | Healthcare
With the new?.NET 9 release, we’ve had the chance to get our hands on the much-improved Aspire toolset. Aspire makes cloud-native development faster and easier. Why? To answer this, we first need to understand the problem Aspire tries to solve.
Building cloud-native apps has always come with its unique challenges, regardless of the technology used. Managing micro-services, configuring observability, service discovery, and orchestrating services often requires stitching together multiple tools across multiple domains, such as Kubernetes, OpenTelemetry, and service discovery frameworks.
Enter?.NET Aspire! The newest addition to the?.NET sphere aims to simplify all the above-mentioned problems in one place. That’s what Aspire tries to solve!
Let’s set up the development environment first.
2. Install docker (or any other OCI compliant container runtime)
3. Install templates
4. Create AppHost and ServiceDefaults projects (I use JetBrains Rider here)
What are AppHost and ServiceDefaults projects?
Lets jump in to?AppHost
Let’s assume we have a Blazor web app and a?.NET web API. With that in mind, let’s take a look at AppHost’s Program.cs file.
var builder = DistributedApplication.CreateBuilder(args);
builder.Build().Run();
The DistributedApplication class represents a distributed application that implements the IHost and IAsyncDisposable interfaces. To build an instance of the IDistributedApplicationBuilder class, we use the CreateBuilder() method.
Afterward, we can “Add” things to this builder.
Let’s say you want a Redis cache to cache your Blazor web app:
领英推荐
"""
Install Aspire.Hosting.Redis package
"""
var builder = DistributedApplication.CreateBuilder(args);
// Add a Redis server to the application.
var cache = builder.AddRedis("cache"); // <- this is your redis container
// Add the frontend project to the application and configure it to use the
// Redis server, defined as a referenced dependency.
builder.AddProject<Projects.MyFrontend>("frontend")
.WithReference(cache) // <- this line creates the connection to redis
.WaitFor(cache); // <- wait for the redis server to enter running state
builder.Build().Run();
Voilà! Just like that, our front-end app will be cached in Redis. You can set up all the other parameters, like duration and cache validations/invalidations, inside your Blazor app as usual.
Want to add a?.NET web API with a PostgreSQL database?
"""
Install Aspire.Hosting.Postgres package
"""
var builder = DistributedApplication.CreateBuilder(args);
// Add the Postgres server
var postgres = builder.AddPostgres("postgres")
.WithDataVolume()// <- to presist data in between multiple app runs
.WithPgAdmin();// <- this will add a seperate web based PgAdmin SQL dashboard
// Creates the database
var myDb = postgres.AddDatabase("MyDb");
// Add the web api service
builder.AddProject<Projects.MyAPI>("MyAPI")
.WithHttpEndpoint(5010)// <- define ports
.WithHttpsEndpoint(7010)
.WithReference(myDb); // <- create the connection to MyDb
builder.Build().Run();
The best part is that all the manual configuration, handling connection strings in multiple places, and worrying about host addresses and port numbers goes away. All of this is now handled by the Aspire.Hosting.* NuGet packages.
Once all the services are in order, call the Build() method to create an instance of DistributedApplication. This will expose the Run() method, which runs all the resources in the AppHost.
Our final code will look something like this:
var builder = DistributedApplication.CreateBuilder(args);
// Add a Redis server to the application.
var cache = builder.AddRedis("cache"); // <- this is your redis container
// Add the frontend project to the application and configure it to use the Redis server.
builder.AddProject<Projects.MyFrontend>("frontend")
.WithReference(cache) // <- this line creates the connection to redis
.WaitFor(cache); // <- wait for the redis server to enter running state
// Add a Postgres server to the application.
var postgres = builder.AddPostgres("postgres")
.WithDataVolume() // <- to presist data in between multiple app runs
.WithPgAdmin(); // <- this will add a seperate web based PgAdmin SQL dashboard
// Creates the database
var myDb = postgres.AddDatabase("MyDb");
// Add the API project to the application and configure it to use the Postgres server.
builder.AddProject<Projects.MyAPI>("MyAPI")
.WithHttpEndpoint(5010)// <- define ports
.WithHttpsEndpoint(7010)
.WithReference(myDb); // <- create the connection to MyDb
builder.Build().Run();
Next, ServiceDefaults
This is a shared project that adds multiple functionalities to Aspire orchestrated apps and services. This includes health checks, logging, observability, security, etc.
This comes with already implemented extension methods like AddServiceDefaults(). Lets take a look at Extensions.cs.
public static class Extensions
{
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
// Turn on resilience by default
http.AddStandardResilienceHandler();
// Turn on service discovery by default
http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
// {
// options.AllowedSchemes = ["https"];
// });
return builder;
}
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation()
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
//.AddGrpcClientInstrumentation()
.AddHttpClientInstrumentation();
});
builder.AddOpenTelemetryExporters();
return builder;
}
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
//{
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
//}
return builder;
}
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks()
// Add a default liveness check to ensure app is responsive
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment())
{
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks("/health");
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
return app;
}
}
In AddServiceDefaults() method
If you want to use the default implementations all you have to do is add builder.AddServiceDefaults(); to your API’s program.cs.
builder.AddServiceDefaults(); // <- adds OpenTelemetry, health checks and Service Discovery
We can add more extensions in this. For example, lets say we need to implement custom resilience policies. We can implement this in ServiceDefaults and add that extension to our web APIs.
Aspier Dashboard
The Aspire dashboard provides a clean and organized interface for managing your application’s resources. It clearly displays the status of various components, from containers like cache, postgres, and postgres-pgadmin to projects such as frontend and MYAPI.
The dashboard offers a summarized view including the resource type, name, state, start time, source, and endpoints, allowing for quick monitoring and easy access to details. With a minimalist design and a dark theme, the Aspire dashboard prioritizes information clarity, enabling developers to efficiently oversee their application's health and performance at a glance. Features like the filterable search bar further enhance usability, making it simple to locate specific resources within a potentially complex system.
You can find completed code in this repo: shalindasilva1/Aspire-test