Suite à mes articles précédents sur la migration d'une librairie en .NET Core , j'ai reçu des remarques de lecteurs concernant le coté un peu obscure de .NET Core dans mes explications.
J'en convient, j'ai fourni des liens sur des explications généralistes, mais finalement pas vraiment d'explications "techniques" sur ce nouveau framework.
Qu'à celà ne tienne, allons à la rencontre de notre nouvel ami.
Actuellement la documentation étant en totale refonte, elle n'existe qu'en anglais.
Je rappelle les articles écrit par Olivier Dahan
Je résume malgré tout les caractéristiques de base de .NET Core:
Tout d'abord il nous installer .NET Core, et un éditeur de texte.
Vous trouverez tout pour l'installation à cette adresse: https://www.microsoft.com/net/core#windows
Si vous avez Visual Studio 2015, assurez-vous d'avoir l'Update 3 et installer les .NET Core 1.0.1 - VS 2015 Tooling Preview 2.
Si vous voulez utiliser les outils en ligne de commande avec un éditeur autre (VS Code, Sublime Text, notepad) vous pouvez vous contenter d'installer .NET Core SDK for Windows.. Le SDK est inclus dans ".NET Core 1.0.1 - VS 2015 Tooling".
Pour ce tutoriel on va utiliser la CLI (command-Line Interface) c'est-à-dire les lignes de commandes.
Pour vérifier que le .NET Core est installé il suffit d'ouvrir un "Invite de commande" ou une console "Powershell" et de saisir dotnet, vous devez obtenir quelque chose comme:
> dotnet
Microsoft .NET Core Shared Framework Host
Version : 1.0.1
Build : cee57bf6c981237d80aa1631cfe83cb9ba329f12
Usage: dotnet [common-options] [[options] path-to-application]
Common Options:
--help Display .NET Core Shared Framework Host help.
--version Display .NET Core Shared Framework Host version.
Options:
--fx-version <version> Version of the installed Shared Framework to use to run the application.
--additionalprobingpath <path> Path containing probing policy and assemblies to probe for.
Path to Application:
The path to a .NET Core managed application, dll or exe file to execute.
If you are debugging the Shared Framework Host, set 'COREHOST_TRACE' to '1' in your environment.
To get started on developing applications for .NET Core, install .NET SDK from:
http://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409
Si ce n'est pas le cas essayez de désintaller et de réinstaller le framework.
Tout ce dont nous avons besoin pour développer une application .NET Core utilise la commande dotnet. Cette commande regroupe un ensemble d'outils qui sont une combinaison de commandes et de paramètres. C'est le même principe qu'un git, npm ou composer pour ceux qui connaissent.
dotnet supporte de base plusieurs commandes:
A savoir que dotnet est conçu pour pouvoir être étendu via des paquet Nuget, donc cet outil est totalement extensible.
Celà peut être perturbant, si vous êtes habitué à utiliser des outils comme Visual Studio, de manipuler une ligne de commande de ce genre. En réalité ce que fait VS c'est d'exécuter des lignes de commande à notre place. Toutefois actuellement on gère tout un ensemble de commande pour construire une application .NET msbuild, csc, etc.
Ce qui change à partir de maintenant c'est que tout est regroupé sous une seule commande: dotnet.
Etudions un peu plus précisement la CLI. Les commandes qui suivent créé une nouvelle application, restaure les paquets, compile l'application et l'exécute:
> dotnet new
> dotnet restore
> dotnet build --output /stuff
> dotnet /stuff/new.dll
Si l'on étudie de plus près ces commandes on peut déterminer 3 éléments :
Le pilote c'est la partie dotnet de la commande. Le pilote à deux responsabilités:
Tout va dépendre de ce qui défini dans la ligne de commande. Dans le premier cas vous devez spécifier la DLL d'une application portable que dotnet doit exécuter, par exemple : dotnet /path/to/app.dll.
Dans le second cas, le pilote essaye d'invoquer la commande spécifiée. Ce qui démarre un processus d'exécution:
Le verbe est simplement une commande qui exécute une action. dotnet build compile votre code. dotnet publish publie votre code. Le verbe est définie dans une application console qui est nommée par convention dotnet-{verb}. Toute la logique est implémentée dans l'application console qui représente le verbe.
Les arguments que vous passez dans la ligne de commande sont les arguments pour le verbe/commande que vous invoquez. Par exemple dotnet publish --output publishedapp, --ouput ... est l'argument passé à la commande publish.
Allez on ne déroge pas à la tradition des développeurs, faisons une petite application "Hello World" :)
Première chose créez un dossier vide, et ouvrez un invite de commande se situant dans ce dossier.
Ensuite créons une nouvelle application de base:
> dotnet new
Vous obtenez deux nouveau fichiers:
Ce fichier contient toutes les informations nécessaire permettant la génération de l'application (dépendances, options, ...).
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable",
"emitEntryPoint": true
},
"dependencies": {},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.1"
}
},
"imports": "dnxcore50"
}
}
}
Ce fichier contient le code de base de l'application avec son point d'entrée.
using System;
namespace ConsoleApplication
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
La seconde chose à faire c'est de mettre à jour les dépendances de l'application.
> dotnet restore
La commande va analyzer le fichier project.json, télécharger les dépendances (paquets Nuget) et écrire le fichier project.lock.json. Ce fichier est nécessaire pour la compilation et l'exécution.
Le fichier project.lock.json contient l'ensemble des dépendances (même indirectes) et des informations qui décrivent l'application. Ce fichier est lu par d'autres commandes comme dotnet build et dotnet run, leur permettant de traiter le code source avec les dépendances correctes.
Maintenant que notre projet est prêt on peut l'exécuter:
> dotnet run
Comme nous n'avons pas compilée l'application avant de l'exécuter, dotnet run l'a détecté et à lancé dotnet build pour compiler l'application. Nous aurions pu le faire nous-même.
On constate que nous avons deux dossiers "bin" et "obj" qui sont apparus, comme n'importe quel projet .NET.
Dans le dossier "bin" on va trouver un dossier "Debug", ce qui reste classique, et surtout cela indique que par défaut nous compilons en "Debug".
En revanche dans le dossier "Debug" on ne trouve pas d'exécutable, mais un dossier "netcoreapp1.0".
En effet .NET Core permet de cibler plusieurs frameworks. C'est-à-dire que nous pouvons générer a partir du même projet, plusieurs frameworks cibles (par exemple une version .NET 4.0, .NET 4.6, .NET Core, PCL, ...). Dans le fichier "project.json" on trouve une liste de "frameworks", et pour chaque framework que trouvera .NET Core il va générer un dossier contenant le résultat de la compilation.
Dans le dossier "netcoreapp1.0" en revanche on trouve une chose a laquelle nous ne sommes pas habitués: il n'y a pas de ".exe" mais un ".dll" portant le nom du dossier dans lequel se trouve le projet. Dans mon cas j'ai fait mon application dans un dossier "netcore" j'ai donc un fichier "netcore.dll".
Comment ce fait-il qu'en ayant créée une application, nous ayons une DLL et pas un exécutable ?
.NET Core est portable, or les ".exe" sont des formats exécutables ne fonctionnant que sous Windows, donc ils ne sont pas portables. Par conséquent par défaut les applications sont compilées dans des DLL et sont exécutées via dotnet. Ainsi une DLL compilée sous Windows peut être exécutée sous Linux (selon le même principe que Java pour ceux qui connaissent).
Comme nous venons de la voir les applications compilées sont "portables". En .NET Core on parle de "Framework Dependent deployment" ou FDD.
Mais .NET Core est capable également de générer des applications "auto-contenues" (ou autonome) (SDC pour "Self-Contained deployment").
Je ne vais pas m'étendre sur ce principe, je vous renvoi vers la documentation.
Je vais juste faire un résumé.
C'est le comportement pas défaut, dans ce mode tout est compilé selon le framework ciblé, et en aucun cas d'un OS.
Pour exécuter ce type d'application il est nécessaire d'avoir .NET Core dans la version cibléé à la compilation d'installée sur le système et de faire appel à dotnet.
Dans ce mode, l'application est compilée selon un "runtime" (cad un OS cible) et inclus le code de l'application mais également toutes les librairies et le .NET Core utilisés pour ce runtime.
L'avantage principal c'est que l'application s'exécute sur l'OS sans dépendances préinstallées. La contre-partie est la taille du fichier de l'application qui peut être conséquent, car on inclus l'ensemble du code.
On va compiler notre application en autonome, en modifiant notre "project.json".
Première chose on va copier notre "project.json" pour pouvoir revenir en arrière si besoin.
> cp project.json project-portable.json
Premier point, supprimons la ligne "type" = "platform" dans la dépendance "Microsoft.NETCore.App". Ce qui va indiquer qu'on va compiler en SCD.
Mais celà ne suffit pas, il faut également indiquer le "runtime" dans lequel on va compiler notre application. Pour celà on ajoute une section "runtimes" dans "project.json".
Voilà notre nouveau fichier:
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable",
"emitEntryPoint": true
},
"dependencies": {},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.1"
}
},
"imports": "dnxcore50"
}
},
"runtimes": {
"win7-x64":{},
"osx.10.11-x64":{}
}
}
Dans "runtimes" j'ai défini deux runtimes "win7-x64" et "osx.10.11-x64" pour indiquer que l'on peut compiler pour Windows 7 en 64 bits et en Mac OS X 64 Bits.
"win7-x64" sont "osx.10.11-x64" des identifiants appelés RID (Runtime IDentifier) la liste des RIDs disponibles ce trouvent sur cette page.
Comme nous avons modifier notre "project.json", il faut restaure les dépendances:
> dotnet restore
log : Restoring packages for C:\Temp\netcore\project.json...
log : Installing runtime.unix.System.Private.Uri 4.0.1.
log : Installing runtime.osx.10.10-x64.runtime.native.System.IO.Compression 1.0.1.
log : Installing runtime.osx.10.10-x64.Microsoft.NETCore.DotNetHost 1.0.1.
log : Installing runtime.osx.10.10-x64.Microsoft.NETCore.Jit 1.0.4.
log : Installing runtime.osx.10.10-x64.runtime.native.System.Net.Security 1.0.1.
log : Installing runtime.unix.System.Net.Primitives 4.0.11.
log : Installing runtime.unix.System.Net.Sockets 4.1.0.
log : Installing runtime.unix.System.Console 4.0.0.
log : Installing runtime.osx.10.10-x64.Microsoft.NETCore.DotNetHostResolver 1.0.1.
log : Installing runtime.osx.10.10-x64.runtime.native.System.Net.Http 1.0.1.
log : Installing runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography 1.0.1.
log : Installing runtime.unix.Microsoft.Win32.Primitives 4.0.1.
log : Installing runtime.unix.System.IO.FileSystem 4.0.1.
log : Installing runtime.osx.10.10-x64.runtime.native.System 1.0.1.
log : Installing runtime.unix.System.Runtime.Extensions 4.1.0.
log : Installing runtime.unix.System.Diagnostics.Debug 4.0.11.
log : Installing runtime.osx.10.10-x64.Microsoft.NETCore.DotNetHostPolicy 1.0.1.
log : Installing runtime.osx.10.10-x64.Microsoft.NETCore.Runtime.CoreCLR 1.0.4.
log : Writing lock file to disk. Path: C:\Temp\netcore\project.lock.json
log : C:\Temp\netcore\project.json
log : Restore completed in 55391ms.
On constate que tous les runtimes sont téléchargés et installés.
Compilons notre application application:
> dotnet build
on peut voir dans notre dossier "bin/Debug/netcoreapp1.0/" un nouveau dossier "win7-x64" dans lequel se trouvent "{project}.dll" qui est notre application et "{project}.exe" qui est l'exécutable qui démarre le framework et ensuite notre application.
On peut appeler cette directement notre application sans dotnet:
> .\bin\Debug\netcoreapp1.0\win7-x64\netcore.exe
mais dotnet run fonctionne toujours.
Si on demande à publier notre application:
> dotnet publish
Un dossier "bin/Debug/netcoreapp1.0/win7-x64/publish" apparaît avec l'application et toutes les DLL nécessaires poour exécuter notre programme. Si on regarde le contenu on a un dossier d'environ 45 Mo. C'est un peu beaucoup pour un simple Hello World :)
Si vous publiez l'application en version portable, vous aurez un dossier avec uniquement la DLL de l'application (5 Ko).
Modifions notre programme pour lui ajouter un calcul de la suite de Fibonacci.
using static System.Console;
namespace ConsoleApplication
{
public class Program
{
public static int FibonacciNumber(int n)
{
int a = 0;
int b = 1;
int tmp;
for (int i = 0; i < n; i++)
{
tmp = a;
a = b;
b += tmp;
}
return a;
}
public static void Main(string[] args)
{
WriteLine("Hello World!");
WriteLine("Fibonacci Numbers 1-15:");
for (int i = 0; i < 15; i++)
{
WriteLine($"{i+1}: {FibonacciNumber(i)}");
}
}
}
}
On compile et on exécute notre application.
> dotnet build
> .\bin\Debug\netcoreapp1.0\win7-x64\netcore.exe
Hello World!
Fibonacci Numbers 1-15:
1: 0
2: 1
3: 1
4: 2
5: 3
6: 5
7: 8
8: 13
9: 21
10: 34
11: 55
12: 89
13: 144
14: 233
15: 377
Bon ca fonctionne, mais tout mettre dans un seul fichier ce n'est pas une très bonne idée, généralement un projet possède une multitude de fichiers. Nous allons donc déplacer notre calcul de Fibonacci dans un fichier a part.
Créez un nouveau fichier "Fibonacci.cs":
using System;
namespace Fibonacci{
public class FibonacciEngine {
public static int FibonacciNumber(int n)
{
int a = 0;
int b = 1;
int tmp;
for (int i = 0; i < n; i++)
{
tmp = a;
a = b;
b += tmp;
}
return a;
}
}
}
modifions notre "Program.cs" pour utiliser notre nouvelle classe:
using Fibonacci;
using static System.Console;
namespace ConsoleApplication
{
public class Program
{
public static void Main(string[] args)
{
WriteLine("Hello World!");
WriteLine("Fibonacci Numbers 1-15:");
for (int i = 0; i < 15; i++)
{
WriteLine($"{i+1}: {FibonacciEngine.FibonacciNumber(i)}");
}
}
}
}
On compile et on exécute:
> dotnet build
> .\bin\Debug\netcoreapp1.0\win7-x64\netcore.exe
Hello World!
Fibonacci Numbers 1-15:
1: 0
2: 1
3: 1
4: 2
5: 3
6: 5
7: 8
8: 13
9: 21
10: 34
11: 55
12: 89
13: 144
14: 233
15: 377
tout ce passe bien, on constante donc qu'à partir du moment où on ajoute un fichier ".cs" dans le dossier, il est compilé et intégré à l'application. Pas besoin d'enregistrer les fichiers dans un fichier projet comme pour les ".csproj". Les fichiers ".cs" peuvent se trouver également dans une sous-dossier.
Nos projets font généralement référence à des librairies tiers, c'est ce qu'on appelle les "dépendances". Dans le monde .NET les dépendances sont gérer avec l'outil Nuget avec son dépôt principal : http://www.nuget.org
Dans les projets .NET Core, les dépendances sont définies dans le section "dependencies" du "project.json".
"dependencies": {
"Newtonsoft.Json": "9.0.1",
"SwissEphNet": "2.5.*"
}
Chaque élément est constitué du nom du paquet, avec comme valeur la version utilisée. Sans entrer dans les détails de la gestion des versions Nuget (consultez cette page dans la documentation Nuget), dans l'exemple fourni on requiert la plus petite version de "Newtonsoft.Json" a partir de la version "9.0.1", et la version la plus récente de "SwissEphNet" a partir de la version "2.5".
La section "dependencies" concerne l'ensemble du projet, si vous ciblez plusieurs frameworks, chaque dépendance sera référencée par chaque cible. Ce qui peut poser des problèmes, en effet certaines librairies ne supportent pas certains framework à partir d'une version, où certaines librairies sont utilisées pour compenser un manque (par exemple la gestion des code pages "Windows-1252" dans les applications .NET Core).
Pour gérer cette situation, on a la possibilité de définir une section "dependencies" pour chaque framework cible, et dans ce cas les dépendances ne s'appliquent qu'au framework concerné. Par exemple:
"frameworks"{
"net40":{
"dependencies":{
"Library": "4.0"
}
},
"netstandard1.6":{
"dependencies":{
"Library": "7.0",
"System.Text.Encoding.CodePages": "4.0.1"
}
}
}
Nous nous amusons bien avec dotnet, mais dans notre réalité quotidienne, un projet seul est rare, généralement nous avons une application ainsi que diverses librairies, et des tests. Nous avons donc un ensemble de projets qui se lient mutuellement. Sous VS nous utilisons une "Solution" qui contient l'ensemble des projets qui forment un tout.
En .NET Core nous avons également une notion de "Solution": un dossier qui englobe tous les dossiers de chaque projet.
La structure recommandée par .NET est la suivante:
/Solution
|_/src
|_/test
|_global.json
Dans le dossier src se trouve tous les dossiers de chaque projet.
Dans le dossier test se trouve tous les dossiers de chaque projet de tests.
Le fichier global.json (qui n'est pas obligatoire mais fortement recommandé) permet de configurer de "dossier global", surtout si vous désirez modifier l'infrastructure. Vous trouverez plus d'informations sur ce fichier ici.
De cette manière chaque projet va pouvoir référencer un autre projet de la même "Solution".
Faisons un petit test. Dans un nouveau dossier vide:
> md src
> md test
> cd src
> md MyApp
> cd MyApp
> dotnet new
> cd ..
> md MyLibrary
> cd MyLibrary
> dotnet new -t lib
> cd ..\..
Nous avons créé une application, et une librairie (remarquez la commande dotnet new -t lib avec l'argument "-t lib" pour définir une librairie comme modèle de projet).
Dans la racine de notre solution on var créer un fichier "global.json":
{
"projects": ["src", "test"]
}
de cette manière nous indiquons à dotnet où trouver les projets. Depuis cette racine restaurons les dépendances.
> dotnet restore
log : Restoring packages for C:\Temp\netcore\src\MyApp\project.json...
log : Restoring packages for C:\Temp\netcore\src\MyLibrary\project.json...
log : Writing lock file to disk. Path: C:\Temp\netcore\src\MyLibrary\project.lock.json
log : C:\Temp\netcore\src\MyLibrary\project.json
log : Restore completed in 1506ms.
log : Writing lock file to disk. Path: C:\Temp\netcore\src\MyApp\project.lock.json
log : C:\Temp\netcore\src\MyApp\project.json
log : Restore completed in 2315ms.
On constate que nos deux projets ont été détectés et restaurés.
Modifions notre librairie, dans le fichiers "src/MyLibrary/Library.cs"
using System;
namespace MyLibrary
{
public class Operations
{
public double Add(double a, double b)
{
return a+b;
}
public double Mul(double a, double b)
{
return a*b;
}
}
}
Je ne vous ferais pas l'affront de vous expliquer ce code :)
Maintenant nous allons référencer notre librairie dans notre application, ouvrons "src/MyApp/project.json" et ajoutons une dépendance à la librairie:
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable",
"emitEntryPoint": true
},
"dependencies": {
"MyLibrary": "1.0.0"
},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.1"
}
},
"imports": "dnxcore50"
}
}
}
la version de la librairie est définie dans son "project.json".
Faisons un dotnet restore pour s'assurer que la référence est correcte.
A partir du moment où on défini les dépendances Nuget au même endroit que les références aux projets librairies, une petite explication sur le processus de résolution s'impose.
Lorsque dotnet résoud les dépendances, il suit le processus suivant:
donc les projets sont prioritaires.
Modifions "src/MyApp/Program.cs" pour utiliser notre librairie:
using System;
using MyLibrary;
namespace ConsoleApplication
{
public class Program
{
public static void Main(string[] args)
{
var op = new Operations();
double a = 12.34;
double b = 98.76;
Console.WriteLine($"Add = {op.Add(a,b)}");
Console.WriteLine($"Mul = {op.Mul(a,b)}");
}
}
}
Compilons et exécutons
> cd src\MyApp
> dotnet build
> dotnet run
Add = 111,1
Mul = 1218,6984
Le dernier type de projet que l'on exploite souvent se sont les tests unitaires. .NET Core intègre la notion de test unitaires avec le support de différentes librairies. Les tests Microsoft existe toujours, mais personnellement je préfère xUnit. Ca tombe bien cette librairie est supportée :) Créons un projet pour les tests de notre librairie.
> cd ..\..
> cd test
> md MyLibrary.Tests
> cd MyLibrary.Tests
> dotnet new -t xunittest
Comme vous voyez il existe un type de projet "xunittest" qui prépare ce qu'il faut pour nous. Modifions le "project.json" pour référencer notre librairie.
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable"
},
"dependencies": {
"System.Runtime.Serialization.Primitives": "4.1.1",
"xunit": "2.1.0",
"dotnet-test-xunit": "1.0.0-rc2-build10025",
"MyLibrary": "1.0.0"
},
"testRunner": "xunit",
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.1"
}
},
"imports": [
"dotnet5.4",
"portable-net451+win8"
]
}
}
}
au passage j'ai modifié la version de "dotnet-test-xunit". On restaure
> dotnet restore
Ecrivons nos tests dans "src/test/MyLibrary.Tests/Tests.cs"
using System;
using Xunit;
using MyLibrary;
namespace Tests
{
public class Tests
{
[Fact]
public void TestAdd()
{
var op = new Operations();
Assert.Equal(12.34, op.Add(10.10, 2.24));
}
[Fact]
public void TestMul()
{
var op = new Operations();
Assert.Equal(22.624, op.Mul(10.10, 2.24), 3);
}
}
}
On compile et on exécute nos tests
> dotnet build
> dotnet test
xUnit.net .NET CLI test runner (64-bit .NET Core win10-x64)
Discovering: MyLibrary.Tests
Discovered: MyLibrary.Tests
Starting: MyLibrary.Tests
Finished: MyLibrary.Tests
=== TEST EXECUTION SUMMARY ===
MyLibrary.Tests Total: 2, Errors: 0, Failed: 0, Skipped: 0, Time: 0,228s
SUMMARY: Total: 1 targets, Passed: 1, Failed: 0.
Voilà nous avons fait un peu le tour de .NET Core, ce n'est qu'un aperçu car le sujet est vaste.
J'espère que ca vous aide à y voir plus clair.
A bientôt,
Yanos