.NET Aspire 13 : Comment orchestrer 5 microservices et 4 frontaux dans une solution .NET 10
.NET Aspire 13 transforme la façon dont on développe, débogue et déploie des architectures distribuées. Dans cet article, nous décortiquons comment la solution StockAsso — 5 microservices métier, 4 applications frontales, du SQL Server, du Service Bus Azure et du Key Vault — est orchestrée par un unique fichier Program.cs de moins de 90 lignes.
1. Le problème : la complexité des architectures distribuées
Quand on développe un monolithe, tout est simple : un seul projet, un seul F5, un seul port. Mais dès qu'on passe à une architecture microservices, les problèmes se multiplient :
- 9 projets à démarrer en parallèle pour tester localement
- Des URLs et ports en dur dans des fichiers
appsettings.jsondispersés - Pas de vue d'ensemble des logs et des traces distribuées
- Des fichiers
docker-compose.ymlqui font 300+ lignes et deviennent impossibles à maintenir - Des health checks implémentés différemment dans chaque service
- Aucune standardisation de l'observabilité (certains services ont des métriques, d'autres non)
C'est exactement le scénario de StockAsso, une plateforme de gestion de stock pour associations.
Mon premier orchestrateur : ClustIIS
Avant de découvrir Aspire, j'avais développé mon propre orchestrateur, ClustIIS, pour résoudre ce problème. ClustIIS me permettait de déployer localement les différents services en mode self-hosted, en simulant une topologie de production sur ma machine de développement.
Sur le papier, ça fonctionnait. En pratique, c'était un cauchemar au quotidien :
- À chaque modification de code, même mineure, il fallait recompiler et redéployer un ou plusieurs services manuellement
- Le cycle modifier → compiler → déployer → tester prenait plusieurs minutes à chaque itération
- Si la modification impactait un service partagé comme
CommonSvc, il fallait redéployer tous les services qui en dépendaient - Le temps de développement effectif était considérablement rallongé : on passait plus de temps à orchestrer qu'à coder
graph LR
subgraph "Cycle de développement avec ClustIIS"
A[✏️ Modifier le code] --> B[🔨 Recompiler le service]
B --> C[📦 Redéployer dans ClustIIS]
C --> D[🔄 Redémarrer les dépendances]
D --> E[🧪 Tester]
E --> A
end
subgraph "Cycle avec Aspire 13"
F[✏️ Modifier le code] --> G[🚀 Hot Reload / F5]
G --> H[🧪 Tester]
H --> F
end
style A fill:#F44336,color:#fff
style B fill:#F44336,color:#fff
style C fill:#F44336,color:#fff
style D fill:#F44336,color:#fff
style F fill:#4CAF50,color:#fff
style G fill:#4CAF50,color:#fffMon propre Service Discovery via Azure Service Bus
En parallèle de ClustIIS, j'avais également développé mon propre mécanisme de Service Discovery, un peu plus évolué que celui proposé aujourd'hui par Aspire. Le principe était simple mais puissant : chaque service self-hosted, au démarrage, s'annonçait lui-même via Azure Service Bus en publiant son adresse IP et le port qu'il exposait. Une registry centralisée collectait ces annonces et servait de point d'entrée unique pour résoudre les adresses des services.
sequenceDiagram
participant SVC as Microservice (self-hosted)
participant BUS as Azure Service Bus
participant REG as Registry centralisée
participant CLI as Service consommateur
SVC->>BUS: Publish "Je suis CommonSvc sur 10.0.1.5:33301"
BUS->>REG: Notification d'enregistrement
REG->>REG: Mise à jour de la table des services
CLI->>REG: "Où est CommonSvc ?"
REG-->>CLI: "10.0.1.5:33301"
Note over SVC,REG: Si le service migre ou redémarre<br/>sur un autre port/IP, il se ré-annonce<br/>et la registry se met à jour dynamiquementL'avantage majeur de cette approche était la résolution dynamique : les adresses pouvaient changer à tout moment (redémarrage sur un autre port, migration vers un autre nœud), et il n'y avait qu'à interroger la registry pour obtenir les coordonnées à jour. Aucune URL en dur, aucune reconfiguration manuelle.
Le Service Discovery d'Aspire 13 fonctionne différemment — les endpoints sont déclarés statiquement dans l'AppHost et injectés via des variables d'environnement — mais il couvre parfaitement le cas d'usage du développement local et du déploiement conteneurisé. La version actuelle me convient largement, et je suis convaincu qu'Aspire évoluera probablement vers ce type de fonctionnalités de découverte dynamique dans les prochaines versions. Et si ce n'est pas le cas, la nature extensible d'Aspire me permettra de les ajouter moi-même.
Le déclic Aspire
ClustIIS et mon Service Discovery maison m'ont servi pendant plusieurs années et m'ont appris énormément sur les problématiques d'orchestration distribuée. Mais quand .NET Aspire 13 est arrivé avec .Net 10, la différence a été immédiate : un F5 dans Visual Studio et tout démarre, avec Hot Reload pour itérer instantanément. Le temps perdu en redéploiements a tout simplement disparu.
C'est exactement le problème que .NET Aspire 13 résout, et bien plus encore.
2. Qu'est-ce que .NET Aspire 13 ?
.NET Aspire est un framework d'orchestration cloud-native pour applications .NET distribuées. La version 13, livrée avec .NET 10, apporte des améliorations majeures :
| Fonctionnalité | Description |
|---|---|
| Aspire.AppHost.Sdk 13.x | SDK dédié pour l'orchestration, remplace les anciens packages |
| Docker Compose natif | AddDockerComposeEnvironment() pour intégrer un docker-compose existant |
| Service Discovery v2 | Résolution automatique des endpoints entre services |
| OpenTelemetry intégré | Logs, traces et métriques configurés en une ligne |
| Health Checks unifiés | /health et /alive sur tous les services |
| Dashboard amélioré | Interface web pour visualiser l'état de tous les services |
| Hosting Azure natif | Packages pour SQL Server, Redis, Service Bus, Key Vault, Storage |
Le SDK Aspire 13 se déclare directement dans le .csproj de l'AppHost :
<Project Sdk="Aspire.AppHost.Sdk/13.1.0">
3. Architecture globale de StockAsso
Vue d'ensemble
graph TB
subgraph "🎯 Aspire AppHost"
AH[StockAsso.AppHost<br/>Orchestrateur Aspire 13]
end
subgraph "🌐 Applications Frontales"
ADMIN[AdminWebApp<br/>Blazor Server<br/>Administration Association]
APPRO[ApproWebApp<br/>Blazor Server<br/>Administration Fournisseur]
FRONT[FrontWebApp<br/>Blazor SSR<br/>Site Public Membres]
API[PublicApi<br/>API REST<br/>Accès Public]
end
subgraph "⚙️ Microservices"
COMMON[CommonSvc<br/>Port 33301/33311<br/>Services Communs]
ACCOUNT[AccountSvc<br/>Port 33302/33312<br/>Comptes & Auth]
CATALOG[CatalogSvc<br/>Port 33303/33313<br/>Catalogue Produits]
PURCHASE[PurchaseSvc<br/>Port 33304/33314<br/>Achats]
SALE[SaleSvc<br/>Port 33305/33315<br/>Ventes]
end
subgraph "🏗️ Infrastructure"
SQL[(SQL Server)]
BUS[Azure Service Bus]
KV[Azure Key Vault]
BLOB[Azure Blob Storage]
end
AH --> ADMIN & APPRO & FRONT & API
AH --> COMMON & ACCOUNT & CATALOG & PURCHASE & SALE
ADMIN & APPRO & FRONT & API --> COMMON & ACCOUNT & CATALOG & PURCHASE & SALE
COMMON & ACCOUNT & CATALOG & PURCHASE & SALE --> SQL
COMMON & ACCOUNT & CATALOG & PURCHASE & SALE --> BUS
COMMON & ACCOUNT & CATALOG & PURCHASE & SALE --> KVMatrice des dépendances inter-services
Chaque microservice peut communiquer avec tous les autres. Aspire 13 rend cette toile de dépendances triviale à configurer :
graph LR
COMMON[CommonSvc] <--> ACCOUNT[AccountSvc]
COMMON <--> CATALOG[CatalogSvc]
COMMON <--> PURCHASE[PurchaseSvc]
COMMON <--> SALE[SaleSvc]
ACCOUNT <--> CATALOG
ACCOUNT <--> PURCHASE
ACCOUNT <--> SALE
CATALOG <--> PURCHASE
CATALOG <--> SALE
PURCHASE <--> SALE
style COMMON fill:#4CAF50,color:#fff
style ACCOUNT fill:#2196F3,color:#fff
style CATALOG fill:#FF9800,color:#fff
style PURCHASE fill:#9C27B0,color:#fff
style SALE fill:#F44336,color:#fff4. Le cœur d'Aspire : l'AppHost
L'AppHost est le chef d'orchestre. C'est un projet .NET minimal qui décrit l'ensemble de l'architecture distribuée. Voici le Program.cs complet de StockAsso :
var builder = DistributedApplication.CreateBuilder(args);
// ═══════════════════════════════════════════
// Microservices avec leurs ports dédiés
// ═══════════════════════════════════════════
var commonApi = builder.AddProject<Projects.StockAsso_CommonSvc>("commonapi")
.WithHttpEndpoint(port: 33301, name: "http")
.WithHttpsEndpoint(port: 33311, name: "https");
var accountApi = builder.AddProject<Projects.StockAsso_AccountSvc>("accountapi")
.WithEndpoint(port: 33312, scheme: "https")
.WithEndpoint(port: 33302, scheme: "http");
var catalogApi = builder.AddProject<Projects.StockAsso_CatalogSvc>("catalogapi")
.WithEndpoint(port: 33313, scheme: "https")
.WithEndpoint(port: 33303, scheme: "http");
var purchaseApi = builder.AddProject<Projects.StockAsso_PurchaseSvc>("purchaseapi")
.WithEndpoint(port: 33314, scheme: "https")
.WithEndpoint(port: 33304, scheme: "http");
var saleApi = builder.AddProject<Projects.StockAsso_SaleSvc>("saleapi")
.WithEndpoint(port: 33315, scheme: "https")
.WithEndpoint(port: 33305, scheme: "http");
// ═══════════════════════════════════════════
// Maillage inter-services (chacun connaît les autres)
// ═══════════════════════════════════════════
accountApi.WithReference(catalogApi)
.WithReference(commonApi)
.WithReference(purchaseApi)
.WithReference(saleApi);
catalogApi.WithReference(accountApi)
.WithReference(commonApi)
.WithReference(purchaseApi)
.WithReference(saleApi);
// ... même pattern pour commonApi, purchaseApi, saleApi
// ═══════════════════════════════════════════
// Applications frontales (exposées à l'extérieur)
// ═══════════════════════════════════════════
builder.AddProject<Projects.StockAsso_AdminWebApp>("adminwebapp")
.WithExternalHttpEndpoints()
.WithReference(accountApi)
.WithReference(catalogApi)
.WithReference(commonApi)
.WithReference(purchaseApi)
.WithReference(saleApi);
builder.AddProject<Projects.StockAsso_ApproWebApp>("approwebapp")
.WithExternalHttpEndpoints()
.WithReference(accountApi)
.WithReference(catalogApi)
.WithReference(commonApi)
.WithReference(purchaseApi)
.WithReference(saleApi);
builder.AddProject<Projects.StockAsso_FrontWebApp>("frontwebapp")
.WithExternalHttpEndpoints()
.WithReference(accountApi)
.WithReference(catalogApi)
.WithReference(commonApi)
.WithReference(purchaseApi)
.WithReference(saleApi);
builder.AddProject<Projects.StockAsso_PublicApi>("publicapi")
.WithExternalHttpEndpoints()
.WithReference(accountApi)
.WithReference(catalogApi)
.WithReference(commonApi)
.WithReference(purchaseApi)
.WithReference(saleApi);
// ═══════════════════════════════════════════
// Intégration Docker Compose existant
// ═══════════════════════════════════════════
builder.AddDockerComposeEnvironment("docker");
await builder.Build().RunAsync();
Ce que fait ce fichier
En moins de 90 lignes, ce fichier :
- Déclare 5 microservices avec leurs ports HTTP et HTTPS
- Établit le maillage complet des dépendances inter-services
- Enregistre 4 applications frontales avec exposition externe
- Injecte automatiquement les URLs de chaque service dans les variables d'environnement des consommateurs
- Intègre un environnement Docker Compose existant
Le fichier projet de l'AppHost
<Project Sdk="Aspire.AppHost.Sdk/13.1.0">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- Packages Aspire pour les ressources Azure -->
<PackageReference Include="Aspire.Hosting.Azure.ServiceBus" Version="13.1.1" />
<PackageReference Include="Aspire.Hosting.Azure.Storage" Version="13.*" />
<PackageReference Include="Aspire.Hosting.Azure.KeyVault" Version="13.*" />
<PackageReference Include="Aspire.Hosting.Docker" Version="13.1.1-preview.1.26105.8" />
<PackageReference Include="Aspire.Hosting.SqlServer" Version="13.*" />
<PackageReference Include="Aspire.Hosting.Redis" Version="13.*" />
</ItemGroup>
<ItemGroup>
<!-- Références vers TOUS les projets orchestrés -->
<ProjectReference Include="..\Clients\StockAsso.AdminWebApp\StockAsso.AdminWebApp.csproj" />
<ProjectReference Include="..\Clients\StockAsso.ApproWebApp\StockAsso.ApproWebApp.csproj" />
<ProjectReference Include="..\Clients\StockAsso.FrontWebApp\StockAsso.FrontWebApp.csproj" />
<ProjectReference Include="..\Clients\StockAsso.PublicApi\StockAsso.PublicApi.csproj" />
<ProjectReference Include="..\MicroServices\StockAsso.AccountSvc\StockAsso.AccountSvc.csproj" />
<ProjectReference Include="..\MicroServices\StockAsso.CatalogSvc\StockAsso.CatalogSvc.csproj" />
<ProjectReference Include="..\MicroServices\StockAsso.CommonSvc\StockAsso.CommonSvc.csproj" />
<ProjectReference Include="..\MicroServices\StockAsso.PurchaseSvc\StockAsso.PurchaseSvc.csproj" />
<ProjectReference Include="..\MicroServices\StockAsso.SaleSvc\StockAsso.SaleSvc.csproj" />
</ItemGroup>
</Project>
Voici ce que ça donne une fois démarré :
5. Les ServiceDefaults : convention over configuration
Le projet StockAsso.ServiceDefaults est le socle commun partagé par tous les services. Marqué <IsAspireSharedProject>true</IsAspireSharedProject>, il fournit une méthode d'extension unique que chaque service appelle :
// Dans chaque Program.cs de chaque service :
builder.AddServiceDefaults();
Cette unique ligne active :
mindmap
root((AddServiceDefaults))
Logging
Filtrage Auth logs
OpenTelemetry Logging
Formatted Messages
Scopes inclus
OpenTelemetry
Traces
ASP.NET Core
HttpClient
Entity Framework Core
MediatR Handlers
Blazor Components
Métriques
ASP.NET Core
HttpClient
Runtime .NET
Blazor Circuits
Export OTLP
Health Checks
/health — global
/alive — liveness
Service Discovery
Résolution auto des endpointsImplémentation concrète
public static IHostApplicationBuilder AddServiceDefaults(
this IHostApplicationBuilder builder)
{
// 1. Configuration du logging (filtrage des logs auth)
builder.ConfigureLogging();
// 2. OpenTelemetry : logs + métriques + traces
builder.ConfigureOpenTelemetry();
// 3. Health checks standardisés
builder.AddDefaultHealthChecks();
// 4. Service Discovery pour la résolution inter-services
builder.Services.AddServiceDiscovery();
return builder;
}
Configuration OpenTelemetry détaillée
public static IHostApplicationBuilder ConfigureOpenTelemetry(
this IHostApplicationBuilder builder)
{
// Logs structurés
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
logging.ParseStateValues = true;
});
// Nom du service résolu dynamiquement
var serviceName = builder.Configuration["OTEL_SERVICE_NAME"]
?? builder.Configuration["StockAssoApi:ServiceName"]
?? builder.Environment.ApplicationName;
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(
serviceName: serviceName,
serviceVersion: "1.0.0"))
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("Microsoft.AspNetCore.Components")
.AddMeter("Microsoft.AspNetCore.Components.Lifecycle")
.AddMeter("Microsoft.AspNetCore.Components.Server.Circuits");
})
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation(options =>
{
options.SetDbStatementForText = true;
})
.AddSource("Microsoft.AspNetCore.Components")
.AddSource("Microsoft.AspNetCore.Components.Server.Circuits")
.AddSource("MediatR.Handlers");
});
return builder;
}
Packages du ServiceDefaults
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.3.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.3.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.12.0-beta.2" />
</ItemGroup>
6. Service Discovery automatique
C'est l'un des plus gros avantages d'Aspire 13. Quand on écrit :
builder.AddProject<Projects.StockAsso_AdminWebApp>("adminwebapp")
.WithReference(catalogApi);
Aspire injecte automatiquement les variables d'environnement suivantes dans AdminWebApp :
services__catalogapi__https__0=https://localhost:33313
services__catalogapi__http__0=http://localhost:33303
Avec Aspire : la sérénité
// Plus aucune URL en dur. Le Service Discovery résout tout.
// L'AppHost est la seule source de vérité.
Flux de résolution
sequenceDiagram
participant AH as AppHost
participant AD as AdminWebApp
participant CS as CatalogSvc
AH->>AD: Injecte services__catalogapi__https__0
AD->>AD: Service Discovery résout "catalogapi"
AD->>CS: GET https://catalogapi:33313/api/products
CS-->>AD: 200 OK [products...]
Note over AH: L'AppHost est la source<br/>de vérité unique pour<br/>tous les endpoints7. Observabilité intégrée : OpenTelemetry clé en main
Aspire 13 fournit un dashboard web qui collecte automatiquement les données OpenTelemetry de tous les services. Sans aucune infrastructure supplémentaire en développement.
Les 3 piliers de l'observabilité
graph LR
subgraph "Pilier 1 : Logs"
L1[Logs structurés]
L2[Scopes inclus]
L3[Filtrage intelligent]
end
subgraph "Pilier 2 : Traces"
T1[ASP.NET Core]
T2[HttpClient]
T3[Entity Framework]
T4[MediatR Handlers]
T5[Blazor Components]
end
subgraph "Pilier 3 : Métriques"
M1[Requêtes HTTP]
M2[Runtime .NET]
M3[Blazor Circuits]
M4[GC / Threads]
end
L1 & T1 & M1 --> OTLP[Export OTLP]
OTLP --> DASH[Dashboard Aspire]
OTLP --> PROD[Grafana / Azure Monitor<br/>en production]Traçage distribué en action
Quand un utilisateur navigue sur AdminWebApp, une requête peut traverser plusieurs services. Grâce à l'instrumentation OpenTelemetry, Aspire trace le parcours complet :
gantt
title Trace distribuée — Création d'une commande
dateFormat X
axisFormat %L ms
section AdminWebApp
Réception requête Blazor :a1, 0, 5
Validation formulaire :a2, 5, 10
section CatalogSvc
Vérification stock :c1, 10, 25
EF Core SELECT Products :c2, 12, 22
section PurchaseSvc
Création commande :p1, 25, 45
EF Core INSERT Order :p2, 27, 40
MediatR SaveOrderRequest :p3, 26, 42
section CommonSvc
Envoi notification email :n1, 45, 55
Azure Service Bus publish :n2, 47, 538. Docker Compose et Aspire
Je n'utilise pas (encore) Aspire pour creer mes fichier docker compose. Pour l'instant ils sont beaucoup trop complexes et tellement differents entre local, staging et prod que j'ai préféré les réaliser manuellement. Mais j'ai lu pas mal sur le sujet et l'équipe d'Aspire avance très vite.
Coexistence intelligente
graph TB
subgraph "Aspire AppHost"
direction TB
ORCH[Orchestrateur]
end
subgraph "Projets .NET gérés par Aspire"
P1[CommonSvc]
P2[AccountSvc]
P3[CatalogSvc]
P4[PurchaseSvc]
P5[SaleSvc]
P6[AdminWebApp]
P7[ApproWebApp]
P8[FrontWebApp]
P9[PublicApi]
end
subgraph "Conteneurs Docker Compose"
D1[SQL Server]
D2[Redis]
D3[Seq / Grafana]
D4[Autres dépendances]
end
ORCH -->|"AddProject()"| P1 & P2 & P3 & P4 & P5 & P6 & P7 & P8 & P9
ORCH -->|"AddDockerCompose()"| D1 & D2 & D3 & D4
P1 & P2 & P3 & P4 & P5 -.->|connexion| D1 & D2Adaptation Kestrel : mode standalone vs Aspire
Le StartupHelper de StockAsso détecte intelligemment si le service tourne sous Aspire ou de façon autonome :
// Ne configurer Kestrel manuellement que si on n'est pas dans un contexte Aspire
// Aspire gère automatiquement les endpoints via AddServiceDefaults()
var isAspireHosted = System.Diagnostics.Debugger.IsAttached;
if (!isAspireHosted)
{
Console.WriteLine("Configuration Kestrel manuelle activée (mode standalone)");
builder.WebHost.ConfigureKestrel(cfg =>
{
cfg.ListenAnyIP(apiSettings.HttpPort, options =>
{
options.Protocols = HttpProtocols.Http1;
});
cfg.ListenAnyIP(apiSettings.HttpsPort, options =>
{
options.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
options.UseHttps(apiSettings.TlsCertificatePfxPath);
});
});
}
else
{
Console.WriteLine("Configuration Kestrel gérée par Aspire");
}
Pour l'instant je n'ai pas trouvé de moyen efficace pour configurer Aspire via AppHost et faire de telle sorte que les services discutent entre eux en https avec mon propre certificat. J'ai gardé ma solution maison.
9. Les 10 avantages concrets pour une architecture à 9+ projets
1. 🚀 Un seul F5 pour tout démarrer
Avant Aspire, il fallait ouvrir 9 terminaux ou configurer un profil multi-startup complexe. Maintenant :
dotnet run --project src/StockAsso.AppHost
9 services démarrés en parallèle, avec un dashboard pour tout superviser, et vraiment très rapidement, j'apprecie beaucoup l'ordre de démarrage avec les dépendances, c'est très pratique.
2. 🔗 Service Discovery sans configuration
Aspire injecte les endpoints via des variables d'environnement standardisées.
3. 📊 Observabilité gratuite
OpenTelemetry configuré une seule fois dans ServiceDefaults, activé automatiquement dans les 9 projets. Logs, traces et métriques unifiés. Sans rien changer en staging et prod j'utilise une stack Grafana, je n'ai plus qu'a indiquer l'url sans aucune modification.
4. 🏥 Health Checks uniformes
Tous les services exposent /health et /alive avec la même implémentation. Le dashboard Aspire les surveille en temps réel.
5. 📡 Résilience HTTP intégrée
Le package Microsoft.Extensions.Http.Resilience dans ServiceDefaults fournit des politiques de retry, circuit breaker et timeout pour tous les appels inter-services.
6. 📦 Cohérence des versions
Le SDK Aspire 13.1.0 et les packages associés garantissent la compatibilité entre tous les composants. Plus de conflits de versions entre services.
7. 🌐 Transition dev → prod transparente
La même topologie décrite dans l'AppHost sert de blueprint pour le déploiement en production. Les variables d'environnement services__* sont le contrat entre dev et prod.
8. Dashboard Aspire : le centre de contrôle
Au lancement de l'AppHost, Aspire ouvre automatiquement un dashboard web accessible sur un port local. Ce dashboard affiche :
graph TB
subgraph "Dashboard Aspire"
direction TB
RES[📋 Resources<br/>État de chaque service]
LOG[📝 Logs<br/>Logs structurés consolidés]
TRA[🔍 Traces<br/>Traces distribuées]
MET[📈 Métriques<br/>Compteurs & histogrammes]
end
subgraph "Resources affichées"
R1["commonapi — Running ✅"]
R2["accountapi — Running ✅"]
R3["catalogapi — Running ✅"]
R4["purchaseapi — Running ✅"]
R5["saleapi — Running ✅"]
R6["adminwebapp — Running ✅"]
R7["approwebapp — Running ✅"]
R8["frontwebapp — Running ✅"]
R9["publicapi — Running ✅"]
end
RES --> R1 & R2 & R3 & R4 & R5 & R6 & R7 & R8 & R9Le dashboard permet de :
- Voir les logs de tous les services dans une vue unifiée, avec filtrage par service
- Inspecter les traces distribuées pour comprendre le parcours d'une requête à travers les microservices
- Consulter les métriques de chaque service (requêtes/sec, latence, utilisation mémoire)
- Vérifier les endpoints et ports de chaque service
- Accéder aux health checks en un clic
9. Déploiement en production
Structure de déploiement
StockAsso utilise une stratégie de déploiement multi-environnement avec Docker :
graph TB
subgraph "Développement"
DEV_AH[Aspire AppHost<br/>F5 local]
DEV_DC[docker-compose-dev.yml]
end
subgraph "Staging"
STG_DC[docker-compose-staging.yml]
STG_CI[Azure DevOps Pipeline]
end
subgraph "Production"
PROD_DC[docker-compose-prod.yml]
PROD_CI[Azure DevOps Pipeline]
end
DEV_AH --> STG_CI
DEV_DC -.-> STG_DC -.-> PROD_DC
STG_CI --> PROD_CIDockerfiles optimisés
Chaque service dispose d'un Dockerfile multi-stage qui utilise une image SDK de base préconfigurée :
# Image de base .NET 10
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
# Build avec SDK préconfiguré (NuGet authentifié)
FROM stockasso-sdk-base:latest AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
# Restauration des packages (couche cachée)
COPY ["src/MicroServices/StockAsso.CommonSvc/StockAsso.CommonSvc.csproj", "..."]
# ... autres .csproj
RUN dotnet restore
# Build
COPY . .
RUN dotnet build -c $BUILD_CONFIGURATION -o /app/build
# Publish
FROM build AS publish
RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# Image finale minimale
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "StockAsso.CommonSvc.dll"]
13. Conclusion
.NET Aspire 13 n'est pas simplement un outil de plus dans l'écosystème .NET. C'est un changement de paradigme pour le développement d'applications distribuées.
Pour StockAsso, avec ses 5 microservices (Common, Account, Catalog, Purchase, Sale), ses 4 applications frontales (AdminWebApp, ApproWebApp, FrontWebApp, PublicApi) et ses dépendances Azure (SQL Server, Service Bus, Key Vault, Blob Storage), Aspire 13 a transformé :
- L'expérience développeur : un
F5au lieu de 9 terminaux - L'observabilité : OpenTelemetry activé sur 100% des services en une ligne
graph LR
A[1 AppHost] --> B[9 Services]
B --> C[Dashboard Unifié]
C --> D[Production Ready]
style A fill:#4CAF50,color:#fff,stroke-width:2px
style B fill:#2196F3,color:#fff,stroke-width:2px
style C fill:#FF9800,color:#fff,stroke-width:2px
style D fill:#9C27B0,color:#fff,stroke-width:2pxL'adoption d'Aspire 13 sur StockAsso a été progressive : d'abord le ServiceDefaults pour standardiser l'observabilité, puis l'AppHost pour l'orchestration locale, et enfin l'intégration Docker Compose pour le pont vers la production. Cette approche incrémentale est la clé pour migrer une solution existante sans tout casser. Au global, j'y ai passé 2 jours entre le tout début et la mise à jour en prod.
Article rédigé dans le contexte de la solution StockAsso, plateforme de gestion de stock pour associations, développée en .NET 10 avec Blazor Server, Blazor SSR et une architecture microservices.
Si vous voulez plus d'infos, contactez-moi, je répondrai avec plaisir.