.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.json dispersés
  • Pas de vue d'ensemble des logs et des traces distribuées
  • Des fichiers docker-compose.yml qui 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:#fff

Mon 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 dynamiquement

L'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 --> KV

Matrice 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:#fff

4. 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 :

  1. Déclare 5 microservices avec leurs ports HTTP et HTTPS
  2. Établit le maillage complet des dépendances inter-services
  3. Enregistre 4 applications frontales avec exposition externe
  4. Injecte automatiquement les URLs de chaque service dans les variables d'environnement des consommateurs
  5. 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é :

Image


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 endpoints

Implé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 endpoints

7. 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, 53

8. 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 & D2

Adaptation 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 & R9

Le 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_CI

Dockerfiles 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 F5 au 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:2px

L'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.