Tous les projets
2026GitHub

Générateur OpenAPI pour ASP.NET Core

Générateur de DTOs et controllers à partir de spécifications OpenAPI, avec support pour les annotations de validation et les conventions ASP.NET Core.

C#ASP.NET CoreOpenAPICode Generation

Contexte

Sur Argon, un Micro-ERP en microservices monorepo, les services doivent communiquer entre eux. La première approche — générer les contrats OpenAPI depuis le code du serveur et les exporter — posait un problème structurel : il fallait build le service producteur pour que le consommateur puisse compiler. Dans un monorepo avec plusieurs services interdépendants, cela crée des dépendances circulaires de build impossibles à résoudre (pas forcément entre deux services, mais sur des chaînes de 4-5 services).

Lors d'un stage chez Lumiplan, j'ai découvert l'approche contract first : on écrit le contrat OpenAPI à la main, proprement, avec l'intention clairement exprimée, et les deux parties (serveur et client) l'implémentent indépendamment. Plus de "le serveur fait sa tambouille et le client s'adapte" — le contrat est la source de vérité, il existe avant le code.

Ce workflow m'a convaincu. Mais les générateurs C# disponibles (notamment openapi-generator-cli) ne me satisfaisaient pas : ils génèrent un projet entier avec Program.cs, Startup.cs et des dizaines de fichiers dont je n'ai pas besoin. Je voulais uniquement les DTOs et les classes de base abstraites pour les controllers, directement intégrés dans mon projet existant, sans friction.

J'ai donc décidé d'en implémenter un moi-même.

Architecture

Première version : Roslyn source generator

La première implémentation utilisait un Roslyn source generator — une API qui s'exécute pendant la compilation et injecte du code C# directement dans la compilation en cours. L'avantage : la génération est quasi instantanée et réactive aux changements.

Le problème est apparu quand j'ai voulu utiliser les DTOs générés avec Riok Mapperly, un mapper qui est lui aussi un source generator Roslyn. Les deux générateurs s'exécutent dans le même pipeline de compilation, et il n'est pas possible de contrôler leur ordre d'exécution. Mapperly ne voyait pas les types générés par le générateur OpenAPI — ils n'existaient pas encore quand il s'exécutait. Il fallait trouver une autre approche.

Version actuelle : MSBuild task

Je me suis inspiré de ce que fait le générateur gRPC pour C# (Grpc.Tools) : une MSBuild task qui s'exécute BeforeTargets="CoreCompile". Le code est généré dans le dossier intermédiaire (obj/) avant que la compilation ne commence, ce qui le rend visible à tous les source generators — dont Mapperly.

[openapi.yaml] → MSBuild Task → .cs générés dans obj/ → Compilation C# → Source Generators

Cette approche est plus robuste et suit un précédent bien établi dans l'écosystème .NET.

Intégration dans le projet

Le développement du packaging NuGet a été instructif : les analyzers et MSBuild tasks ont des contraintes très spécifiques sur comment les dépendances sont incluses dans le package. Contrairement à un package classique, les assemblies ne sont pas résolues de la même façon — il faut les embarquer explicitement dans les dossiers analyzers/ et build/ du package, avec des fichiers .props et .targets qui les référencent. Un bon exercice pour comprendre le format NuGet en profondeur.

Configuration

L'intégration se fait en une seule entrée dans le .csproj :

<ItemGroup>
  <OpenApiGeneratorServer Include="openapi.yaml">
    <BaseNamespace>MyApp.Generated</BaseNamespace>
    <UseRecords>true</UseRecords>
    <GenerateValidationAttributes>true</GenerateValidationAttributes>
    <GenerateXmlDocumentation>true</GenerateXmlDocumentation>
    <ControllerGroupingStrategy>ByTag</ControllerGroupingStrategy>
  </OpenApiGeneratorServer>
</ItemGroup>

Au dotnet build, les fichiers C# sont générés dans obj/OpenApiGenerated/ et inclus dans la compilation.

Exemple

Avec ce contrat :

openapi: 3.1.0
info:
  title: Users API
  version: 1.0.0
paths:
  /users/{id}:
    get:
      tags: [Users]
      operationId: GetUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 100

Le générateur produit :

DTO :

namespace MyApp.Generated.Contracts;

/// <summary>User</summary>
public record User
{
    [Required]
    public Guid Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Name { get; set; }
}

Controller (classe de base abstraite) :

namespace MyApp.Generated.Controllers;

[ApiController]
public abstract class UsersControllerBase : ControllerBase
{
    /// <summary>GetUser</summary>
    [HttpGet("/users/{id}")]
    public abstract Task<IActionResult> GetUser([FromRoute] Guid id);
}

Il reste ensuite à hériter de cette classe et implémenter la logique métier — routes, paramètres, DTOs et validation sont déjà là.

Limites

Le générateur couvre OAS 3.x et gère ce dont j'ai besoin sur Argon. Il ne prétend pas implémenter la spécification OpenAPI dans son intégralité — certains cas avancés (allOf/oneOf complexes, callbacks, liens) ne sont pas gérés.

L'autre limitation notable vient du choix MSBuild task : les IDEs ne refreshent pas toujours les types générés en temps réel quand on modifie un contrat — il faut parfois recharger le projet manuellement. C'était précisément ce que les Roslyn source generators géraient bien, avec une réactivité immédiate dans l'éditeur. C'est le compromis accepté pour avoir une génération fiable et ordonnée par rapport aux autres générateurs.