Aspire: Schnellere Entwicklung, einfacherer Start

Wie .NET Aspire das lokale Setup verteilter .NET-Anwendungen vereinfacht und das Onboarding spürbar beschleunigt
Mark N.
06/2026

TL;DR

Verteilte .NET-Anwendungen lokal aufzusetzen kostet oft Zeit: Container, Connection-Strings, Startreihenfolge und Observability werden meist von Hand verdrahtet. Aspire beschreibt die gesamte Topologie als versionierbaren C#-Code im AppHost. Datenbanken und Caches starten automatisch, Service Discovery und OpenTelemetry sind ab der ersten Zeile aktiv, und ein zentrales Dashboard bündelt Logs, Traces und Metriken. Das App-Modell lässt sich zudem per Aspire.Hosting.Testing als Ganzes integrationstesten. Ergebnis: schnelleres Onboarding, reproduzierbare Umgebungen und weniger „Bei mir lief es doch". Für Teams, die regelmäßig verteilte .NET-Projekte lokal betreiben, lohnt ein genauer Blick.

„Funktioniert bei mir doch" - und wie Aspire diesen Satz endlich überflüssig macht.

Wie Aspire das Local Development entlastet und das Onboarding beschleunigt

Zwei Dinge wird ein Entwickler im Laufe seines Lebens garantiert hören: Das eine sind Datenstrukturen und Algorithmen - das andere ist der Satz „Bei mir lief es doch!", kurz nachdem jemand auf Produktion deployed hat.

Gerne wird das als humorvolles Geplänkel abgetan, als Eigenart der Maschinen und ihrer Spezifikationen: ein anderes Betriebssystem, eine vergessene SDK-Version, die nie jemand aktualisiert hat, weil sich die „Never change a running system"-Mentalität tief in unser Ausreden-Repertoire eingegraben hat. Auf lange Sicht kostet genau das aber erstaunlich viel Zeit - und Zeit ist bekanntlich die einzige Währung, die man mit Geld nicht kaufen kann. Jede Minute, die hier und da fürs Debuggen draufgeht oder dafür, eine neue Kollegin ins laufende Projekt einzuarbeiten, summiert sich.

Egal, ob reines .NET-Projekt oder ein bunter Mix aus JavaScript-Frontend und Python-Backend: Der Kern der meisten Frustration lag (zumindest in der Praxis) immer darin, wie einfach – oder eben nicht – sich ein Projekt lokal aufsetzen und bearbeiten lässt. Ist die Umgebung einmal eingerichtet, läuft sie ja. Nur passt eben nicht alles sauber in eine Projektstruktur: Abhängigkeiten wie Datenbanken oder Caching-Provider gehören zum Projekt dazu, leben aber außerhalb davon.

Mit Containerisierung und Orchestrierung über Docker und Docker Compose wurde das über die Jahre deutlich einfacher – ein besonders willkommener Schritt für Windows-Rechner, auf denen WSL (Windows Subsystem for Linux) zunehmend Fuß fasst, während Hosting-Anbieter auf robuste, enterprise-erprobte und überwiegend Linux-basierte Server setzen. Systeme ließen sich leichter aufsetzen, aber sofern einem nicht ein DevOps-Kollege freundlicherweise ein fertiges Docker-Compose-Skript hinterlegt hatte, musste man die lokale Umgebung doch wieder von Hand konfigurieren. Und damit blieb genau die Fehlerquelle, die sich zum klassischen „Aber bei mir lief es doch!" auswächst.

.NET Aspire (seit Version 13 schlicht Aspire) will das ändern – und zwar nicht nur für .NET-Projekte, sondern auch für die eben erwähnten mehrsprachigen Setups. Man darf sich Aspire als den Sandkasten vorstellen, der bereits alle Bauanleitungen und die spaßigen Integrationen mitbringt, damit sämtliches Spielzeug automatisch „einfach funktioniert" und harmonisch zusammenspielt – ganz ohne, dass man seine lokale Umgebung vorab präparieren muss, nur um einen Bug an einem laufenden System zu testen.

Erreicht wird das, indem Aspire die Docker-Engine zur Orchestrierung nutzt und die gesamte Konfiguration in einem C#-Projekt bündelt (mit .NET 10 sogar als einzelnes file-based Projekt). Jede notwendige Einstellung ist damit vorkonfiguriert und einsatzbereit – starten lässt sich das Ganze mit einem einzigen CLI-Befehl wie aspire run.

Dieser Artikel demonstriert die Möglichkeiten von Aspire rund um Orchestrierung, zeigt einige der mitgelieferten Features und wie man sie in bestehende .NET-Projekte integriert, um Onboarding und Local Development spürbar zu beschleunigen. Der vollständige Quellcode samt aller hier gezeigten Snippets liegt öffentlich auf GitHub: github.com/tavanuka/Aspire.Sample.

Das Problem: Orchestrierung von Hand ist undankbar

Eine moderne Cloud-native-Anwendung besteht selten aus einem einzelnen Prozess. In unserem Beispiel sind es bereits fünf bewegliche Teile:

  • ein Blazor-Server-Frontend (Sample.Web),
  • eine REST-API auf Basis von FastEndpoints (Sample.ApiService),
  • ein dedizierter Migration-Worker, der die Datenbank anlegt und befüllt,
  • eine PostgreSQL-Datenbank und
  • ein Redis-Cache.

Von Hand bedeutet das: Container starten, Reihenfolge beachten (die API darf erst los, wenn die Migrationen durch sind), Ports vergeben, Connection-Strings durchreichen, und am Ende noch irgendwie nachvollziehen, welcher der fünf Prozesse gerade beschlossen hat, nicht zu funktionieren. Das ist kein intellektuell anspruchsvolles Problem – es ist nur lästig, fehleranfällig und für jede neue Person im Team aufs Neue zu erklären.

Der Lösungsansatz: Ein App-Modell als Single Source of Truth

Aspire dreht den Spieß um. Statt einer Sammlung von Skripten und einer README mit zwölf Schritten beschreibt man die gesamte Topologie der Anwendung in C# – in einem eigenen Projekt, dem AppHost. Dieses Projekt ist der zentrale Einstiegspunkt: Man startet es, und Aspire kümmert sich um den Rest.

So sieht das komplette App-Modell unseres Beispiels aus – und ja, das ist tatsächlich alles:

1var builder = DistributedApplication.CreateBuilder(args);
2
3var postgres = builder.AddPostgres("postgres")
4    .WithDataVolume()
5    .WithPgWeb()
6    .WithLifetime(ContainerLifetime.Persistent);
7
8var coreDb = postgres.AddDatabase("core-db");
9
10var cache = builder.AddRedis("cache")
11    .WithClearCommand(); // Custom command that flushes the current cache
12
13// Migration service that separates the concern for population of the database and applying migrations.
14var migrationWorker = builder.AddProject<Projects.Sample_MigrationWorker>("migration-worker")
15    .WithReference(coreDb)
16    .WaitFor(coreDb);
17
18var apiService = builder.AddProject<Projects.Sample_ApiService>("apiservice")
19    .WithReference(migrationWorker)
20    .WaitForCompletion(migrationWorker)  // Ensures database is populated before starting the service
21    .WithReference(coreDb)
22    .WaitFor(coreDb)
23    .WithHttpHealthCheck("/health");
24
25builder.AddProject<Projects.Sample_Web>("webfrontend")
26    .WithExternalHttpEndpoints()
27    .WithReference(cache)
28    .WaitFor(cache)
29    .WithReference(apiService)
30    .WaitFor(apiService);
31
32builder.Build().Run();

Lesen Sie diese Datei einmal von oben nach unten – und Sie haben das gesamte System verstanden. Genau das ist der Punkt. Ein paar Details, die mehr leisten, als sie auf den ersten Blick verraten:

  • AddPostgres / AddRedis ziehen die jeweiligen Container automatisch hoch. Niemand muss mehr ein docker run von Hand absetzen.
  • WithDataVolume() und WithLifetime(ContainerLifetime.Persistent) sorgen dafür, dass die Datenbank einen Neustart überlebt – kein erneutes Seeden bei jedem F5.
  • WithReference(...) verdrahtet Services miteinander. Aspire injiziert die passenden Connection-Strings und Service-URLs als Konfiguration in das Zielprojekt. Connection-Strings von Hand zusammensuchen entfällt damit ersatzlos.
  • WaitFor(...) und WaitForCompletion(...) modellieren die Startreihenfolge. Der Unterschied ist subtil, aber wichtig: WaitFor wartet, bis eine Ressource gesund ist; WaitForCompletion wartet, bis sie fertig durchgelaufen ist. Genau deshalb startet die API erst, wenn der Migration-Worker seine Arbeit abgeschlossen hat – die klassische „Tabelle existiert noch nicht"-Race-Condition ist damit deklarativ gelöst, statt mit einem hoffnungsvollen Thread.Sleep.

Service Discovery & Telemetrie – fast schon unfair beiläufig

Wie findet das Frontend nun die API? Nicht über einen hartkodierten Port, sondern über den logischen Namen aus dem App-Modell:

1builder.Services.AddRefitClient<IWeatherApiClient>()
2    .ConfigureHttpClient(client =>
3        // "https+http://" signalisiert: HTTPS bevorzugt, HTTP als Fallback.
4        client.BaseAddress = new("https+http://apiservice/api/weatherforecast"));

apiservice ist exakt der Name, unter dem die API im AppHost registriert ist. Die Auflösung übernimmt die Service Discovery von Aspire. Möglich macht das ein gemeinsames ServiceDefaults-Projekt, das jeder Service mit einem einzigen Aufruf einbindet – builder.AddServiceDefaults(). Darin stecken, ohne dass man sie einzeln verkabeln müsste: Service Discovery, ein Standard-Resilience-Handler (Retry, Timeout, Circuit Breaker), Health Checks und vollständige OpenTelemetry-Instrumentierung für Logs, Metriken und Traces. Observability ist hier also kein Projekt für nächstes Quartal, sondern Standard ab der ersten Zeile.

Ein API-Request aus Scalar mit zugehöriger Trace-Darstellung

Mehr als nur Start: Das Dashboard als Kommandozentrale

Startet man den AppHost, öffnet sich automatisch das Aspire-Dashboard – eine Weboberfläche, die alle Ressourcen, ihre Logs, verteilten Traces und Metriken an einer Stelle bündelt. Das übliche Jonglieren mit einem halben Dutzend Terminals reduziert sich auf einen einzigen Browser-Tab.

Das Schöne daran: Das Dashboard ist erweiterbar. In unserem Beispiel hängt am Redis-Cache ein eigener Button namens „Clear cache". Statt sich im laufenden Betrieb in einen Container einzuloggen und FLUSHALL zu tippen, klickt man im Dashboard. Realisiert wird das über eine Custom Command – und dank C#-13-Extension-Members liest sich die Registrierung erfreulich sauber:

1public static class RedisResourceBuilderExtensions
2{
3    extension(IResourceBuilder<RedisResource> builder)
4    {
5        public IResourceBuilder<RedisResource> WithClearCommand()
6        {
7            var commandOptions = new CommandOptions
8            {
9                UpdateState = OnUpdateResourceState,
10                IconName = "TrayItemRemove",
11                IconVariant = IconVariant.Filled
12            };
13
14            return builder.WithCommand("clear-cache", "Clear cache",
15                context => OnRunClearCacheCommandAsync(builder, context),
16                commandOptions);
17        }
18    }
19
20    private static async Task<ExecuteCommandResult> OnRunClearCacheCommandAsync(
21        IResourceBuilder<RedisResource> builder, ExecuteCommandContext context)
22    {
23        var cs = await builder.Resource.GetConnectionStringAsync()
24                 ?? throw new InvalidOperationException($"Unable to get the '{context.ResourceName}' connection string.");
25
26        await using var c = await ConnectionMultiplexer.ConnectAsync(cs);
27        await c.GetDatabase().ExecuteAsync("FLUSHALL");
28
29        return CommandResults.Success();
30    }
31}

Das UpdateState-Delegate ist dabei ein netter Detail-Fanatiker-Moment: Der Button ist nur dann aktiv, wenn der Cache laut Health-Status auch wirklich gesund ist. Man kann also nicht versehentlich einen Cache leeren, der gerade gar nicht erreichbar ist.

WithClearCommand wird verfügbar, sobald Redis läuft

In dieselbe Kerbe schlagen Kleinigkeiten wie WithPgWeb() (eine Web-UI für PostgreSQL, ebenfalls per Klick im Dashboard erreichbar) und die Einbindung einer Scalar-API-Referenz, die die OpenAPI-Endpunkte der API hübsch dokumentiert. Nichts davon ist welterschütternd – aber in Summe sind es genau die Reibungspunkte, die sonst täglich Zeit fressen.

Benutzerdefinierte Ressourcen-URL-Mappings

Graph-Ansicht der Ressourcenabhängigkeiten und -korrelationen

Vertrauen ist gut, Integrationstests sind besser

Ein deklaratives App-Modell hat einen willkommenen Nebeneffekt: Man kann es als Ganzes testen. Mit dem Paket Aspire.Hosting.Testing lässt sich der komplette Anwendungsgraph in einem Test hochfahren – inklusive Container. Im Beispiel-Repository kommt dafür xUnit v3 zum Einsatz; wer es lieber moderner und ganz ohne Attribut-Magie mag, kann genauso gut zu TUnit greifen – Aspire.Hosting.Testing ist bewusst Test-Framework-agnostisch und schreibt einem nichts vor.

1[Fact]
2public async Task GetWebResourceRootReturnsOkStatusCode()
3{
4    var cancellationToken = TestContext.Current.CancellationToken;
5
6    var appHost = await DistributedApplicationTestingBuilder
7        .CreateAsync<Projects.Sample_AppHost>(cancellationToken);
8
9    appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
10        clientBuilder.AddStandardResilienceHandler());
11
12    await using var app = await appHost.BuildAsync(cancellationToken)
13        .WaitAsync(DefaultTimeout, cancellationToken);
14    await app.StartAsync(cancellationToken).WaitAsync(DefaultTimeout, cancellationToken);
15
16    var httpClient = app.CreateHttpClient("webfrontend");
17    await app.ResourceNotifications
18        .WaitForResourceHealthyAsync("webfrontend", cancellationToken)
19        .WaitAsync(DefaultTimeout, cancellationToken);
20
21    var response = await httpClient.GetAsync("/", cancellationToken);
22
23    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
24}

Der Test spricht für sich: Dieselbe Projects.Sample_AppHost-Definition, die auch im Entwicklungsbetrieb läuft, wird hier in der Testumgebung gestartet. CreateHttpClient("webfrontend") liefert einen Client, der – wieder über den logischen Namen – direkt auf das Frontend zeigt. WaitForResourceHealthyAsync wartet geduldig, bis die Ressource gesund ist, bevor der erste Request abgeht. Keine geratenen Timeouts, kein „in der CI ist es halt manchmal zu langsam".

1[Theory]
2[InlineData("migration-worker", "core-db")]
3[InlineData("apiservice", "core-db")]
4[InlineData("apiservice", "migration-worker")]
5[InlineData("webfrontend", "cache")]
6[InlineData("webfrontend", "apiservice")]
7public async Task ResourceHasReferenceToExpectedResource(string resourceName, string referencedResourceName)
8{
9    // Arrange
10    var cancellationToken = TestContext.Current.CancellationToken;
11    var appHost = await DistributedApplicationTestingBuilder.CreateAsync<Projects.Sample_AppHost>(cancellationToken);
12
13    await using var app = await appHost.BuildAsync(cancellationToken);
14
15    var model = app.Services.GetRequiredService<DistributedApplicationModel>();
16
17    // Act
18    var resource = model.Resources.Single(r => r.Name == resourceName);
19    var references = resource.Annotations
20        .OfType<ResourceRelationshipAnnotation>()
21        .Select(a => a.Resource.Name);
22
23    // Assert
24    Assert.Contains(referencedResourceName, references);
25}

Weitere Testfälle und Integrationstests finden Sie im Quellcode-Repository im Projekt Sample.AppHost.Tests.

Voraussetzungen für das Projekt

Wer das Repository selbst ausprobieren möchte, braucht erfreulich wenig:

  • .NET 10 SDK – alle Projekte zielen auf net10.0.
  • Eine Container-Runtime – Docker Desktop oder Podman. Aspire nutzt sie, um PostgreSQL und Redis automatisch bereitzustellen.
  • Git, um das Repository zu klonen.
  • Optional die Aspire CLI. Seit Aspire 9 ist kein separates Workload mehr nötig – das Aspire.AppHost.Sdk (hier in Version 13.2.0) wird direkt als SDK referenziert.

Danach genügt im Wurzelverzeichnis ein einziger Befehl:

aspire run
# alternativ, ganz ohne CLI:
dotnet run --project Sample.AppHost

Das Dashboard öffnet sich von selbst, alle Container fahren hoch, und die Anwendung steht. Kein zwölfstufiges README-Ritual.

Fazit: Weniger Zeremonie, mehr Entwicklung

.NET Aspire löst kein Problem, das sich mit genügend Disziplin und einer langen README nicht auch von Hand lösen ließe. Aber genau darum geht es: Aspire nimmt einem die Disziplin ab, die ohnehin niemand gerne aufbringt. Die Topologie der Anwendung lebt an einer Stelle, in einer Sprache, die das Team bereits spricht. Container, Connection-Strings, Startreihenfolge und Observability sind keine Tribal-Knowledge-Themen mehr, sondern Code – versioniert, reviewbar und testbar.

Für das Onboarding ist das ein spürbarer Unterschied. „Klone das Repo, installiere das .NET-SDK und eine Container-Runtime, User Secrets einrichten, dann aspire run" ist eine Anleitung, die auf einen Bierdeckel passt und an einem Vormittag nicht scheitert. Der eingangs gefürchtete Satz „Bei mir lief es doch!" verliert dabei seinen Schrecken – denn auf allen Maschinen läuft jetzt nachweislich dasselbe Modell.

Die Einschätzung nach diesem Proof of Concept: Aspire ist kein Hochglanz-Selbstzweck, sondern angenehm pragmatisch. Es glänzt nicht durch das, was es hinzufügt, sondern durch das, was es einem erspart. Wer regelmäßig verteilte .NET-Anwendungen lokal startet, sollte sich das genauer ansehen.

Probieren Sie es aus: Das vollständige Beispiel finden Sie unter github.com/tavanuka/Aspire.Sample. Einmal klonen, einmal starten – und dann selbst entscheiden, ob die zwölf Terminals fehlen.

Referenzen

No items found.
Foto von Mark
Mark N.

Mehr zum Thema

Pfeil nach rechts (Verlinkung)
NuGet Showcase: Wie ein modernes .NET-Test-Framework Discovery, Parallelität und CI/CD beschleunigt.
06/2026

NuGet Showcase: Testen mit TUnit, die „New Generation" der Test-Framework-Alternativen

Blauer Pfeil nach rechts (Verlinkung)
LoRaWAN-Geofencing mit .NET 10 und Azure für sicherheitskritisches Pferde-Tracking.
06/2026

LoRaWAN-Geofencing mit .NET 10 und Azure: PoC für eine sicherheitskritische Tracking-Plattform

Blauer Pfeil nach rechts (Verlinkung)
06/2026

Validierung in Blazor: FluentValidation und Blazilla

Blauer Pfeil nach rechts (Verlinkung)
Performance-Optimierung in .NET
05/2026

Performance-Optimierung in modernen .NET Webanwendungen

Blauer Pfeil nach rechts (Verlinkung)
Devware GmbH verpflichtet sich, Ihre Privatsphäre zu schützen. Wir benötigen Ihre Kontaktinformationen, um Sie bezüglich unserer Produkte und Dienstleistungen zu kontaktieren. Mit Klick auf Absenden geben Sie sich damit einverstanden. Weitere Informationen finden Sie unter Datenschutz. Ihre Daten behandeln wir vertraulich. Versprochen.
Vielen Dank für Ihr Vertrauen.
Unser Team prüft Ihre Anfrage sorgfältig und meldet sich in der Regel innerhalb von 48 Stunden bei Ihnen zurück.
Falls es besonders eilig ist, erreichen Sie uns auch telefonisch:
+ 49 (0) 202 478 269 0.
Da ist etwas schief gegangen beim Absenden des Formulars.