.NET Aspire: Cloud-native Anwendungen richtig erstellen

· 10 Min. Lesezeit

Wenn Sie jemals eine verteilte Anwendung in .NET erstellt haben, kennen Sie die Übung. Sie starten eine Web-API, fügen einen Hintergrund-Worker hinzu, fügen Redis für das Caching, PostgreSQL für die Persistenz und vielleicht RabbitMQ für die Nachrichtenübermittlung hinzu – und plötzlich verbringen Sie mehr Zeit mit der Verkabelung der Infrastruktur als mit dem Schreiben von Geschäftslogik. Über appsettings.json Dateien verstreute Verbindungszeichenfolgen, Gesundheitsprüfungen, die Sie vergessen haben zu konfigurieren, und Beobachtbarkeit, die immer „das Problem des nächsten Sprints“ ist.

Ich war dort. Mehrmals, als ich zugeben möchte.

Das ist genau das Problem, das .NET Aspire lösen soll. Nachdem ich es nun seit mehreren Monaten in der Produktion laufe, möchte ich mit Ihnen teilen, was ich gelernt habe – das Gute, das Großartige und die Fallstricke.

Was ist .NET Aspire?

.NET Aspire ist ein eigenständiger Stack zum Erstellen beobachtbarer, produktionsbereiter, verteilter Anwendungen mit .NET. Es handelt sich nicht um ein Framework im herkömmlichen Sinne – es ersetzt weder ASP.NET Core noch zwingt es Sie zu einem neuen Programmiermodell. Stattdessen baut es auf dem auf, was Sie bereits wissen, und füllt die Lücken, die beim Erstellen cloudnativer Apps schon immer bestanden haben.

Im Kern bietet Ihnen Aspire vier Dinge:

  1. AppHost – Ein Projekt, das Ihre gesamte verteilte Anwendungstopologie definiert. Welche Dienste existieren, wovon sie abhängen und wie sie miteinander verbunden sind.
  2. Dienststandards – Ein gemeinsames Projekt, das übergreifende Belange wie Gesundheitsprüfungen, Ausfallsicherheitsrichtlinien und OpenTelemetry einmalig für alle Ihre Dienste konfiguriert.
  3. Komponenten – NuGet-Pakete, die standardisierte Integrationen mit unterstützenden Diensten wie Redis, PostgreSQL, RabbitMQ, Azure Storage und mehr bieten.
  4. Entwickler-Dashboard – Eine Echtzeit-Benutzeroberfläche, die Protokolle, Ablaufverfolgungen und Metriken für Ihre gesamte verteilte App während der lokalen Entwicklung anzeigt.

Die Philosophie ist einfach: Wenn jede .NET-Cloud-App diese Dinge benötigt, warum implementieren wir sie dann alle jedes Mal von Grund auf?

Einrichten Ihres ersten Aspire-Projekts

Der Einstieg ist unkompliziert. Sie benötigen .NET 8 oder höher und den Aspire-Workload installiert:

dotnet workload update
dotnet workload install aspire

Erstellen Sie nun ein neues Aspire-Starterprojekt:

dotnet new aspire-starter -n MyCloudApp

Dadurch entsteht eine Lösung mit vier Projekten:

  • MyCloudApp.AppHost – Der Orchestrator
  • MyCloudApp.ServiceDefaults – Gemeinsame Konfiguration
  • MyCloudApp.ApiService – Eine Beispiel-Web-API
  • MyCloudApp.Web – Ein Blazor-Frontend

Führen Sie AppHost aus und Sie sehen sofort, dass das Aspire-Dashboard in Ihrem Browser geöffnet ist und alle Ihre Dienste, ihre Protokolle und ihren Zustand anzeigt. Keine Docker Compose-Datei. Keine manuelle Portverwaltung. Es funktioniert einfach.

dotnet run --project MyCloudApp.AppHost

Diese erste Erfahrung hat mich fasziniert. In weniger als einer Minute haben Sie eine vollständig orchestrierte verteilte App mit integrierter Observability.

Das AppHost-Muster

Der AppHost ist der Ort, an dem die Magie lebt. Es handelt sich um eine kleine Konsolenanwendung, die ein Builder-Muster verwendet, um die Topologie Ihrer verteilten Anwendung zu definieren – welche Ressourcen vorhanden sind und wie Dienste eine Verbindung zu ihnen herstellen.

So sieht ein realistischer AppHost aus:

var builder = DistributedApplication.CreateBuilder(args);

// Infrastructure resources
var postgres = builder.AddPostgres("postgres")
    .WithPgAdmin()
    .AddDatabase("catalogdb");

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

var rabbitmq = builder.AddRabbitMQ("messaging")
    .WithManagementPlugin();

// Application services
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReference(rabbitmq);

var orderApi = builder.AddProject<Projects.OrderApi>("order-api")
    .WithReference(postgres)
    .WithReference(rabbitmq);

var frontend = builder.AddProject<Projects.WebFrontend>("frontend")
    .WithExternalHttpEndpoints()
    .WithReference(catalogApi)
    .WithReference(orderApi);

builder.Build().Run();
```Lesen Sie diesen Code laut vor. Es dokumentiert sich praktisch von selbst. Die Katalog-API verweist auf PostgreSQL, Redis und RabbitMQ. Das Frontend verweist auf die Katalog-API und die Bestell-API. Das ist Ihre Architektur, ausgedrückt im Code.

Ein paar Dinge, die es zu beachten gilt:

- **`WithReference`** übernimmt die schwere Arbeit. Über Umgebungsvariablen und Konfiguration fügt es automatisch Verbindungszeichenfolgen und Dienst-URLs in das konsumierende Projekt ein. Ihre Dienste müssen nicht wissen, *wo* Redis ausgeführt wird  Aspire kümmert sich darum.
- **`WithPgAdmin()`** und **`WithManagementPlugin()`** richten neben den eigentlichen Diensten auch Admin-UIs für PostgreSQL und RabbitMQ ein. Während der Entwicklung sind diese von unschätzbarem Wert.
- **`WithExternalHttpEndpoints()`** markiert einen Dienst als extern zugänglich, was zum Zeitpunkt der Bereitstellung wichtig ist.

### Ressourcenkonfiguration

Sie können Ressourcen mit differenzierter Steuerung konfigurieren:

```csharp
var postgres = builder.AddPostgres("postgres")
    .WithEnvironment("POSTGRES_MAX_CONNECTIONS", "200")
    .WithDataVolume("postgres-data")
    .AddDatabase("catalogdb");

var redis = builder.AddRedis("cache")
    .WithDataVolume("redis-data");

Datenmengen stellen sicher, dass Ihre lokalen Entwicklungsdaten Container-Neustarts überstehen. Kleines Detail, große Verbesserung der Lebensqualität.

Dienststandards: Der unbesungene Held

Das ServiceDefaults-Projekt ist der am meisten unterschätzte Teil von Aspire. Es handelt sich um eine gemeinsam genutzte Bibliothek, auf die jeder Dienst in Ihrer Lösung verweist, und sie konfiguriert alle übergreifenden Anliegen, die Sie sonst vergessen oder inkonsistent implementieren würden.

So sieht ein typisches Extensions.cs in ServiceDefaults aus:

public static class Extensions
{
    public static IHostApplicationBuilder AddServiceDefaults(
        this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();
        builder.AddDefaultHealthChecks();
        builder.Services.AddServiceDiscovery();

        builder.Services.ConfigureHttpClientDefaults(http =>
        {
            http.AddStandardResilienceHandler();
            http.AddServiceDiscovery();
        });

        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()
                    .AddRuntimeInstrumentation();
            })
            .WithTracing(tracing =>
            {
                tracing.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddGrpcClientInstrumentation();
            });

        builder.AddOpenTelemetryExporters();
        return builder;
    }

    public static IHostApplicationBuilder AddDefaultHealthChecks(
        this IHostApplicationBuilder builder)
    {
        builder.Services.AddHealthChecks()
            .AddCheck("self", () => HealthCheckResult.Healthy(),
                ["live"]);

        return builder;
    }
}

Dann erledigt im Program.cs jedes Dienstes eine Zeile alles:

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();

// Your service-specific configuration...

var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();

Mit diesem einzigen AddServiceDefaults()-Aufruf erhalten Sie:

  • OpenTelemetry mit strukturierter Protokollierung, Metriken und verteilter Ablaufverfolgung
  • Gesundheitsprüfungen mit Liveness- und Readiness-Endpunkten
  • Diensterkennung, damit Dienste einander anhand ihres Namens finden können
  • Resilienzrichtlinien für alle ausgehenden HTTP-Aufrufe (Wiederholungsversuche, Leistungsschalter, Zeitüberschreitungen)

Das ist es, was eine „funktioniert auf meiner Maschine“-Demo von einem produktionsbereiten System unterscheidet. Und Aspire macht es zur Standardeinstellung und nicht zu einem nachträglichen Gedanken.

Aspire-Komponenten

Aspire-Komponenten sind NuGet-Pakete, die standardisieren, wie Ihre Dienste mit der unterstützenden Infrastruktur verbunden werden. Sie sind mehr als nur Client-Bibliotheken – sie umfassen sofort einsatzbereite Integritätsprüfungen, Protokollierung, Ablaufverfolgung und konfigurierbare Ausfallsicherheit.

Redis

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddRedisDistributedCache("cache");

Das ist es. Die Verbindungszeichenfolge kommt vom AppHost über WithReference. Die Komponente registriert einen von Redis unterstützten IDistributedCache, wobei Gesundheitsprüfungen und OpenTelemetry-Instrumentierung bereits verkabelt sind.

Sie können Redis auch für das Ausgabe-Caching verwenden:

builder.AddRedisOutputCache("cache");

PostgreSQL mit Entity Framework Core

builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb", settings =>
{
    settings.DisableRetry = false;
});

Dadurch wird Ihr DbContext mit einer Verbindung zur im AppHost definierten catalogdb-Datenbank registriert. Es umfasst Verbindungspooling, Zustandsprüfungen und Wiederholungsrichtlinien.

RabbitMQ

builder.AddRabbitMQClient("messaging");

Registriert einen IConnection aus der RabbitMQ-Clientbibliothek, vollständig konfiguriert und auf Integrität überprüft.

Azure-Integrationen

Auch für Azure-Dienste verfügt Aspire über erstklassige Komponenten:

// Azure Blob Storage
builder.AddAzureBlobClient("blobs");

// Azure Service Bus
builder.AddAzureServiceBusClient("servicebus");

// Azure Key Vault
builder.AddAzureKeyVaultClient("secrets");
```Das Muster ist immer das gleiche: eine Zeile im AppHost zum Definieren der Ressource, eine Zeile im verbrauchenden Dienst zur Verwendung. Verbindungsdetails fließen automatisch.

## Das Entwickler-Dashboard

Das Aspire-Dashboard ist eine dieser Funktionen, die zunächst wie ein Nice-to-have erscheinen, bis Sie sie tatsächlich verwenden  dann können Sie sich die Arbeit ohne sie nicht mehr vorstellen.

Wenn Sie Ihren AppHost lokal ausführen, wird das Dashboard gestartet und bietet Ihnen Folgendes:

- **Ressourcenübersicht**  Alle Ihre Dienste und Infrastruktur auf einen Blick, mit Statusanzeigen
- **Strukturierte Protokolle**  Protokoll-Streaming in Echtzeit von jedem Dienst, filterbar und durchsuchbar
- **Verteilte Ablaufverfolgungen**  End-to-End-Anfrageverfolgungen über mehrere Dienste hinweg, visualisiert als Flammendiagramme
- **Metriken**  HTTP-Anfrageraten, Fehlerraten, Latenzen und benutzerdefinierte Metriken in Echtzeit
- **Konsolenprotokolle**  Raw stdout/stderr von jedem Container und Projekt

Besonders wertvoll ist das verteilte Tracing. Wenn eine Anfrage Ihr Frontend erreicht, durch die Katalog-API fließt, Redis und PostgreSQL berührt, sehen Sie die gesamte Kette mit dem Timing für jeden Hop. Kein Rätselraten mehr, wo der Engpass liegt.

Ich fand das Dashboard beim Debuggen am nützlichsten. Anstatt mehrere Terminalfenster zu verfolgen oder zwischen Protokolldateien zu springen, befindet sich alles an einem Ort mit Korrelations-IDs, die verwandte Ereignisse dienstübergreifend verknüpfen.

Das Dashboard ist auch als eigenständiger Container verfügbar, sodass Sie es in Staging-Umgebungen oder CI-Pipelines verwenden können:

```bash
docker run --rm -p 18888:18888 \
  mcr.microsoft.com/dotnet/aspire-dashboard:latest

Bereitstellung

Aspire hilft bei der Entwicklung, aber wie sieht es mit der Produktion aus? Hier wird es praktisch.

Azure-Container-Apps

Der einfachste Bereitstellungspfad ist Azure Container Apps (ACA), der erstklassige Aspire-Unterstützung bietet. Sie können die Bereitstellung direkt über die Azure Developer CLI durchführen:

azd init
azd up

Der Befehl azd init erkennt Ihren Aspire AppHost und generiert die erforderliche Infrastruktur als Code. azd up stellt alles bereit – Containerregistrierung, Container-Apps, Datenbanken, Redis-Instanzen – basierend auf Ihrer AppHost-Topologie.

Ihr AppHost wird im Wesentlichen zu Ihrem Bereitstellungsmanifest. Derselbe Code, der „Katalog-API hängt von PostgreSQL und Redis ab“ definiert, steuert die Infrastrukturbereitstellung.

Kubernetes

Für Kubernetes-Bereitstellungen generiert Aspire Manifeste nicht direkt, aber Ihre AppHost-Topologie wird sauber den Kubernetes-Ressourcen zugeordnet. Das Community-Tool aspirate (Aspir8) kann Helm-Charts oder Kubernetes-Manifeste aus Ihrem AppHost generieren:

dotnet tool install -g aspirate
aspirate generate
aspirate apply

Überlegungen zur Bereitstellung

Ein paar Dinge, die Sie beachten sollten:- Verbindungszeichenfolgen ändern sich zwischen Umgebungen. Lokal startet Aspire Container und fügt Verbindungszeichenfolgen automatisch ein. In der Produktion verweisen Sie auf verwaltete Dienste. Aspire verwendet die standardmäßige .NET-Konfiguration, sodass Umgebungsvariablen und Azure Key Vault wie erwartet funktionieren.

  • Der AppHost wird nicht in der Produktion ausgeführt. Es handelt sich um ein Entwicklungs- und Bereitstellungs-Orchestrierungstool. In der Produktion werden Ihre Dienste unabhängig ausgeführt und über Umgebungsvariablen und Orchestratoren konfiguriert.
  • Infrastrukturressourcen werden zu verwalteten Diensten. Ihr lokaler PostgreSQL-Container wird zur Azure-Datenbank für PostgreSQL. Ihr lokaler Redis-Container wird zu Azure Cache for Redis. Der konsumierende Code ändert sich nicht.

Tipps aus der Praxis

Nachdem wir Aspire eine Zeit lang in der Produktion betrieben haben, sind hier die Lektionen, die uns Zeit gespart haben:

1. Verwenden Sie benutzerdefinierte Ressourcenlebenszyklusprüfungen

Verlassen Sie sich nicht nur auf den Container-Start, um festzustellen, ob eine Ressource bereit ist. PostgreSQL akzeptiert möglicherweise TCP-Verbindungen, bevor es tatsächlich bereit ist, Abfragen zu bedienen.

var postgres = builder.AddPostgres("postgres")
    .AddDatabase("catalogdb")
    .WithHealthCheck();

Aspire kann Integritätsprüfungen für Ressourcen durchführen und abhängige Dienste zurückhalten, bis sie tatsächlich bereit sind.

2. Gemeinsame Muster in Erweiterungen extrahieren

Wenn mehrere Dienste ähnliche Konfigurationen teilen, erstellen Sie Erweiterungsmethoden:

public static class AppHostExtensions
{
    public static IResourceBuilder<ProjectResource> AddWorkerService(
        this IDistributedApplicationBuilder builder,
        string name,
        IResourceBuilder<IResourceWithConnectionString> db,
        IResourceBuilder<IResourceWithConnectionString> messaging)
    {
        return builder.AddProject<Projects.WorkerService>(name)
            .WithReference(db)
            .WithReference(messaging)
            .WithReplicas(3);
    }
}

3. Nutzen Sie WithReplicas für Lasttests

var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReplicas(5);

WithReplicas startet mehrere Instanzen eines Dienstes lokal. Dies eignet sich hervorragend zum Testen von Lastausgleich, Parallelitätsfehlern und verteiltem Caching-Verhalten ohne Bereitstellung in einem Cluster.

4. Verwenden Sie Parameter für sensible Werte

Kodieren Sie Anmeldeinformationen nicht fest, auch nicht für die lokale Entwicklung:

var dbPassword = builder.AddParameter("db-password", secret: true);

var postgres = builder.AddPostgres("postgres", password: dbPassword)
    .AddDatabase("catalogdb");

Bei lokaler Ausführung fragt Aspire nach dem Wert oder liest ihn aus den Benutzergeheimnissen. In CI/CD kommt es aus Umgebungsvariablen.

5. Integrationstests werden trivial

Aspire enthält ein Testpaket, das das Integrationstesten verteilter Apps bemerkenswert einfach macht:

[Fact]
public async Task CatalogApiReturnsProducts()
{
    var appHost = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.MyCloudApp_AppHost>();

    await using var app = await appHost.BuildAsync();
    await app.StartAsync();

    var httpClient = app.CreateHttpClient("catalog-api");
    var response = await httpClient.GetAsync("/api/products");

    response.EnsureSuccessStatusCode();

    var products = await response.Content
        .ReadFromJsonAsync<List<Product>>();
    Assert.NotEmpty(products);
}

Dadurch wird Ihre gesamte verteilte Anwendung hochgefahren – einschließlich Datenbanken und Nachrichtenbrokern –, Ihr Test wird dagegen ausgeführt und sie wird heruntergefahren. Echte Integrationstests mit echter Infrastruktur in Ihrer CI-Pipeline. Keine Verspottungen.

6. Überwachen Sie die Ressourcennutzung lokal

Behalten Sie beim lokalen Betrieb mehrerer Container den Ressourcenverbrauch im Auge. Eine PostgreSQL-, Redis- und RabbitMQ-Instanz mit Verwaltungsoberfläche kann leicht 2–3 GB RAM verbrauchen. Wenn Sie einen eingeschränkten Computer verwenden, sollten Sie die Verwendung einfacherer Ressourcenkonfigurationen in Betracht ziehen:

var redis = builder.AddRedis("cache")
    .WithContainerRuntimeArgs("--memory=256m");

Fazit

.NET Aspire hat die Art und Weise, wie ich verteilte Anwendungen erstelle, grundlegend verändert. Nicht, weil es revolutionäre Konzepte einführt – Gesundheitschecks, OpenTelemetry und Container-Orchestrierung sind nicht neu. Sondern weil es sie zur Standardeinstellung macht. Es nimmt die hundert kleinen Entscheidungen, die Sie normalerweise treffen müssten, implementiert sie mit sinnvollen Standardeinstellungen und ermöglicht es Ihnen, sie bei Bedarf zu überschreiben.Das AppHost-Muster ist meiner Meinung nach der größte Gewinn. Wenn Ihre verteilte Anwendungstopologie als Code ausgedrückt wird – und nicht über Docker Compose-Dateien, Kubernetes-Manifeste und README-Dokumente verstreut ist –, wird das System verständlich. Neue Teammitglieder können Program.cs im AppHost öffnen und die gesamte Architektur in wenigen Minuten verstehen.

Wenn Sie verteilte Anwendungen mit .NET erstellen, sollten Sie sich Aspire ernsthaft ansehen. Beginnen Sie mit der Vorlage aspire-starter, erkunden Sie das Dashboard und übernehmen Sie nach und nach die Komponenten, wenn Sie sie benötigen. Sie müssen nicht gleich vom ersten Tag an aufs Ganze gehen – Aspire ist von Natur aus additiv.

Die Zeiten, in denen Sie Ihren ersten Sprint mit der Verkabelung der Infrastruktur-Grundbausteine ​​verbrachten, sind vorbei. Überlassen Sie Aspire die Installation, damit Sie sich auf das Wesentliche konzentrieren können: Ihre Anwendung.