.NET Aspire and Aspirate (Aspir8)

Understanding of .NET Aspire Starter Application project template and what is Aspirate (Aspir8)?

For the past few weeks, I have been exploring .NET Aspire, and it appears to hold promise for developing multi-project applications while also offering seamless orchestration, resilience, service discovery, and telemetry capabilities. For more detail you can visit .NET Aspire.

You can follow below instructions to execute and see the working of .NET Aspire.

Prerequisites

  • .NET 8.0
  • .NET Aspire workload
  • Docker Desktop
  • Visual Studio Code

Install or update .NET Aspire workload

The .NET Aspire workload makes available .NET Aspire project templates. These project templates allow you to create new apps pre-configured with the .NET Aspire project structure and default settings.

dotnet workload update

Install .NET Aspire workload and check the Aspire templates

dotnet workload install update
dotnet new list aspire

Create a new .NET Aspire Starter application

dotnet new aspire-starter --use-redis-cache --output AspireSample

It creates four projects

  • AspireSample.Web - An ASP.NET Core Blazor App project with default .NET Aspire service configurations.
  • AspireSample.ApiService: An ASP.NET Core Minimal API project is used to provide data to the front end.
  • AspireSample.ServiceDefaults - A .NET Aspire shared project to manage configurations that are reused across the projects. it provides resilience, service discovery, and telemetry.
  • AspireSample.AppHost - An orchestrator project designed to connect and configure the different projects and services of your app. The orchestrator should be set as the Startup project. It is only available for development environment.

Run app

dotnet run --project AspireSample/AspireSample.AppHost

After successful run command, you can open dashboard, clicking host section's url from terminal window.

  • Dashboard containing Resources, Console, Structured, Traces and Metrics options
  • Redis container running in docker desktop

NOTE: The Redis container is automatically prepared, and once you stop execution, it disappears.

What is the magic behind this?

The AspireSample.Host project is responsible for this magic. Refer comments in below code snippet.

var builder = DistributedApplication.CreateBuilder(args);
 
// this line instructs to add a Redis container to the application model.
var cache = builder.AddRedis("cache");
 
// this line instructs to add a .NET project to the application model. it's used to configure service discovery and communication between the projects in app.
var apiService = builder.AddProject<Projects.AspireSample_ApiService>("apiservice");
 
// this line instructs to add a .NET project to the application model.The WithReference API  injects either service discovery information or connection string configuration into the project being added to the application model.
builder.AddProject<Projects.AspireSample_Web>("webfrontend")
    .WithReference(cache)
    .WithReference(apiService);
 
builder.Build().Run();

The AspireSample.Web project is refer 'cache' and 'apiservice'. Refer comments in below code snippet.

using AspireSample.Web;
using AspireSample.Web.Components;
 
var builder = WebApplication.CreateBuilder(args);
 
// Add service defaults & Aspire components.
builder.AddServiceDefaults();
// Referencing 'cache' name from host project.
builder.AddRedisOutputCache("cache");
 
// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
 
// Referencing 'apiservice' name from host project.
builder.Services.AddHttpClient<WeatherApiClient>(client => client.BaseAddress = new("http://apiservice"));
 
var app = builder.Build();
 
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
 
app.UseStaticFiles();
 
app.UseAntiforgery();
 
app.UseOutputCache();
 
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();
 
app.MapDefaultEndpoints();
 
app.Run();

What is in ServiceDefaults?

The AspireSample.ServiceDefaults project is referenced in both projects e.g., AspireSample.ApiService and AspireSample.Web. It adds resilience, service discovery, and telemetry.

public static class Extensions
{
    public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
    {
        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.UseServiceDiscovery();
        });
 
        return builder;
    }
 
    public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
    {
        builder.Logging.AddOpenTelemetry(logging =>
        {
            logging.IncludeFormattedMessage = true;
            logging.IncludeScopes = true;
        });
 
        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics =>
            {
                metrics.AddAspNetCoreInstrumentation()
                       .AddHttpClientInstrumentation()
                       .AddProcessInstrumentation()
                       .AddRuntimeInstrumentation();
            })
            .WithTracing(tracing =>
            {
                if (builder.Environment.IsDevelopment())
                {
                    // We want to view all traces in development
                    tracing.SetSampler(new AlwaysOnSampler());
                }
 
                tracing.AddAspNetCoreInstrumentation()
                       .AddGrpcClientInstrumentation()
                       .AddHttpClientInstrumentation();
            });
 
        builder.AddOpenTelemetryExporters();
 
        return builder;
    }
 
    private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
    {
        var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
 
        if (useOtlpExporter)
        {
            builder.Services.Configure<OpenTelemetryLoggerOptions>(logging => logging.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());
            builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
        }
 
        // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
        // builder.Services.AddOpenTelemetry()
        //    .WithMetrics(metrics => metrics.AddPrometheusExporter());
 
        // 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 IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
    {
        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)
    {
        // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
        // app.MapPrometheusScrapingEndpoint();
 
        // 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;
    }
}

What is Aspirate (Aspir8) and How to use it?

Aspirate (Aspir8) handles deployment yaml generation for a .NET Aspire.

Use this command to install aspirate

dotnet tool install -g aspirate --prerelease

Use this command to check installation

aspirate

Navigate to your Aspire project's AppHost directory, and run below commands to generate docker-compose.yml and to deploy on Docker desktop:

aspirate generate --output-format compose

The manifests will be in the AppHost/aspirate-output directory by default.

docker compose up

webfrontend, apiservice and cache get hosted on Docker Desktop.

Aspirate also provides following commands for generating manifest files for Kubernetes and deploying to a Kubernetes cluster.

aspirate generate

The manifests will be in the AppHost/aspirate-output directory by default.

To apply the manifest to the kubernetes cluster, run

aspirate apply

NOTE: I have used Kubernetes option of Docker Desktop. It starts a Kubernetes single-node cluster. Also, I have used Lens - The Kubernetes IDE to see Cluster, Nodes, Workloads, Config, Network, Storage, Namespace etc and launch the webfrontend application.

To remove the manifest from the kubernetes cluster, run

aspirate destroy

Happy Coding...