packages.config vs PackageReference - le fonctionnement de Nuget avec le nouveau format csproj

Si vous développez un projets sur le framework .NET "classique", et que vous avez décidé de passer à .NET Standard pour les projets liés, vous avez peut-être eu droit à une erreur FileNotFoundException au chargement d'une dll (utilisée par votre projet .NET Standard) - vous obligeant à référencer le package Nuget en question directement depuis l'application appellante.

Depuis quelques temps, les développeurs .NET peuvent utiliser un nouveau format de fichier .csproj (et pour les développeurs Visual Basic, tout ce que j'explique ici à propos des csproj s'applique aussi aux vbproj), dans les projets .NET Core ou .NET Standard.
Voici par exemple les 2 formats de csproj, l'un étant un projet console en .NET classique, l'autre en .NET Core :

Comparaison des anciens et nouveaux formats csproj

Outre le gain évident en terme de lisibilité, le nouveau format apporte une nouvelle gestion des packages Nuget. Voyons comment tout ça fonctionne maintenant.

L'ancien fonctionnement : le fichier packages.config

Si vous êtes développez avec l'ancien format csproj, lorsque vous référencez un package Nuget, il est ajouté dans le fichier packages.config, qui se trouve à la racine du projet.

<?xml version="1.0" encoding="utf-8"?>
<packages>
    <package id="Newtonsoft.Json" version="12.0.1" targetFramework="net471" />
</packages>

Si le package en référence d'autres, toutes les dépendances seront ajoutées dans ce fichier. On peut donc très vite arriver à un assez grand nombre de packages référencés ici.

Le fonctionnement de Nuget est ensuite très simple : il va télécharger le package et le mettre dans un dossier packages, situé au même niveau que le sln. Il va ensuite modifier le csproj pour ajouter la référence vers la ou les dll. Ces dll seront ensuite copiées dans le répertoire de destination en même temps que le projet.

Le nouveau fonctionnement :

La refonte du format csproj a été l'occasion d'améliorer la gestion des packages, et donc de gérer Nuget nativement. Cette fois, plus de fichier packages.config, si vous référencez un package il est directement ajouté dans le csproj en tant que package :

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp2.1</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
    </ItemGroup>
</Project>

Contrairement à l'ancien csproj, où toutes les références étaient des liens vers des dll, ici on sait que la référence vient de Nuget quand on utilise la balise <PackageReference>.

Autre différence avec l'ancien format : nous n'avons plus le dossier packages. Maintenant, les dll sont téléchargées dans le profil utilisateur (allez jeter un œil dans %userprofile%\.nuget\packages sur votre disque dur pour voir la liste), et elles ne sont pas copiées dans l'output lors de la compilation.

Utiliser les deux à la fois

Si vous avez des projets .NET Standard référencés par un projet .NET classique, vous allez vous retrouvez avec les 2 systèmes à la fois, qui ne sont pas vraiment compatibles, et c'est ce qui va causer la FileNotFoundException.

Supposons que votre projet .NET Standard référence Newtonsoft.Json. Il utilise le nouveau Nuget, et ne va pas copier la dll dans l'output.
Votre projet .NET classique lui utilise toujours l'ancien système de références de projets : il va récupérer l'output de l'autre projet, mais ne récupèreras donc pas Newtonsoft.Json. Et donc, lorsque vous aurez besoin de sérialiser des données à l'exécution, ça plante.

System.IO.FileNotFoundException

Un moyen simple de réparer ce problème est de simplement ajouter le package Nuget dans le projet principal, pour être sûr qu'il le récupère. Mais il y a mieux.

Utiliser PackageReference dans l'ancien csproj.

En fait, il est possible d'utiliser les dans les anciens csproj, la documentation se trouve ici :
Migrate from packages.config to PackageReference.

Attention, ça ne fonctionne que dans Visual Studio 2017 (v 15.7 minimum), et les projets C++ et ASP.NET ne sont pas encore supportés.
Pour utiliser PackageReference, il suffit d'ajouter <RestoreProjectStyle>PackageReference</RestoreProjectStyle> dans le csproj :

</PropertyGroup>
    <RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>

Attention, si vous avez déjà un packages.config, il faut désinstaller tous les packages avant de les remettre. Visual Studio 2017, vous propose de le faire automatiquement, il suffit de faire un clic droit sur le fichier packages.config, et cliquer sur "Migrate packages.config to PackageReference".

Migration de packages.config vers PackageReference

Si tout se passe bien, ça marchera du premier coup (sinon bon courage pour débuguer les conflits de version). Une fois sur le nouveau Nuget, vous ne devriez plus avoir de soucis de fichiers manquants.

Générer des documents xlsx avec le OpenXml SDK

Depuis que je fais des applications métier, il y a une tâche qu'on me demande régulièrement : exporter des données dans Excel. Pour faire ça, il y a plusieurs solutions, plus ou moins simples, chacune avec avantages ou inconvénients.
On peux par exemple : utiliser des outils externes (Telerik, SSRS), faire de l'interrop Excel, ou générer un simple fichier texte au format CSV qui sera compris sans trop de problème avec le logiciel.

J'ai décidé de mon coté de me lancer dans la solution qui n'est pas la plus simple, mais qui me semble la plus propre : générer directement le fichier xlsx à l'aide du Open XML SDK ; avec l'avantage de pouvoir obtenir directement un fichier natif sans s'encombrer de dépendances, mais un développement plus compliqué car on va devoir travailler directement sur la structure du fichier OpenXML. Le SDK est compatible avec le framework .NET depuis la version 3.5, et avec .NET Standard 1.3, donc il devrait fonctionner dans à peu près tous vos projets C#.

Ici on fait un simple export de données, ça reste relativement simple puisqu'il va nous suffir de remplir des cases ; ça sera autre chose si on veut utiliser des fonctionnalités plus avancées d'Excel. Pour mon code, je me suis basé sur ce post de blog, qui explique comment démarrer : Creating a simple XLSX from scratch using the Open XML SDK.

Génération du fichier Excel

On commence donc par installer le Open XML SDK, qui se trouve maintenant sur Nuget : DocumentFormat.OpenXml, puis on va créer notre fichier :

using (var package = SpreadsheetDocument.Create(@"C:\\Temp\\Export.xlsx", SpreadsheetDocumentType.Workbook))
{
    // Initialisation du document
    var workbookPart = package.AddWorkbookPart();

    var workbook = new Workbook();
    workbook.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
    workbookPart.Workbook = workbook;

    var sheets = new Sheets();
    workbook.Append(sheets);

    // Ajout du premier onglet
    var sheet = new Sheet() { Name = "Onglet1", SheetId = 1, Id = "rId1" };
    sheets.Append(sheet);
    var worksheetPart = workbookPart.AddNewPart<WorksheetPart>("rId1");
    var worksheet = new Worksheet();
    var sheetData = new SheetData();
    worksheet.Append(sheetData);
    worksheetPart.Worksheet = worksheet;
}

Ce code là est le stric minimum pour avoir un document qui s'ouvre dans Excel sans erreur : on initialise le document, on crée une liste d'onglets, on ajoute un premier onglet et on le prépare à recevoir des données.

Ensuite, on va pouvoir remplir les cases :

    // Première ligne
    var row = new Row();
    sheetData.Append(row);

    // Ajout d'une cellule à la ligne
    Cell cell = new Cell()
    {
        CellReference = "A1",
        DataType = CellValues.InlineString
    };
    InlineString inlineString = new InlineString();
    Text text = new Text();
    text.Text = "Bonjour Excel !";
    inlineString.Append(text);
    cell.Append(inlineString);
    row.Append(cell);

Et voilà, la première case est remplie. Il vous suffit de faire ça pour chaque case et c'est bon !
Le format fonctione par ligne (Row), donc tant que vous êtes sur la même ligne (A1, B1, C1) vous ajoutez dans le même row, et dès que vous allez à la ligne vous en créez un nouveau.

Mutualisation du code

Maintenant que ça c'est fait, je me suis dit que c'était pas la peine de le refaire à chaque nouveau projet. J'ai donc créé un package Nuget qui permets de faire ça plus facilement. Il suffit de lui envoyer une liste d'objets ou le résultat d'une requête SQL, et ça génère le fichier.

Le package Nuget est ici, et le code source est là. Ça permets d'exporter au format CSV ou XLSX. D'ailleurs au passage, j'en profite pour demander de l'aide, si quelqu'un sait comment générer des fichiers OpenDocument pour LibreOffice, ça m'intéresse !

Pour générer vos fichier, vous pouvez ajouter des attributs ExportColumn sur vos classes :

public class SampleData
{
    [ExportColumn(Title = "Number", Order = 1)]
    public int IntData { get; set; }

    [ExportColumn(Title = "Text", Order = 2)]
    public string TextData { get; set; }
}

Puis les envoyer dans le DataExporter :

var data = new List<SampleData>()
{
    new SampleData{ IntData=5, TextData="Hello"},
    new SampleData{ IntData=20, TextData="Yoo"},
    new SampleData{ IntData=10, TextData="This is some text"},
};

var xlsxExporter = new XlsxDataExporter();
var xlsxResult = xlsxExporter.Export(data);

Si vous voulez juste exporter le résultat d'une requête SQL sans le mapper, vous pouvez aussi :

using (SqlConnection connection = new SqlConnection(connectionString))
{
    SqlCommand command = new SqlCommand("SELECT OrderID, CustomerID FROM dbo.Orders", connection);
    connection.Open();
    SqlDataReader reader = command.ExecuteReader();
    var xlsxExporter = new XlsxDataExporter();
    var xlsxResult = xlsxExporter.Export(reader);
}

Le résultat est un MemoryStream, que vous pouvez enregistrer directement dans un fichier, ou retourner comme résultat d'une action dans un site MVC.

J'espère que tout ça vous sera utile. J'ai écrit l'API pour mon propre usage, n'hésitez pas à tester et me faire des retours car ça peut encore évoluer.

Retour d'expérience - déploiement automatique de sites web #nowwwel

Aujourd'hui un article un peu différent de ce que vous avez l'habitude de voir sur ce blog. Depuis le 1er décembre des auteurs français se relaient autour du hashtag #nowwwel à l'initiative de HTeuMeuLeu pour offrir chaque jour un nouvel article sur la conception web ; et je vous propose donc aujourd'hui mon article.

Je vais vous parler aujourd'hui de déploiement automatisé. Ou, pour être précis, je vais vous faire un retour d'expérience sur la manière dont je suis passé en plusieurs années de la copie par FTP au déploiement par TFS Release Management.

Les fichiers HTML et le FTP

J'étais au lycée quand j'ai fait ma première page perso. J'avais trouvé un logiciel sur le CD de Wanadoo qui permettait de faire son propre site web, et j'ai donc décidé de me lancer. Claris Home Page était un éditeur WYSIWYG, et j'ai découvert le HTML en regardant le code qu'il générait. Je n'ai su que beaucoup plus tard que ce n'était pas la meilleure manière d'apprendre, mais ça fonctionnait, et c'est tout ce qui m'importait.

Pour déployer, c'était très simple : on trouve un hébergeur gratuit, on crée un compte, et on envoie tous les fichiers par FTP. C'était du contenu pûrement statique, donc pas de question à se poser, ça ne pouvait pas ne pas marcher. Il fallait juste s'assurer que la page d'accueil s'appelle index.html.

Quelques temps plus tard, je me suis mis au php, pour développer un site web avec un ami. Ça fonctionnait toujours pareil : on se connecte au ftp et on copie les fichiers. Et vu qu'on était 2, le ftp servait aussi à partager le code entre nous...

Avantages : c'est facile à utiliser

Inconvénients : à l'époque j'en voyais aucun :)

Découverte du monde professionnel et des sites compilés

En 2006 j'ai rejoins la société Bewise en tant que stagiaire, où j'ai énormément appris. C'est là que j'ai débuté avec les technologies de Microsoft, j'ai découvert que TFS permettait de gérer les codes sources très efficacement (avec le recul, j'ai du mal à comprendre comment j'ai pu faire 5 ans de fac en gardant mes codes sources sur clé usb), et j'ai surtout découvert ASP.NET.

La grosse différence avec ce dont j'avais l'habitude, c'est que cette fois le code est compilé ; pas question donc de se contenter d'envoyer les fichiers par ftp.
Je travaillais sur un projet pour Airbus (c'est le passage obligé pour un Toulousain), et pas question de déployer directement sur le serveur, ils ont des équipes dédiées pour ça. Nous leur fournissions un programme d'installation (InstallShield, le même que lorsque vous installez un jeu sur votre PC) qui installait le site et configurait IIS correctement.

Avantages : Le process à suivre limitait le risque d'erreur

Inconvénients : On perdait beaucoup de temps à chaque livraison

Un peu de simplicité avec WebDeploy

L'exemple précédent est particulier, en général pour déployer un site ASP.NET on préfèrera passer par l'outil de déploiement intégré à Visual Studio. Cet outil va compiler le site et nous fournir la version prête à déployer. Il propose aussi de déployer directement le site sur un serveur IIS distant en utilisant le protocole Web Deploy.

Web Deploy

Ici le fonctionnement est très simple, on rentre l'adresse du serveur, le nom du site dans IIS, le login et mot de passe, et c'est parti. Le protocole est basé sur HTTPS, donc le transfert sera plus sécurisé que par FTP.

Avantages : Très simple et rapide, sécurisé

Inconvénients : Il ne faut pas oublier de récupérer le code des collègues avant de déployer

Déploiement automatique par un serveur de compilation

Début 2015 j'ai rejoins ma société actuelle, et je découvrais de nouvelles conditions de travail. Au lieu de développer et livrer des projets à des clients, j'avais maintenant la charge de développer et maintenir des applications web sur le long terme.
J'avais entendu parler des vertus de la compilation automatique et de l'intégration continue –notamment grâce à mon ami Patrice– mais je n'avais jamais réellement pratiqué.

Honteux de mon ignorance à ce sujet, je me suis empressé d'apprendre à configurer un serveur de build, avant que mes nouveaux collègues ne s'aperçoivent de mon incompétence.
J'ai donc commencé par mettre en place des compilations automatiques, avant d'enchainer par le déploiement. J'étais tellement fier de moi que j'en ai fait un article sur ce blog.

Il existe de nombreux outils d'intégration continue (Jenkins, TeamCity pour les plus connus), j'ai choisi TFS pour une raison très simple (et certainement très mauvaise) : on utilisait déjà TFS, et c'était intégré. Mais ça fonctionnait, et ça a très bien évolué par la suite, je ne regrette aujourd'hui absolument pas ce choix.

Enfin, pour être franc, à l'époque j'ai regretté de l'avoir fait un poil trop tôt :)

Avantages : Le déploiement est entièrement automatisé, on ne risque pas de se louper. On peut bloquer le déploiement si les tests unitaires ne passent pas. On peut se moquer des collègues qui font planter la compilation.

Inconvénients : La gestion des définitions de build demande un travail supplémentaire. Les collègues se moquent de moi quand je fais planter la compilation.

Des scénarios plus poussés avec Release Management

Dans la continuité de la section précédente, j'ai commencé à utiliser un nouvel outil : Release Management.
Maintenant, au lieu d'avoir une compilation qui déploie, on a 2 outils séparés pour la compilation et le déploiement. Cela permet notamment de gérer plus facilement les différent environnements : on ne compile qu'une fois, et on déploie sur les différents serveurs sans problème.

J'en ai aussi profité pour ajouter la base de données de mes applis à mon process de déploiement. L'image de la base de données est dans mon contrôleur de source, au même titre que le code, et peut être déployée manuellement sur les postes des développeurs, ou automatiquement sur les serveurs de test et de production lors du déploiement de l'application.

Release Management

Avantages : Comme le paragraphe d'avant, mais en mieux.

Inconvénients : C'est toujours du travail. Mais moins pénible parce que je commence à maitriser l'outil

Et après ?

L'idéal serait de stabiliser une technique de déploiement, et arrêter de changer tous les 3 mois. Mais j'ai encore des améliorations à faire ; et plus j'utilise l'outil plus j'ai envie d'aller encore plus loin.
Premier possibilité : séparer les branches de dev et de déploiement pour pouvoir lancer la publication juste en archivant sur la bonne branche, et ne plus passer par l'interface web. Mais il va falloir que l'équipe prenne l'habitude d'utiliser les branches (en n'oubliant pas qu'on utilise TFS, qui malgré ses qualités a une gestion des branches moins pratique que Git).
Autre possibilité, soyons fous, un déploiement auto de la branche dev tous les soirs. Mais là attention, ça va demander beaucoup de discipline de la part des développeurs, je ne suis pas sûr qu'on soit encore prêts.

Je n'ai pas de section commentaire sur mon blog, mais vous pouvez réagir sur twitter ; merci pour votre attention, et joyeux #nowwwel à tous !