Comment configurer un projet multi-target dans Visual Studio 2017 ?

Il existe une fonctionnalité dans Visual Studio 2017 qui n’est pas évidente à repérer et qui consiste à avoir plusieurs frameworks cibles pour un projet donné. Traditionnellement dans Visual Studio, on crée un projet pour un framework donné, or depuis la multiplication récente des cibles de compilation (.NET Standard ou .NET Core), il peut être nécessaire de produire des assemblies destinées à des environnements différents à partir d’un même code.

On va, dans un premier temps, indiquer ce qui peut justifier l’utilisation de projets multi-target. Ensuite, on va indiquer la méthode pour générer des assemblies pour plusieurs frameworks cible. Enfin, on indiquera des directives à utiliser dans le code pour générer du code spécifique à une plateforme.

Multitude de plateformes cibles

Les technologies Microsoft adressent un grand nombre d’environnements différents allant de systèmes d’exploitation comme Windows à des appareils mobiles comme les tablettes. D’autres parts, depuis quelques années, il est possible d’exécuter du code .NET sur d’autres plateformes que Windows. Cette ouverture a encore augmenté le nombre de plateformes sur lesquelles des technologies pouvaient s’exécuter: Linux, macOS, iOS, Android etc… Toutes ces plateformes ont des spécificités qui nécessitent, pour chacune d’entre elles, une implémentation particulière.

Pour Microsoft, un premier challenge a été de tenter d’uniformiser ses bibliothèques pour les rendre moins spécifiques à une plateforme. Ce premier travail a abouti à 3 grandes familles de technologies:

  • .NET Framework déployable sur des machines Windows,
  • .NET Core déployable sur Windows, Linux, macOS mais aussi sur des tablettes, smartphones et Xbox avec Universal Windows Platform (UWP).
  • Xamarin déployable sur Android et iOS.

Cette uniformisation ne permet pas, à elle-seule, d’utiliser un même code pour adresser plusieurs plateformes. Si on développe une bibliothèque, on est obligé d’avoir un projet spécifique pour chaque plateforme et il est nécessaire de compiler tout son code pour chacune des plateformes.

Pour permettre de compiler du code pour plusieurs plateformes et ainsi, encapsuler la complexité d’un déploiement sur plusieurs plateformes, une approche de Microsoft a été d’introduire une abstraction supplémentaire avec le .NET Standard.

.NET Standard

.NET Standard permet d’introduire une couche supplémentaire entre le code et les plateformes où seront déployées les bibliothèques:

  • Une bibliothèque .NET Standard définit un ensemble d’API qui sont communes à plusieurs plateformes.
  • Une bibliothèque .NET Standard n’est pas liée à une plateforme. Ainsi faire une bibliothèque se baser sur une version de .NET Standard permet d’éviter de la faire se baser sur une plateforme particulière. Il n’y a donc, plus de lien entre la bibliothèque et la plateforme sur laquelle elle sera déployée.
    Cette caractéristique permet d’éviter d’écrire du code pour une plateforme spécifique. Le code de la bibliothèque est écrit pour une version du .NET Standard.
  • La couche supplémentaire qu’est .NET Standard rend plus facile la mise à jour éventuelle d’une plateforme. Au lieu de compiler une bibliothèque pour une version spécifique de la plateforme, on la compile pour une version du .NET Standard.
    Si la nouvelle version d’une plateforme est compatible avec le .NET Standard sur lequel se base des bibliothèques, on peut mettre à jour la plateforme sans craindre une incompatibilité de ces bibliothèques. En effet c’est le .NET Standard qui va garantir la compatibilité.

Différences entre une bibliothèque et un exécutable

L’approche .NET Standard ne convient pas dans tous les cas. Elle permet de faciliter le déploiement de bibliothèques en ajoutant une abstraction de façon à éviter de baser ces bibliothèques directement sur des plateformes. Cette approche est possible car dans la majorité des cas, une bibliothèque de classes n’utilisent pas d’API spécifiques à une plateforme donnée.

En revanche, si une bibliothèque utilise des API trop spécifiques à une plateforme (comme par exemple, WPF qui nécessite un système Windows), elle ne pourra pas se baser sur .NET Standard (.NET Standard ne comporte pas de classes WPF).
De la même façon, un exécutable est spécifique à une plateforme. On ne pourra pas baser une exécutable sur un .NET Standard. Un exécutable est implémenté pour une plateforme précise.

Pour rendre l’approche .NET Standard la plus efficace possible, il faut donc placer un maximum de code dans des bibliothèques qui se basent sur .NET Standard. Le reste du code, étant plus spécifique, se basera sur une plateforme précise.

.NET Core

En plus de .NET Standard qui permet une certaine abstraction, la technologie .NET Core est capable de compiler du code pour des plateformes différentes. En effet .NET Core est une implémentation de Microsoft qui a pour but de rendre la technologie .NET disponible sur plusieurs plateformes. Contrairement à .NET Standard, .NET Core permet de produire des bibliothèques de classes et des exécutables. A la compilation, on précise le framework cible et l’environnement où ces assemblies seront exécutées (Windows, Linux, MacOS etc…).

Ces assemblies sont, ainsi, déployables et exécutables sur des environnements et des plateformes différents.

Fichiers projet .csproj simplifiés

.NET Core et .NET Standard ont introduit une grande flexiblité dans la technologie .NET puisqu’ils permettent de facilement générer des assemblies exécutables sur des plateformes différentes. De façon à rendre ces technologies plus facilement implémentables sur des plateformes différentes de Windows, Microsoft a fait un grand effort pour simplifier l’installation d’un environnement de développement en passant par la CLI .NET Core (CLI pour Command Line Interface) et éviter l’installation trop complexe de Visual Studio. Dans son effort de simplification, les fichiers projet .csproj ont aussi été considérablement simplifié pour ne conserver que le stricte minimum en élément de configuration de façon à ce qu’ils soient facilement éditables à la main.

Par exemple, le fichier .csproj d’un projet permettant de produire un exécutable .NET Core contient les éléments suivants:

<Project Sdk="Microsoft.NET.Sdk"> 

  <PropertyGroup> 
    <OutputType>Exe</OutputType> 
    <TargetFramework>netcoreapp2.0</TargetFramework> 
  </PropertyGroup> 

</Project>

Avec la CLI .NET Core (cf. Commandes courantes de la CLI .NET Core), on peut créer un projet console en exécutant:

dotnet new console -n <nom du projet> 

Liste de fichiers du projet

Contrairement aux fichiers .csproj traditionels, par défaut le nouveau format n’indique pas explicitement les fichiers de code du projet. Tous les fichiers .cs font partie du projet, il n’est pas nécessaire de les spécifier un à un.

On peut toutefois spécifier les fichiers un à un ou avec une wildcard en désactivant le comportement par défaut avec l’élément de configuration EnableDefaultCompileItems et en utilisant l’option Compile:

<Project Sdk="Microsoft.NET.Sdk"> 

  <PropertyGroup>   
    <OutputType>Exe</OutputType> 
    <TargetFramework>netcoreapp2.0</TargetFramework>     
    <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
  </PropertyGroup> 

  <ItemGroup> 
    <Compile Include="Program.cs"  />     
    <Compile Include="InternalClass.cs"  />     
  </ItemGroup>

</Project>

Référence entre projets

Les références entre projets sont indiquées en utilisant l’élément de configuration ProjectReference:

<Project Sdk="Microsoft.NET.Sdk"> 

  <!-- ... --> 

  <ItemGroup> 
    <ProjectReference Include="..\DotNetAssembly\DotNetAssembly.csproj" />
  </ItemGroup> 

</Project> 

On peut rajouter une référence d’un projet vers un autre en exécutant la commande CLI suivante:

dotnet add <chemin .csproj où ajouter la dépendance> reference  
<chemin .csproj à rajouter>

PackageReference vs packages.config

L’élément de configuration PackageReference concerne les packages NuGet installés pour le projet. Par exemple, si on rajoute le package NuGet Microsoft.AspNet.WepApi.Core au projet, le contenu du fichier projet .csproj devient:

<Project Sdk="Microsoft.NET.Sdk"> 

  <PropertyGroup> 
    <OutputType>Exe</OutputType> 
    <TargetFramework>netcoreapp2.0</TargetFramework> 
  </PropertyGroup> 

  <ItemGroup> 
    <PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.2.6" />
  </ItemGroup> 

</Project>

On peut ajouter une référence vers un package NuGet en exécutant la commande CLI suivante:

dotnet add <chemin .csproj> package <nom du package NuGet>

Le fichier .csproj ne contient qu’une référence vers le package NuGet rajouté. Toutes les dépendances transitives du package ne sont pas indiquées dans le fichier. Les dépendances du packages NuGet ne sont pas rajoutées au fichier contrairement aux fichiers .csproj traditionnels.

Dans les projets Visual Studio traditionnels, les références vers les packages NuGet installés et les dépendances transitives sont ajoutés dans le fichier packages.config. D’autre part, le fichier projet .csproj contient des références vers les assemblies du package NuGet.

Configurer un projet multi-target

On peut facilement configurer le projet pour compiler le code pour plusieurs frameworks cibles en modifiant l’élément de configuration TargetFramework. Dorénavant, avec les nouveaux fichiers projet .csproj, il faut juste renommer l’élément TargetFramework en TargetFrameworks (avec un “s”):

<TargetFrameworks>netcoreapp2.0;netstandard2.0</TargetFrameworks>

Dans cet exemple, on produit des assemblies pour une application .NET Core 2.0 et une bibliothèque de classes pour .NET Standard 2.0. Les éléments netcoreapp2.0 et netstandard2.0 sont appelés TFM (pour Target Framework Moniker). Chaque code TFM correspond à un framework cible particulier, par exemple:

.NET Core netcoreapp2.0, netcoreapp2.1
.NET Framework net20
net45, net452
net46, net462
net47, net471, net472
.NET Standard netstandard2.0

On peut trouver une liste exhaustive des TFM utilisés sur la page suivante: docs.microsoft.com/fr-fr/nuget/reference/target-frameworks#supported-frameworks.

Utiliser des fichiers projet simplifiés dans Visual Studio

Le fichier projet .csproj traditionnel dans Visual Studio contient énormément de détails comme:

  • La liste des fichiers .cs de code du projet,
  • La liste des références d’assembly du projet qui peuvent être des assemblies tiers, des assemblies provenant d’un autre projet de la solution ou d’assemblies provenant de packages NuGet.
  • La liste des packages NuGet installés ainsi que les dépendances de chaque package est présente dans un fichier packages.config présent dans le projet.

Ces fichiers .csproj sont, malheureusement incontournables pour la plupart des projets générés dans Visual Studio en particulier les projets produisant des assemblies exécutables sur une plateforme Windows comme:

  • Les applications Winforms,
  • Les applications WPF,
  • Les projets ASP.NET,
  • Les applications Universal Windows (UWP).

En revanche il est possible d’utiliser des fichiers projet .csproj avec une syntaxe simplifiée pour les assemblies de type:

  • Bibliothèques de classes,
  • Applications console,
  • Applications ASP.NET Core,
  • Les applications .NET Core.

Configurer les packages NuGet en mode “PackageReference”

Cette configuration est valable pour tous les types de projet. Dans le mode par défaut, les dépendances NuGet sont indiquées dans un fichier packages.config présent dans le projet. Ce fichier contient plus précisement:

  • Les packages NuGet installés explicitement et
  • Les dépendances des packages NuGet installés.

Par exemple, si on installe dans un projet le package NuGet Microsoft.AspNet.Mvc, ce package sera installé et ajouté dans le fichier packages.config. D’autre part, les dépendances transitives de ce package seront aussi installées à savoir:

  • Microsoft.AspNet.WebPages
  • Microsoft.AspNet.Razor
  • Microsoft.Web.Infrastructure

La contenu du fichier packages.config devient:

<?xml version="1.0" encoding="utf-8"?> 
<packages> 
  <package id="Microsoft.AspNet.Mvc" version="5.2.6" targetFramework="net461" /> 
  <package id="Microsoft.AspNet.Razor" version="3.2.6" targetFramework="net461" /> 
  <package id="Microsoft.AspNet.WebPages" version="3.2.6" targetFramework="net461" /> 
  <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net461" /> 
</packages> 

Les assemblies contenues dans ces packages seront ajoutées au projet et visibles dans la partie “Références” (ou References) dans Visual Studio:

Pour configurer le mode “PackageReference” dans Visual Studio 2017:

  1. Cliquer sur le menu “Outils” (ou “Tools”) ⇒ “Options”
  2. Aller dans la partie “Gestionnaire de package Nuget” (ou “Nuget package manager”) ⇒ “Général”
  3. Dans la partie “Gestion de packages” (ou “package management”), sélectionner le paramètre “Format de gestion de package par défaut”: “PackageReference” au lieu de “packages.config”:
  4. Cliquer sur OK

Si avec cette configuration, on crée un nouveau projet (quel que soit le type) et si on ajoute un package NuGet, par exemple en exécutant dans la console du gestionnaire de package (i.e. Package Manager Console):

install-package Microsoft.AspNet.Mvc 

La présentation du projet dans Visual Studio est un peu différente:

  • Il n’y a plus de fichier packages.config
  • On ne voit plus qu’une référence vers le package NuGet installé, les dépendances transitives du package NuGet ne sont plus visibles:

Dans le fichier .csproj, il y a une référence vers le package installé mais pas de liens vers les dépendances transitives du package:

<?xml version="1.0" encoding="utf-8"?> 
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 

  <!-- ... --> 

  <ItemGroup> 
    <PackageReference Include="Microsoft.AspNet.Mvc"> 
      <Version>5.2.6</Version> 
    </PackageReference> 
  </ItemGroup>

  <!-- ... --> 

</Project>

De même, il n’y a pas de répertoire packages dans le répertoire du projet contenant les packages NuGet téléchargés. Les packages NuGet sont téléchargés dans le répertoire par défaut du cache c’est-à-dire:

%LocalAppData%\NuGet\Cache

L’utilisation du mode “PackageReference” présente plusieurs avantages:

  • Il simplifie considérablement le fichier projet .csproj puisqu’il n’y a plus les références d’assemblies. En effet, ces références changeaient à chaque mise à jour du package.
  • On ne voit plus les dépendances transitives des packages qui alourdissaient les fichiers projet .csproj.

En revanche, la résolution des conflits de versions dans les packages NuGet est plus complexe. Pour faciliter cette résolution, on peut:

Pour plus de détails, voir Nuget en 5 min.

A la compilation, les assemblies sont directement copiées dans le répertoire de sortie.

Rendre flexible la version d’une référence de package

Au lieu de figer la version de la référence de package NuGet comme dans l’exemple précédent:

<PackageReference Include="Microsoft.AspNet.Mvc"> 
  <Version>5.2.6</Version> 
</PackageReference>

On peut utiliser des wildcards pour indiquer la version de la dépendance:

<PackageReference Include ="Microsoft.AspNet.Mvc" version="5.2.*"/> 

On peut aussi indiquer des contraintes de version:

1.0 1.0 ≤ x Version 1.0 ou supérieure
(1.0,) 1.0 < x Version strictement supérieure à 1.0
[1.0] x == 1.0 Exactement la version 1.0
(,1.0] x ≤ 1.0 Version 1.0 ou antérieure
(,1.0) x < 1.0 Version strictement antérieure à 1.0
[1.0,2.0] 1.0 ≤ x ≤ 2.0 Version entre la 1.0 et 2.0
(1.0) indication non valide

Par exemple:

<PackageReference Include ="Microsoft.AspNet.Mvc" version="[5.0.0,5.2)"/>

Forcer l’utilisation d’un répertoire “packages” avec le mode “PackageReference”

On peut toutefois forcer la présence du répertoire packages dans le répertoire du projet en éditant le fichier .csproj et en ajoutant l’élément de configuration RestorePackagePath:

<?xml version="1.0" encoding="utf-8"?> 
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" 
  Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> 
  <PropertyGroup> 

    <!-- ... --> 

    <RestorePackagesPath>packages</RestorePackagesPath>
  </PropertyGroup> 

  <!-- ... --> 

</Project> 

Configurer un projet multi-target dans Visual Studio 2017

Comme indiqué plus haut, cette fonctionnalité est disponible pour les fichiers .csproj à la syntaxe simplifée. Cette syntaxe est possible seulement pour certains types de projet:

  • Bibliothèques de classes,
  • Applications console,
  • Applications ASP.NET Core,
  • Les applications .NET Core

Ainsi dans Visual Studio, si on crée un projet de type Console pour le framework .NET Core, les références du projet sont présentées de façon différente. Elles sont disponibles dans la partie “Dépendances” (i.e. Dependancies) du projet:

Traditionnellement dans les propriétés du projet, il est possible de modifier le framework cible. Pour accéder aux propriétés du projet, il faut:

  1. Faire un clique droit sur le projet dans Visual Studio
  2. Cliquer sur Propriétés
  3. Dans l’onglet “Application”, on peut éditer le paramètre “framework cible”:

Pour configurer le projet pour cibler plusieurs frameworks, il faut:

  1. Faire un clique droit sur le projet.
  2. Cliquer sur “Modifier <nom du projet>” (i.e. “Edit <nom du projet>”).
  3. Il est possible d’éditer le fichier .csproj directement dans Visual Studio (dans les versions actuelles, il n’existe pas d’éléments graphiques dans Visual Studio pour effectuer cette modification).
    Si “Modifier <nom du projet>” n’est pas présent, c’est que la syntaxe du fichier projet .csproj n’est pas une syntaxe simplifiée.
    Il n’y a pas d’assistants pour passer d’une syntaxe traditionnelle à une syntaxe simplifiée.
  4. Comme précédemment, il faut modifier le paramètre TargetFramework en ajoutant des TFM (i.e. Target Framework Moniker):
    <Project Sdk="Microsoft.NET.Sdk"> 
    
      <PropertyGroup> 
        <OutputType>Exe</OutputType> 
        <TargetFramework>netcoreapp2.1</TargetFramework>
      </PropertyGroup> 
    
    </Project>
    
  5. Ajouter un “s” à TargetFramework, par exemple:
    <Project Sdk="Microsoft.NET.Sdk"> 
    
      <PropertyGroup> 
        <OutputType>Exe</OutputType> 
        <TargetFrameworks>netcoreapp2.1;netstandard2.0</TargetFrameworks>
      </PropertyGroup> 
    
    </Project>
    
  6. Enregistrer le fichier .csproj.

Après rechargement du fichier par Visual Studio, les dépendances contiennent plusieurs frameworks cible:

En allant dans les propriétés du projet, on ne peut plus éditer le paramètre “Framework cible”:

Pourquoi configurer plusieurs plateformes cible alors qu’on peut utiliser .NET Standard ?

On peut se poser la question de savoir quelle est la nécessité de paramétrer plusieurs plateformes cible alors qu’il suffit de paramétrer une plateforme cible .NET Standard. Comme .NET Standard est compatible avec plusieurs frameworks (on peut voir les compatibilités sur la page docs.microsoft.com/fr-fr/dotnet/standard/net-standard), en configurant la plateforme cible .NET Standard, on s’assure d’être compatible avec tous les frameworks prenant en charge par le standard. C’est vrai, toutefois:

  • Comme indiqué plus haut, .NET Standard permet de produire des assemblies seulement pour les bibliothèques de classes. On ne peut pas paramétrer une cible correspondant à .NET Standard pour un exécutable. Si on souhaite produire des exécutables différents pour un même code, il faut pouvoir paramétrer des frameworks cibles différents.
  • Certains packages NuGet comportent des assemblies spécifiques à une plateforme donnée. Ces plateformes peuvent être différentes d’une version de .NET Standard. En paramétrant des frameworks cible précis, si la compilation réussit, on peut être sûr que les dépendances NuGet seront assurées pour les cibles paramétrées.
  • Un des intérêts de cette fonctionnalité est de produire des assemblies pour plusieurs frameworks à partir d’un même code. Il est possible d’affiner la compilation en appliquant des directives de compilation pour des plateformes spécifiques. On s’assure, ainsi, à la compilation que le code compilé est compatible avec le framework auquel il est destiné. Ces directives de compilation seront indiquées par la suite.

NuGet est intégré à MSBuild

Pour permettre de simplifier les fichiers, NuGet a été intégré à MSBuild. Cette intégration a plusieurs avantages:

  • L’exécution de NuGet fait partie d’une tâche MsBuild qu’on peut lancer directement en exécutant MsBuild. Par exemple, pour générer un package NuGet, on peut lancer la tâche suivante:
    msbuild.exe "<chemin du fichier projet .csproj>" /t:pack
    
  • On peut générer un package NuGet directement à la compilation du projet en allant dans les propriétés du projet dans Visual Studio, dans l’onglet “Package” et en cochant “Generate NuGet package on build”. On peut ainsi préciser des paramètres liés au package NuGet à générer, par exemple:
    <Project Sdk="Microsoft.NET.Sdk"> 
    
      <PropertyGroup> 
        <TargetFrameworks>netcoreapp2.1;netstandard2.0</TargetFrameworks> 
        <GeneratePackageOnBuild>True</GeneratePackageOnBuild> 
        <Company>CompanyName</Company> 
        <Authors>AuthorName</Authors> 
        <Description>Description</Description> 
        <Version>1.1.1</Version>
      </PropertyGroup> 
    
      <!-- ... --> 
    
    </Project> 
    

    Il existe un onglet de configuration dans Visual Studio dans les propriétés du projet:

  • On peut appliquer des conditions d’application pour l’utilisation d’une référence de package dans le fichier projet .csproj:
     <ItemGroup> 
        <PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.2.6" 
        Condition = "'$(TargetFramework)' == 'netstandard2.0'" /> 
    </ItemGroup>
    

Pour plus d’informations: docs.microsoft.com/fr-fr/nuget/reference/msbuild-targets.

On peut effectuer le téléchargement des packages NuGet directement avec MsBuild dans une usine de build, on n’est plus obligé de configurer une étape supplémentaire pour exécuter nuget restore.

Possibilité d’avoir des symboles de préprocesseur dans le code.

On peut préciser des symboles de préprocesseur pour que des parties du code soient compilées pour une plateforme cible spécifique.

Par exemple:

public static class MultiTargetCode 
{ 
        public static string GetTargetFramework() 
        { 
#if NETCOREAPP2_1 
            return ".NET Core"; 
#elif NETFULL 
            return ".NET Framework"; 
#else 
            throw new NotImplementedException();  
#endif 
        } 
}

Quelques autres symboles de préprocesseur:

.NET Framework NET20, NET46, NET461, NET462, NET47, NET471, NET472
.NET Standard NETSTANDARD1_5, NETSTANDARD1_6, NETSTANDARD2_0
.NET Core NETCOREAPP2_0, NETCOREAPP2_1

Une liste exhaustive de ces symboles se trouve sur la page suivante: docs.microsoft.com/fr-fr/dotnet/standard/frameworks.

Références

Leave a Reply