title: "Comment je référence une librairie NuGet interne en local, puis sur nuget.org en production" description: "La technique que j’utilise dans AuditStock pour déboguer SuperBlazorAIAgent depuis son dépôt source tout en conservant une référence NuGet en production." author: "Marc Chouteau" tags:

  • .NET 10
  • NuGet
  • MSBuild
  • Directory.Build.props
  • Blazor Server
  • AuditStock

Comment je référence une librairie NuGet interne en local, puis sur nuget.org en production

Mutualiser le code a un prix

Je mutualise le code dès qu’il sert à plusieurs applications.

C’est important.

Une librairie interne évite les copier-coller.

Elle centralise les règles métier.

Elle facilite les corrections.

Elle garantit un comportement commun.

Dans mon cas, SuperBlazorAIAgent regroupe une partie de la couche IA et des composants utilisés par mes applications Blazor.

Le code vit dans un dépôt dédié.

AuditStock le consomme comme une librairie NuGet.

Cette organisation est propre pour la production.

Mais elle ralentit le développement.

Quand je modifie SuperBlazorAIAgent, je dois normalement :

  1. construire le package ;
  2. le publier sur nuget.org ;
  3. attendre la disponibilité du package ;
  4. mettre à jour la version dans AuditStock ;
  5. restaurer les packages ;
  6. redéployer l’application ;
  7. commencer seulement ensuite le débogage.

Une petite correction peut donc provoquer un long aller-retour.

Je voulais conserver la distribution NuGet.

Je voulais aussi pouvoir poser un point d’arrêt directement dans le code source de SuperBlazorAIAgent.

La solution naturelle : Directory.Build.props

La solution repose sur les points d’extension MSBuild Directory.Build.props et Directory.Build.targets.

Ces fichiers sont pris en compte automatiquement par les projets situés dans leur dossier et ses sous-dossiers.

Directory.Build.props est chargé tôt.

Directory.Build.targets est chargé plus tard.

Cette différence est utile.

Je définis mes options dans le fichier .props.

Je modifie les références de projet dans le fichier .targets.

Depuis quand cette fonctionnalité existe-t-elle ?

Cette fonctionnalité n’est pas spécifique à .NET 10.

Elle vient de MSBuild 15.0.

MSBuild 15.0 a été livré avec Visual Studio 2017.

Depuis cette version, Microsoft.Common.props recherche automatiquement Directory.Build.props.

Microsoft.Common.targets fait de même pour Directory.Build.targets.

Je peux donc appliquer une règle à toute une arborescence sans répéter le même XML dans chaque .csproj.

Microsoft documente ce mécanisme dans Customize the build by folder.

Ce que je conserve dans le fichier projet

Dans AuditStock, je conserve la référence NuGet habituelle.

Par exemple, le projet Blazor contient une référence de ce type :

<PackageReference Include="SuperBlazorAIAgent" Version="1.4.21" />

Le projet qui utilise les contrats conserve aussi sa référence NuGet :

<PackageReference Include="SuperBlazorAIAgent.Abstractions" Version="1.1.6" />

Je ne remplace pas ces lignes par des conditions Debug dans chaque projet.

Les fichiers projet restent lisibles.

Ils décrivent la dépendance officielle de l’application.

La bascule locale est centralisée ailleurs.

Étape 1 : définir le comportement par défaut

Je place Directory.Build.props à la racine d’AuditStock.

Voici sa structure :

<Project>
  <PropertyGroup>
    <UseLocalSuperAIAgent Condition="'$(UseLocalSuperAIAgent)' == ''">false</UseLocalSuperAIAgent>
    <SuperAIAgentRoot Condition="'$(SuperAIAgentRoot)' == ''">$(MSBuildThisFileDirectory)..\Libs\SuperAIAgent</SuperAIAgentRoot>
  </PropertyGroup>

  <Import Project="$(MSBuildThisFileDirectory)Directory.Build.Local.props"
          Condition="Exists('$(MSBuildThisFileDirectory)Directory.Build.Local.props')" />
</Project>

Je définis deux propriétés.

UseLocalSuperAIAgent vaut false par défaut.

Le comportement standard reste donc NuGet.

SuperAIAgentRoot pointe vers le dépôt local de SuperBlazorAIAgent.

Dans mon arborescence, les deux dépôts sont voisins :

E:\S5\Appliman\
├── AuditStock\
└── Libs\
    └── SuperAIAgent\

J’utilise MSBuildThisFileDirectory.

Le chemin reste ainsi relatif au dépôt AuditStock.

Je peux aussi le surcharger ponctuellement :

dotnet build -p:SuperAIAgentRoot=E:\chemin\vers\SuperAIAgent

Étape 2 : activer le mode local uniquement sur ma machine

Je crée ensuite un fichier local nommé Directory.Build.Local.props.

Il contient seulement ceci :

<Project>
  <PropertyGroup>
    <UseLocalSuperAIAgent>true</UseLocalSuperAIAgent>
  </PropertyGroup>
</Project>

Ce fichier n’est pas commité.

Dans AuditStock, il est déjà ignoré par Git.

Le dépôt partagé ne force donc pas le chemin d’un dépôt local qui n’existe pas chez les autres développeurs.

Je peux alors lancer AuditStock en configuration Debug.

La compilation utilise le code source de SuperBlazorAIAgent.

Je peux mettre un point d’arrêt dans ce code.

Je vois les variables.

Je parcours les appels.

Je corrige la librairie et je relance immédiatement.

Étape 3 : remplacer la référence NuGet par une référence projet

Le fichier Directory.Build.props sait activer une option.

Il ne suffit pas, à lui seul, à remplacer les items PackageReference.

Je place cette logique dans Directory.Build.targets.

<Project>
  <ItemGroup Condition="'$(UseLocalSuperAIAgent)' == 'true'">
    <_LocalSuperBlazorAIAgentPackage
      Include="@(PackageReference->WithMetadataValue('Identity', 'SuperBlazorAIAgent'))" />

    <_LocalSuperBlazorAIAgentAbstractionsPackage
      Include="@(PackageReference->WithMetadataValue('Identity', 'SuperBlazorAIAgent.Abstractions'))" />

    <PackageReference Remove="SuperBlazorAIAgent"
                      Condition="'@(_LocalSuperBlazorAIAgentPackage)' != ''" />

    <PackageReference Remove="SuperBlazorAIAgent.Abstractions"
                      Condition="'@(_LocalSuperBlazorAIAgentAbstractionsPackage)' != ''" />

    <ProjectReference
      Include="$(SuperAIAgentRoot)\src\SuperBlazorAIAgent\SuperBlazorAIAgent.csproj"
      Condition="'@(_LocalSuperBlazorAIAgentPackage)' != ''" />

    <ProjectReference
      Include="$(SuperAIAgentRoot)\src\SuperBlazorAIAgent.Abstractions\SuperBlazorAIAgent.Abstractions.csproj"
      Condition="'@(_LocalSuperBlazorAIAgentAbstractionsPackage)' != ''" />
  </ItemGroup>
</Project>

La logique suit quatre mouvements.

  1. Je détecte la référence NuGet existante.
  2. Je la mémorise dans un item MSBuild temporaire.
  3. Je retire la référence package.
  4. J’ajoute la référence vers le projet local correspondant.

Je ne crée pas une référence projet en plus de la référence NuGet.

Cela évite les doublons et les conflits d’assembly.

Le ciblage par nom permet d’appliquer la règle uniquement à SuperBlazorAIAgent.

Les autres packages d’AuditStock ne changent pas.

Le contrôle qui évite les chemins silencieusement invalides

Je valide aussi l’existence des deux projets locaux avant la compilation.

<Target Name="ValidateLocalSuperAIAgent"
        BeforeTargets="PrepareForBuild"
        Condition="'$(UseLocalSuperAIAgent)' == 'true'">
  <Error
    Condition="!Exists('$(SuperAIAgentRoot)\src\SuperBlazorAIAgent\SuperBlazorAIAgent.csproj')"
    Text="Le projet local SuperBlazorAIAgent est introuvable dans '$(SuperAIAgentRoot)'." />

  <Error
    Condition="!Exists('$(SuperAIAgentRoot)\src\SuperBlazorAIAgent.Abstractions\SuperBlazorAIAgent.Abstractions.csproj')"
    Text="Le projet local SuperBlazorAIAgent.Abstractions est introuvable dans '$(SuperAIAgentRoot)'." />
</Target>

Je préfère arrêter la compilation avec un message clair.

Je ne veux pas découvrir après plusieurs minutes que le débogueur utilise finalement un vieux package NuGet.

Ce qui se passe en production

En production, je ne fournis pas Directory.Build.Local.props.

La propriété conserve donc sa valeur par défaut :

UseLocalSuperAIAgent = false

Le Directory.Build.targets local ne fait rien.

Les PackageReference restent actifs.

Le restore récupère la version déclarée dans les fichiers .csproj.

AuditStock référence alors SuperBlazorAIAgent depuis nuget.org.

Le même code projet peut donc suivre deux chemins :

flowchart LR
    A[AuditStock.csproj] --> P[PackageReference]
    P --> N[nuget.org<br/>CI et production]

    L[Directory.Build.Local.props<br/>présent seulement en local] --> S[UseLocalSuperAIAgent = true]
    S --> T[Directory.Build.targets]
    T --> R[Retire PackageReference]
    R --> J[Ajoute ProjectReference]
    J --> C[SuperBlazorAIAgent<br/>code source local]

Je garde ainsi un contrat de production explicite.

Je ne dépends pas d’un package local copié dans un dossier temporaire.

Je ne change pas les versions NuGet pour déboguer.

Je ne modifie pas les endpoints de restauration.

Mon cycle de développement après cette mise en place

Mon cycle devient beaucoup plus court :

sequenceDiagram
    actor Moi
    participant AuditStock
    participant Agent as SuperBlazorAIAgent local

    Moi->>AuditStock: Modifie un appel ou un composant
    Moi->>AuditStock: Lance Debug
    AuditStock->>Agent: Référence le projet local
    Moi->>Agent: Pose un point d'arrêt
    Agent-->>Moi: Observe et corrige le comportement
    Moi->>AuditStock: Relance le test

Quand le correctif est prêt, je publie une nouvelle version de SuperBlazorAIAgent.

Je mets ensuite à jour la version NuGet d’AuditStock.

La publication reste donc contrôlée.

Elle intervient au moment où le code est validé.

Les points auxquels je fais attention

Le nom du fichier est exact : Directory.Build.props.

Le nom du fichier local est lui aussi exact : Directory.Build.Local.props.

Je recharge la solution après avoir créé ou modifié ces fichiers.

MSBuild évalue les imports au début de la compilation.

Je vérifie aussi le chemin SuperAIAgentRoot.

Enfin, je m’assure que le fichier local n’est jamais copié dans l’agent CI ou dans le serveur de production.

Le mode local est une décision explicite.

Il n’est pas automatiquement lié à Debug.

Je l’utilise en Debug parce que c’est le moment où j’ai besoin du code source.

Si je veux verrouiller un build de production, je peux aussi imposer la valeur :

dotnet publish -c Release -p:UseLocalSuperAIAgent=false

Conclusion : je supprime tout le temps mort du cycle NuGet

Cette technique ne supprime pas la publication NuGet.

Elle la déplace au bon moment.

Pendant le développement, je travaille directement sur la librairie source.

En production, je garde une dépendance NuGet versionnée et reproductible.

Je gagne tout le temps du cycle intermédiaire : construction du package, envoi, attente de nuget.org, mise à jour de version, restauration et redéploiement.

Une correction qui demandait auparavant un aller-retour complet devient une boucle locale de compilation et de débogage.

Je conserve donc les bénéfices de la mutualisation.

Je retire presque toute la friction du développement quotidien.