Microservices en 10 min, partie 2: appels entre services

Les liens entre les services sont complexes à gérer car ils nécessitent d’aborder certaines problématiques qui sont, souvent, inexistantes dans le cas d’une application monolithe. Les choix effectués pour définir et implémenter ces liens doivent respecter une règle d’or:
“Peut-on modifier et déployer un service sans en impacter un autre ?”

Les problématiques les plus importantes concernant les appels entre services sont:

  • Le choix des interfaces
  • Le versionnement des interfaces
  • Le tolérance aux erreurs
  • Effectuer des appels synchrones/asynchrones
  • Le topologie des appels

Choix des interfaces

Le choix des interfaces entre microservices est délicat car une mauvaise définition de ces interfaces peut contraindre à de nombreux refactorings qui impacteront plusieurs services. Il faut donc définir des interfaces pour qu’elles soient le plus stable possible de façon à minimiser les changements:

  • Eviter d’utiliser des interfaces avec des types trop abstraits (comme object par exemple): il est préférable d’utiliser des types précis quitte à multiplier les fonctions. Dans le cas où on utilise plusieurs fonctions, une modification peut amener à modifier quelques signatures sans devoir modifier toutes les signatures. Les quelques fonctions modifiées peuvent ne pas impacter tous les clients du service. D’autre part, utiliser des interfaces trop indéfinies nécessitent une connaissance “à priori” de la part des clients sur les types réels utilisés. Il ne pourra pas “découvrir” les interfaces.
  • Utiliser une approche API first: il faut définir les interfaces entre 2 microservices avant de les implémenter. La définition des interfaces permet de se mettre d’accord avec tous les clients et éviter des incompréhensions.
  • Etre flexible à la lecture pour être plus robuste aux changements d’interfaces.
Exemple de technique utilisée pour être flexible à la lecture

Martin Fowler a appelé cette astuce Tolerant Reader, elle permet d’être plus tolérant aux changements dans les réponses des microservices. Pour chercher un objet dans un fichier XML, on peut utiliser le XPath relatif ou le XPath absolu.

Si on prends l’exemple XML suivant:

<catalog>  
  <book>  
    <title>XML</title>  
  </book>  
</catalog>

Pour lire le titre dans le nœud title, on peut exécuter le code suivant:

using System.Xml;  
...  
  
string xml = ...  
  
XmlDocument doc = new XmlDocument();  
doc.LoadXml();  
XmlNode titleNodeUsingAbsPath = doc.SelectSingleNode("/catalog/book/title");  
XmlNode titleNodeUsingRelPath = doc.SelectSingleNode("//book/title");

La requête peut être effectuée en utilisant le XPath absolu avec la syntaxe /catalog/book/title.
Si on utilise le XPath relatif, la syntaxe utilisée est //book/title.

Dans le cas où le fichier XML change de structure:

<catalog>  
  <content>  
    <book>  
      <title>XML</title>  
    </book>  
  </content>  
</catalog>

Le XPath relatif avec la même syntaxe permet toujours de récupérer le contenu du nœud title. En revanche la version avec le XPath absolu ne permet plus de récupérer le nœud title.

Utiliser le “semantic versioning”

Le “semantic versioning” permet d’indiquer qu’une version contient des breaking changes:

  • Les versions sont numérotées en utilisant 3 nombres: MAJOR.MINOR.PATCH (par exemple 4.7.1)
  • Un incrément du nombre MAJOR indique un breaking change avec la version précédente.
  • Un incrément du nombre MINOR indique l’ajout d’une fonctionnalité rétrocompatible.
  • Un incrément du nombre PATCH indique des corrections de bugs rétrocompatibles.

En cas de changement de version, les clients du service peuvent savoir l’importance des changements effectués et envisager la mise à jour de leurs interfaces.

Créer un nouveau point d’accès en cas de “breaking changes”

Pour éviter un impact trop fort sur les clients d’un service en cas de breaking changes des interfaces, une méthode consiste à procéder par étape:

  • Etape 1: utilisation des interfaces V1 par tous les clients.
  • Etape 2: on introduit la nouvelle interface V2 et on indique aux clients de migrer vers l’interface V2. Les clients migrent au fur et à mesure. On maintient les 2 interfaces V1 et V2 pour ne pas trop impacter les clients qui utilisent toujours les interfaces V1. On indique aux clients une date à partir de laquelle V1 ne sera plus disponible.
  • Etape 3: quand tous les clients ont migré, on supprime les interfaces V1 et tous les clients utilisent V2.

Appels synchrones ou asynchrones

On peut se poser la question de savoir comment effectuer des appels entre les clients et leurs microservices:

  • Effectuer des appels synchrones: le client envoie une requête au microservice, il attend pendant le traitement de la requête et il récupère la réponse du service en fin de traitement.
  • Effectuer des appels asynchrones: le client envoie une requête au microservice mais n’attend pas pendant le traitement. Le client peut être notifié à la fin du traitement directement par le service ou le client s’abonne à des évènements déclenchés par le service de façon à recevoir des notifications.

Les appels synchrones correspondent au modèle request/response. Le client doit s’adapter au temps de traitement du service et doit prendre en compte ce temps de traitement dans son implémentation. Prendre en compte le temps de traitement permet, par exemple, d’éviter des timeouts dans le cas où le temps est trop long. Si le temps de traitement se rallonge suivant le type de requête, le client devra s’adapter en conséquence. L’avantage des appels synchrones est que le client peut avoir une réponse immédiate sur le statut de la requête. Du fait de l’adaptation du client en fonction du temps de traitement, ce type d’appels augmente le couplage entre service.

A l’opposé les appels asynchrones permettent d’éviter une adaptation du client suivant le temps de traitement de la requête. Ce type d’appels correspond au modèle orienté évènement. Le client s’abonne à des évènements du service en fonction de ce pourquoi il veut être notifié. L’émetteur de l’évènement n’a pas de connaissances des clients qui s’abonnent. Ce type d’appels permet de moins coupler les clients à leur service.

Suivant la topologie des échanges entre un microservice et ses clients, on peut être amener à faire un choix dans le type d’appels à implémenter:

Un à un Un à plusieurs
Synchrone Request/Response N/A
Asynchrone Notification Publication/Souscription
Request/Response asynchrone Publication/Réponses asynchrones

Effectuer des appels asynchrones est plus complexe qu’effectuer des appels synchrones. On peut distinguer 2 façons de faire des appels asynchrones:

  • Mécanisme de souscription: le client souscrit auprès du microservice et il est notifié au déclenchement d’un évènement.
  • Mécanisme d’observation: le client fait du polling auprès du microservice sur lequel il a effectué un appel. Le client déclenche lui-même ses actions en fonction de ce qu’il a découvert pendant le polling.

Tolérer les échecs partiels

Les microservices sont de petites applications conçues pour être autonomes. Pourtant pour effectuer un traitement, microservice peut devoir faire appel à d’autres services et ainsi de suite. Si les services appelés ne sont pas en mesure de répondre ou si une partie du réseau est en défaut, le microservice ne sera pas en mesure d’effectuer son traitement et ne répondra pas à son tour à une requête qui lui a été adressée. Un défaut dans un service de bas niveau peut, ainsi être propagé à d’autres services de plus haut niveau mettant en péril toute l’application.

L’implémentation d’un microservice doit donc tolérer les échecs lors des appels à d’autres microservices. Il faut prendre en compte les scénarios d’échecs lors des appels pour ne pas propager l’échec et donner une réponse même si le traitement n’a pas abouti. Une première approche est d’être en mesure de détecter un échec lors d’un appel à travers le réseau en mettant en place des timeouts.

Quand l’échec est détecté, il faut implémenter une logique en rapport avec les impératifs du contexte fonctionnel. Par exemple, dans certains cas il peut être inutile de répéter une requête car les paramètres de la requête peuvent être obsolètes en cas d’appels répétés. Dans d’autres cas, on peut se permettre d’effectuer de nouvelles tentatives. Quel que soit la solution implémentée, il faut avoir une logique pour le cas le plus défavorable. Ainsi dans le cas de requêtes répétées et non traitées, il faut prévoir un nombre maximum de requêtes en défaut et avoir un traitement particulier si ce nombre est atteint comme, par exemple, considérer le service appelé comme inaccessible et ne plus envoyer de requêtes vers ce service.

Pattern “circuit breaker”

Le pattern circuit breaker vise à apporter une solution homogène lors d’échecs dans les appels entre microservices. La solution consiste à placer entre les microservices un composant appelé circuit breaker. Ce composant analyse les appels d’un microservice à l’autre et détecte les cas où un appel n’a pas abouti.

Dans le schéma suivant, on peut voir que le circuit breaker se place entre le client et le service (appelé supplier sur le schéma) et sert d’intermédiaire entre les appels du client au service. Dans le cas où les appels aboutissent, le circuit breaker n’effectue aucun traitement particulier. En revanche, s’il détecte un appel non abouti vers un service après un timeout, il “ouvre” le circuit pour que les futurs appels vers ce service ne soit plus effectués. Le circuit breaker répondra systématiquement par un échec pour les appels suivants au service (mécanisme de heartbeat).


Source: https://martinfowler.com/bliki/CircuitBreaker.html

Dans des implémentations plus sophistiquées, le circuit breaker peut effectuer des appels vers le service en défaut pour détecter s’il redevient opérationnel de façon à refermer le circuit et à rediriger, à nouveau, les appels.

L’interruption des appels par le circuit breaker peut se faire lorsque les requêtes en échec dépasse un certain seuil et pas forcément à partir du premier appel non abouti. Le circuit breaker peut aussi indiquer à des outils de monitoring qu’un service est en défaut.

Netflix Hystrix
Netflix Hystrix est un exemple d’implémentation du pattern circuit breaker en java. Hystrix est disponible sur Github.

Chef d’orchestre ou chorégraphie

Dans le cas de workflows, un traitement peut nécessiter d’appeler successivement plusieurs microservices. La logique de ces workflows peut être implémentée de 2 façons dans le service appelant:

  • Appels en chef d’orchestre (i.e orchestration): le workflow est implémenté complètement dans le service appelant. Il sait exactement quels sont les services qu’il doit appelé et l’ordre d’appel de ces services. Le service appelant, appelle successivement les services comme s’il était un chef d’orchestre.
  • Appels en chorégraphie (i.e. choregraphy): le service principal (ou service master) ne connaît pas les services qui dépendent de lui. Les autres services souscrivent auprès du service master pour être notifié quand un évènement particulier survient. Quand un traitement doit être effectué par le service master, il déclenche certains évènements. Les services qui se sont abonnés sont notifiés du déclenchement de ces évènements et effectuent un traitement en fonction de l’évènement. Le cas échéant, chaque service peut renvoyer un résultat à la suite du déclenchement d’un évènement.
    Ce mécanisme correspond à une chorégraphie puisque les services abonnés s’abonnent eux-mêmes au service master et décident eux-mêmes d’effectuer un traitement.

Dans le cas du chef d’orchestre, l’ordonnancement entre microservices est plus facile à implémenter et se fait directement dans le service master. Cette implémentation donne la possibilité d’effectuer des appels synchrones aux services, ainsi on peut facilement stopper le workflow si un appel à un service n’aboutit pas.
L’inconvénient majeur de cette approche est que le service master connaît tous les services qu’il doit appeler et la logique d’ordonnancement est implémentée directement dans le service ordonnanceur.
Cette connaissance des autres services augmente le couplage entre service puisque il peut être nécessaire de modifier l’ordonnanceur si une interface change dans les services appelés.

La chorégraphie est plus complexe à implémenter que le mécanisme de chef d’orchestre. Dans le cas de la chorégraphie, il peut être plus difficile d’interrompre le workflow en cas d’erreurs, en particulier si le déclenchement des évènements se fait de façon asynchrone. De même avec ce type de mécanisme, il est plus complexe d’effectuer un ordonnancement entre les services en fonction des évènements déclenchés.
L’intérêt du mécanisme en chorégraphie est que le service master n’a pas de connaissances des services appelés. Il ignore aussi l’ordre dans lequel les services doivent effectuer leur traitement. Le service master se contente de déclencher des évènements et ce sont les services eux-mêmes qui ont la connaissance de savoir s’ils doivent s’exécuter ou non. Ainsi la chorégraphie permet de diminuer le couplage entre microservices.

Service discovery

Lorsque des microservices sont exécutés sur des machines différentes, il faut avoir certaines informations pour savoir comment appeler ces services comme l’adresse IP des services ou les ports de connexions. Dans le cas d’autoscaling où des instances de service sont rajoutées à “chaud”, comment savoir qu’une nouvelle instance est active ?

Toutes ces problématiques peuvent se résoudre de 2 façons:

  • En connaissant la configuration des services en avance pour savoir comment les appeler ou
  • Découvrir cette configuration “à chaud” c’est-à-dire pendant l’exécution sans la connaître au préalable.

Connaître la configuration des services en avance

Cette solution est la plus rapide à implémenter et convient bien dans le cas où il n’y a pas beaucoup d’instances de services et que la topologie des services ne change pas. Dans le cas où la configuration change fréquemment, avoir les paramètres de connexion des services avant exécution peut être assez contraignant car le moindre changement peut nécessiter le redémarrage des services et l’interruption de l’application.

Ce type de configuration peut rendre plus difficile l’assignation dynamique d’une adresse IP aux différents services, par exemple, pour assurer des fonctions comme l’autoscaling ou le load-balancing. D’une façon générale, configurer les paramètres des connexions en avance nécessite d’indiquer ces paramètres dans la configuration des services. Cette connaissance des paramètres amène un couplage des clients à leurs services.

Pattern “client-side discovery”

Découvrir la configuration des services à l’exécution permet une affectation dynamique des paramètres de connexion. Un exemple de mécanisme permettant la configuration “à chaud” est le pattern client-side discovery. Ce mécanisme nécessite un service qui référence les paramètres de connexion des autres services.

  • Dans un premier temps, chaque service indique sa configuration auprès du service registry qui va la conserver.
  • Chaque client souhaitant effectuer une requête auprès d’un service, doit au préalable récupérer la configuration auprès du service registry.
  • Le client fait ensuite appel directement au service avec les paramètres qu’il a obtenu après avoir interrogé le service registry.

Ce mécanisme permet l’implémentation d’algorithmes de load-balancing directement dans le client. Le client peut effectuer une requête à une instance particulière d’un service en fonction de la charge. L’inconvénient du pattern client-side discovery est que les logiques de connexion au service et le load-balancing sont implémentées dans chaque client. Il n’y a pas un composant qui effectue ce traitement de façon homogène pour tous les clients.

Il existe des exemples d’implémentation du pattern client-side discovery dans Netflix OSS (Netflix Open Source Software) disponible sur GitHub:

Pattern “server-side discovery”

Le pattern server-side discovery ajoute un composant par rapport au pattern client-side discovery. Un router est ajouté pour servir d’intermédiaire entre le client et les services. Le client ne fait plus appel directement aux services:

  • Dans un premier temps, le client appelle le router dans le but d’effectuer une requête auprès d’un service.
  • Le router effectue une requête auprès du service registry pour récupérer les paramètres de connexion du service.
  • Le router appelle, ensuite, directement les services avec la requête du client.

Les autres mécanismes sont les mêmes que pour le client-side discovery c’est-à-dire:

  • Les services enregistrent leur configuration auprès du service registry,
  • Seulement le service registry possède la configuration des services.

L’intérêt du server-side discovery est de permettre l’implémentation de l’algorithme de load balancing à un seul endroit c’est-à-dire dans le router. Il n’est pas nécessaire d’implémenter le load-balancing dans chaque client.

Il existe quelques exemples de router:

  • Kubernetes et Marathon exécutent un proxy sur chaque host d’un cluster pour effectuer du load balancing.

Exemples de service registry:

  • Etcd: base de données clé-valeur distribuée ayant des fonctionnalités de service discovery,
  • Consul: propose une API pour enregistrer et découvrir “à chaud” des services,

Quelques technologies utilisées pour les appels

Il est possible d’utiliser une multitude de technologies pour effectuer des appels entre microservices. Le choix de la technologie doit correspondre aux besoins toutefois d’une façon générale:

  • Il faut éviter d’utiliser des middlewares propriétaires qui dirigent trop l’implémentation des microservices.
  • Il faut prendre en compte les risques liés au réseau
  • Il faut distinguer les technologies synchrones et asynchrones.

Les techonologies les plus couramment utilisées dans le cas de communications synchrones sont:

Dans le cas de communications asynchrones par message:

Outre le protocol utilisé, il faut aussi faire un choix sur le format des messages:

  • Format texte: les formats XML et JSON sont couramment utilisés.
  • Format binaire: on peut considérer Apache Avro ou Protocol Buffers.

Communications asynchrones par messagerie

Il existe beaucoup d’implémentations permettant des communications par messagerie:

Toutes ces implémentations permettent une plus grande flexibilité que les communications synchrones classiques:

  • Découplage entre les clients et les services: le client envoie sa requête sur un canal sans connaître le service qui va la traiter. Il n’y a pas de mécanismes pour chercher le service.
  • Message tampon: les messages sont placés dans des files d’attente et seront traités de façon asynchrone par le service même s’il n’est pas disponible au moment de l’envoi du message.
  • Interactions flexibles entre les clients et les services: la plupart des mécanismes sont supportés.
  • Communications interprocessus explicites: il n’y a pas de différences entre un appel à un service local ou à distance.

Ces technologies offrent plus de flexibilité toutefois elles sont plus complexes à mettre en œuvre car il faut installer un agent de messagerie et il faut configurer les communications.
D’autres part, les communications request/response deviennent complexes à mettre en œuvre car les communications se font par des canaux qui sont identifiés avec des ID. Le client doit corréler sa requête avec la réponse en utilisant l’ID du canal.

Communications synchrones

Le mécanisme le plus courant utilisé est celui permettant d’effectuer des appels request/response entre le client et le service. Le protocole utilisé est souvent REST over HTTP.

Certaines solutions permettent d’effectuer des appels request/response de façon asynchrone sans passer par des solutions de messagerie, par exemple:

REpresentational State Transfer (REST)

On parle souvent de REST pour effectuer des appels entre microservices en le qualifiant de protocole. Il s’agit d’un abus de langage car REST n’est ni un protocole, ni une norme. REST a été introduit dans la thèse de Roy Fielding qui le définit comme un style d’architecture imposant des contraintes. Ces contraintes concernent différents points:

  • Communication Client-Serveur: le client est séparé du serveur.
  • Communication sans état: une requête doit contenir toutes les informations nécessaires à son exécution. Le serveur ne doit pas stocker de données de contexte.
  • Mise en cache: une réponse du serveur contient des informations pour que le client puisse mettre en cache la réponse. Ces informations peuvent être considérées comme la durée de validité de la réponse.
  • Des interfaces uniformes: les interfaces permettent d’identifier les ressources disponibles. La manipulation de ces ressources doit se faire au travers d’une représentation. Les messages doivent être autodescriptifs c’est-à-dire qu’ils doivent suffire à comprendre les informations qu’ils contiennent.
  • Système hiérarchisé en couches: un client peut se connecter à un serveur final ou à un intermédiaire sans qu’il s’en aperçoive. Le fait de passer par un intermédiaire doit être transparent pour le client. Cette contrainte permet d’effectuer du load-balancing.
  • Code-on-demand: cette contrainte permet d’exécuter des scripts récupérés à partir du serveur. Tous les traitements ne s’effectuent pas du coté du serveur.

On applique REST le plus souvent partiellement dans les appels entre microservices toutefois son intérêt est d’énoncer des contraintes s’appliquant à ces appels notamment:

  • Le client est désolidarisé du serveur,
  • Il n’y pas de gestion d’états,
  • Une requête peut être répartie sur plusieurs serveurs,
  • On peut utiliser HTTP
  • Une API REST implémentée totalement n’a pas besoin de documentation,
  • Le client peut “découvrir” les fonctionnalités proposées par l’API sans connaissances préalables.

REST over HTTP

REST over HTTP est un protocole utilisant HTTP. L’intérêt de HTTP est de mettre à disposition des éléments qui facilitent la mise en œuvre d’appels entre client et serveur:

  • URL: une adresse qui permet d’indiquer l’adresse du microservice.
  • Type MIME: les messages peuvent être de type différent, on peut indiquer le type dans l’entête des messages. Le plus couramment on utilise JSON ou XML.
  • Verbes HTTP: ce sont des méthodes qui permettent des traitements particuliers:
    • GET pour effectuer des opérations de lecture d’une ressource,
    • POST pour créer une ressource,
    • PUT pour mettre à jour une ressource et
    • DELETE pour supprimer une ressource.

Les codes d’erreurs HTTP sont aussi très utiles puisqu’ils indiquent des codes de retours possibles pour les appels:

Code Message Signification
200 OK Succès pour toutes les méthodes sauf POST
201 Created Réponse à un POST
400 Bad Request Le contenu de la requête n’a pas été compris
401 Unauthorized L’authentification a échouée
403 Forbidden Authentification correcte mais l’utilisateur ne peut pas accéder à la ressource
404 Not Found Le ressource n’a pas été trouvée
429 Too Many Requests La limite de requêtes autorisées est dépassée
500 Internal Error Problème interne au service
503 Service unavailable Service non disponible

API RESTful

Une API RESTful définit une API qui respecte toutes les contraintes définies par REST. La très grande majorité du temps, les API ne respectent pas l’intégralité de REST, elles prennent en compte quelques contraintes. De façon à mesurer le niveau de maturité d’une API avec REST, Leonard Richardson a défini 4 niveaux:

  • Plus le niveau est élevé et plus l’API respecte les contraintes REST
  • Plus le niveau est élevé et moins le client a besoin d’informations préalables pour envoyer sa quête au service.
  • Plus le niveau est élevé et plus le couplage est faible.

Niveau 0

Les clients envoient des requêtes HTTP POST vers un seul point d’accès du service. Chaque requête contient:

  • L’action à effectuer,
  • L’objet cible sur lequel va porter l’action,
  • Les paramètres nécessaires à l’exécution de l’action.

Ce niveau nécessite une connaissance du client pour pouvoir envoyer sa requête. Le client ne peut pas exécuter la requête sans savoir où indiquer les différents éléments.

Par exemple, si on souhaite récupérer une liste de livres en faisant appel à une API REST, une requête pourrait être:

POST /books HTTP/1.1 
Content-Type: application/json

La réponse de l’API pourrait être:

{ 
  "books": [ 
       { 
            "title": "The Little Prince",  
            "id": "1", 
            "author": { 
                   "firstname": "Antoine", 
                   "lastname": "Saint-Exupery" 
            }  
       }, 
      { 
            "title": "Madame Bovary",  
            "id": "2", 
            "author": { 
                   "firstname": "Gustave", 
                   "lastname": "Flaubert" 
            }  
       } 
   ] 
}

Pour supprimer un livre, la requête pourrait être:

POST /books HTTP/1.1 
Content-Type: application/json 
{ 
    "delete": { 
        "book": [ 
             {     "id": "1"    },  {     "id": "2"    } 
        ]  
    } 
}

Quelque soit l’action a effectué, la méthode HTTP utilisée est POST. Le corps de la requête contient:

  • l’action à effectuer c’est-à-dire: delete;
  • l’objet cible à savoir: book et
  • les paramètres nécessaires à l’exécution: 1 et 2.

Le client doit connaître la syntaxe de la requête pour pouvoir l’exécuter. Cette connaissance couple le client avec le service.

Niveau 1

L’API supporte la notion de ressource. Une ressource correspond à un élément sur lequel on souhaite effectuer une action. L’action peut être une création, une mise à jour, une suppression etc… Chaque requête à l’API REST concerne une ressource particulière.
Une requête contient:

  • L’action à effectuer auprès de la ressource,
  • Les paramètres nécessaires à l’exécution de l’action.

Chaque ressource comporte une identifiant rangé dans un champ dont le nom est Id. L’utilisation d’un nom de champ identique pour tous les identifiants permet d’éviter d’avoir une connaissance trop précise de la structure des ressources. On sait que quelque soit la ressource, l’identifiant sera rangé dans un champ dont le nom est Id.

Par exemple, pour avoir la liste des livres, une requête pourrait être:

POST /books HTTP/1.1 
Content-Type: application/json

La réponse de l’API pourrait être:

{ 
  "books": [ 
       { 
            "title": "The Little Prince",  
            "id": "1", 
            "authorId": "antoine_saint_ex" 
       }, 
      { 
            "title": "Madame Bovary",  
            "id": "2", 
            "authorId": "gustave_flaubert" 
       } 
   ] 
}

Les auteurs sont une ressource différente de celle des livres. La liste des livres ne comprends pas de données sur les auteurs, seuls les identifiants des auteurs sont utilisés. Les données sur les auteurs peuvent être obtenues en effectuant une requête spécifique pour les auteurs.

Pour obtenir des informations sur un auteur, on pourrait effectuer une requête:

POST /author?id=antoine_saint_ex HTTP/1.1 
Content-Type: application/json

La réponse contient les données de l’auteur avec un champ Id pour indiquer l’identifiant de l’auteur:

{ 
     "author": { 
          "id": "antoine_saint_ex", 
          "firstname": "Antoine", 
          "lastname": "Saint-Exupéry" 
    } 
}

Pour ce niveau, le client doit aussi connaître des éléments de syntaxe de la requête pour pouvoir l’exécuter. Toutefois certaines données comme, par exemple, les identifiants sont indiquées en utilisant un nom moins spécifique.

Niveau 2

Les requêtes effectuées utilisent des verbes HTTP pour indiquer l’action à effectuer:

  • GET pour une lecture,
  • POST pour une insertion,
  • PUT pour une mise à jour et
  • DELETE pour une suppression.

Comme pour les autres niveaux, les paramètres se trouvent dans le corps de la requête. A ce niveau, on utilise les codes de retours HTTP dans les réponses aux requêtes.

Par exemple, pour obtenir la liste de livres:

GET /books HTTP/1.1

Pour obtenir un auteur particulier:

GET /author?id=1 HTTP/1.1

Pour supprimer un livre:

DELETE /books?id=2 HTTP/1.1

A ce niveau, l’API paraît plus uniformisée. Il n’y a pas de connaissances à avoir sur la syntaxe des requêtes:

  • L’utilisation des verbes HTTP indiquent l’action à effectuer,
  • L’organisation en ressource permet d’avoir une logique qui est la même quel que soit le type d’objet.

Niveau 3

Une API satisfaisant ce niveau est basée sur le principe HATEOAS (Hypertext As The Engine Of Application State). HATEOAS est une contrainte qui permet d’indiquer dans la réponse à une requête GET, toutes les autres opérations possibles sur l’API. Ces opérations sont indiquées sous forme de lien hypertext dans le corps de la réponse.

Par exemple, si on envoie une requête pour obtenir la liste de livres:

POST /books?id=1 HTTP/1.1

Un exemple de réponse de niveau 3 pourrait être:

{ 
  "book":  
       { 
            "title": "The Little Prince",  
            "id": "1", 
            "authorId": "antoine_saint_ex", 
            "links": [ 
                 { 
                       "rel": "self", 
                       "href": "http://localhost:8080/books/1" 
                 }, 
                 { 
                       "rel": "list", 
                       "href": "http://localhost:8080/books" 
                 }, 
            ] 
       } 
}

La réponse contient des liens qui permettent de parcourir les ressources de l’API. En s’aidant de ces liens, la connaissance nécessaire pour utiliser l’API est encore abaissée et on peut “découvrir” toutes les fonctionnalités de l’API sans connaissances préalables.

Niveau de maturité

L’intérêt des niveaux de maturité n’est pas forcément d’implémenter le niveau le plus élevé pour une API. Généralement le niveau 2 suffit, dans la majorité des cas, à avoir une API standardisée facilement utilisable.

Utiliser des niveaux de maturité pendant le développement d’une API permet de:

  • Standardiser la syntaxe pour éviter aux clients d’avoir une connaissance préalable de cette syntaxe.
  • Utiliser un même niveau de maturité pour toutes les API de l’application en microservices: l’intérêt est d’éviter d’implémenter des niveaux inutiles s’il y a une trop grosse différence de maturité entre les services.
    Par exemple si un service implémente le niveau 3, et que tous les autres services implémentent le niveau 1, les clients de ce service peuvent ne pas utiliser les fonctionnalités des niveaux 2 et 3. Ce qui rend inutile l’effort d’implémentation jusqu’au niveau 3.
    Il est donc préférable d’avoir un niveau de maturité homogène dans une application en microservices.

Eviter les “anemic domain models”

La notion de ressource peut donner l’impression que les services REST ne font pas de traitements fonctionnels sur des objets et qu’ils ne permettent que de publier le contenu de ces objets.

Dans l’exemple plus haut, les ressources sont les livres ou les auteurs. Le service REST ne sert qu’à consulter ces objets comme on pourrait l’effectuer dans une base de données. Ce type de service peut mener à des anemic domain models.

Un anemic domain model est une notion introduite par Martin Fowler pour qualifier un modèle qui ne possède pas de logique mais seulement des propriétés. Le modèle n’a donc aucune logique fonctionnelle et se contente d’exposer ses propriétés. Dans le cas de microservices, il faut éviter d’implémenter ce type de REST API car ils font perdre l’intérêt des microservices. Même s’il peut être utile d’avoir un niveau de maturité permettant d’exposer des ressources, ça ne veut pas dire que le service REST sera dépourvu de logique fonctionnelle. Les ressources ne traduisent pas forcément un objet en base mais juste une notion issue d’un traitement fonctionnel.

Par exemple, si on prends le cas d’un service permettant d’autoriser des “deals” de marché financier en fonction de certains critères. Les données relatives au “deal” sont fournies lors de la requête. Le service effectue des contrôles sur ce “deal” en fonction des données fournies et donne une réponse contenant éventuellement une autorisation. Dans ce cas, la ressource est l’autorisation du “deal”. Cette ressource correspond à une notion obtenue après un traitement fonctionnel.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Microservices en 10 min, partie 1: Concevoir des microservices

Quelques définitions

Quelques définitions en préambule…

Architecture orientée service (SOA)

L’approche SOA a le même objectif que l’architecture en microservices:

  • Casser l’architecture en monolithe: un monolithe est une application qui est implémentée dans un seul projet. Cette architecture est généralement la plus facile à mettre en œuvre puisque toutes les problématiques d’implémentation et d’exécution (choix de la technologie de programmation, problématiques d’accès concurrents à une ressource, communications entre composants, déploiement, usine de build, intégration continue etc…) se posent pour un seul projet.
    A l’opposé ce type d’architecture rend plus difficile l’expérimentation de technologies exotiques. Elle peut devenir contraignante dans le cas où le code existant devient trop complexe à faire évoluer.
  • Permet de promouvoir la réutilisation de briques de services: isoler un service et le séparer d’un monolithe permet d’isoler des fonctionnalités et de faciliter leur réutilisation par plusieurs projets.
  • Facilite l’intégration de services: un des objectifs des services est de permettre de les appeler à partir de projets différents. L’utilisation de technologies de communication ou de middleware permet d’appeler des services en étant dans un processus différent à travers le réseau. Faciliter les communications entre processus permet de partager plus facilement une fonctionnalité.

L’approche SOA est considérée par beaucoup comme un échec car:

  • Trop théorique: beaucoup d’architectes ont écrit des articles pour décrire cette approche sans forcément donner des indications pratiques sur la façon de casser un monolithe et d’avoir une implémentation évolutive d’un service. Les services sont perçus comme des monolithes pour lesquels on a facilité les communications.
  • Pas de prise en compte des difficultés opérationnelles: l’approche trop théorique n’a pas apporté de solutions à des problématiques opérationnelles comme le déploiement, la scalabilité, le monitoring etc… Les implémentations de services peuvent se heurter à des difficultés à assurer des problématiques qui se résolvent plus facilement avec une approche en monolithe.
  • Protocoles de communication difficiles à utiliser: l’architecture en services a, parfois été vendue par des éditeurs de middlewares qui proposaient des solutions de communications souvent couteuses et propriétaires. Ces solutions avaient une empreinte forte sur l’implémentation des services ce qui couplaient les services au middleware.
  • Choix d’architecture peu évolutive et contraignante: l’utilisation de ces middlewares peut aussi rendre l’architecture en services peu évolutive et contraignante car très dépendantes des middlewares.

Qu’est-ce que l’approche en microservices ?

Les microservices sont un cas particulier des services: ce sont des services autonomes de petites tailles travaillant ensemble.

La plupart des définitions que l’on peut trouver évoque l’importance de l’autonomie et de la taille:


“En informatique, les microservices sont un style d’architecture logicielle à partir duquel un ensemble complexe d’applications est décomposé en plusieurs processus indépendants et faiblement couplés, souvent spécialisés dans une seule tâche.” “In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.”
Wikipedia
James Lewis and Martin Fowler


A la différence de l’approche SOA, l’approche Microservices a émergé dans le but de répondre plus facilement à des problématiques opérationnelles. L’architecture en microservices énonce des principes d’architectures en indiquant des solutions possibles et pratiques pour la plupart des problématiques.
Il n’est pas forcément pertinent d’appliquer rigoureusement toutes les solutions envisagées, tout dépend du contexte. L’important est d’avoir en tête des solutions possibles et d’adapter certaines d’entres elles à son contexte en fonction de ses problématiques propres.

Comme on l’a indiqué plus haut, l’objectif principal d’une architecture en microservices est de casser une application en monolithe pour la rendre moins complexe:




On peut énoncer quelques caractéristiques de l’architecture en microservices:

  • “Doing one thing well”: on doit tenter de limiter un microservice à une seule fonction. La règle n’est pas absolue mais chaque microservice doit avoir une responsabilité limitée. Les délimitations du microservice peuvent correspondre à des frontières fonctionnelles.
  • Autonomie: un microservice doit être autonome par rapport aux autres microservices. Le but est d’éviter de trop coupler les microservices entre eux.
  • Utiliser des technologies plus adaptées aux besoins: un atout des microservices est de permettre une plus grande libertée sur les choix technologiques par rapport à une application monolitique. La taille limitée d’un microservice rends plus facile des choix technologiques risqués ou exotiques. Les développeurs sont, ainsi, plus libres de leurs choix techniques en utilisant une technologie plus adaptée.
  • Tolérance aux pannes: les microservices peuvent être plus tolérants aux pannes par rapport à une application en monolithe. En cas d’échec d’un service, les autres services peuvent toujours fonctionner. L’absence d’un service dégrade l’application toutefois elle peut rester partiellement opérationnelle. A l’inverse, si une application en monolithe crashe, on peut difficilement la faire fonctionner de façon partielle, il faut généralement la redémarrer entièrement.
  • S’adapter à la charge: les microservices permettent de s’adapter plus facilement à la charge.
    Adapter la charge peut se faire de 2 façons:

    • Mise à l’échelle verticale (i.e. scale-up): pour augmenter les capacités de l’application, on augmente les capacités de la machine hôte. Ce type d’opération est couteux et plus difficile à mettre en œuvre car il faut remplacer la machine hôte et interrompre le fonctionnement de l’application pendant l’opération.
    • Mise à l’échelle horizontale (i.e. scale-out): l’augmentation des capacités de l’application se fait en augmentant le nombre d’instances. Ce type d’opération pose d’autres problèmes comme le load-balancing toutefois elle est moins couteuse qu’une augmentation des capacités de la machine hôte. En outre, elle donne la possiblité d’adapter la charge “à chaud” c’est-à-dire sans interruption de service en ajoutant ou en diminuant le nombre d’instances.

    L’approche en microservice rend plus facile la mise à l’échelle horizontale qui est la méthode la plus scalable et la moins couteuse.

  • Faciliter les déploiements: déployer un service peut être plus facile que de déployer une application entière. Il est plus facile de limiter l’interruption de service lorsqu’on déploie un service par rapport au déploiement d’une application complête. Pendant le déploiement d’un service, les autres services peuvent continuer à fonctionner. Dans le cas d’une application en monolithe, un nouveau déploiement implique l’arrêt complet de l’application.
    De même, il est moins risqué de déployer un service qu’une application entière. Si on constate un bug, on peut plus facilement effectuer un rollback de la nouvelle version.

Les microservices ne sont pas forcèment la solution idéale

L’architecture en microservices apporte de nombreux avantages toutefois, elle est loin d’être une solution idéale car elle déplace la compléxité de l’implémentation vers d’autres problématiques par rapport à une application en monolithe:

  • La compléxité des microservices n’est pas dans le code source comme pour un monolithe, mais dans les interactions entres les services.
  • Les microservices peuvent être très hétérogènes ce qui peut rendre leur implémentation plus complexe qu’un monolithe.

En réalité, une application en microservices est un système distribué. En plus de la compléxité fonctionnelle de l’application, se posent d’autres problématiques plus difficiles à résoudre que pour un monolithe comme par exemple:

  • Les communications entre services,
  • Le partitionnement de la base de données,
  • La modification d’un service par rapport au fonctionnement des autres services,
  • Les tests,
  • Le déploiement,
  • Etc…

Concevoir des microservices

Le but de cette partie est d’énoncer quelques principes pour la conception d’une architecture en microservices idéale. Il n’y a pas de solutions parfaites ou universelles, ces principes ne servent qu’à apporter quelques pistes de résolution qu’il convient d’appliquer en fonction du contexte.

Principe général

D’une façon générale, la conception de microservices doit assurer:

  • Un faible couplage: de façon à permettre de modifier les services indépendamment et d’assurer une autonomie dans leur fonctionnement. Des services faiblement couplés permettront de tirer partie au maximum de l’architecture en microservices: tolérance aux pannes, s’adapter à la charge, faciliter les déploiements, etc…
  • Grande cohésion: assurer une cohésion entre les services vise à rendre les échanges entre ces services de façon la plus cohérente possible en:
    • Utilisant des interfaces claires avec des types précis: par exemple, il faut éviter d’utiliser des types comme object pas assez précis qui laissent trop de libertés quant au type des objets. De même, il faut éviter de définir des fonctions qui ont plusieurs objectifs, il est préférable de limiter une fonction à un seul cas d’utilisation.
    • Eviter les choix technologiques trop exotiques dans les communications entre service: par exemple, il faut éviter d’utiliser des middlewares qui sont généralement couteux en licence et peuvent avoir une empreinte forte dans l’implémentation des services.
    • Eviter les breaking changes: il faut penser les interfaces pour limiter les breaking changes lors des évolutions des services. Des breaking changes dans les interfaces d’un service nécessitent la modification des services qui y font appel. Ces breaking changes peuvent compliquer les déploiements.
    • Ne pas exposer des détails d’implémentation internes d’un service: exposer des détails de l’implémentation interne d’un service peut donner des indices sur son fonctionnement. D’autres services peuvent involontairement tirer partie de ce fonctionnement et avoir une implémentation dépendant de ce fonctionnement. Des implémentations trop dépendantes rendent le couplage plus important entre les services.

Séparation de la logique fonctionnelle en contextes bornés

Les contextes bornés (i.e. bounded context) correspond un notion qui provient du “Domain-Driven Design” de Eric Evans. Le gros intérêt de cette approche est qu’elle propose une solution pour séparer une application en microservices. Un contexte borné peut correspondre à plusieurs microservices ayant en commun un contexte fonctionnel.

Dans l’approche DDD:

  • La complexité fonctionnelle est séparée en contextes bornés: chaque contexte borné répond à un besoin fonctionnel qui possède un langage spécifique, c’est l’ubiquitous language”. Ce langage permet d’avoir une logique spécifique au contexte borné qui ne déborde pas de ce contexte.
  • Les frontières du contexte borné sont franchises seulement avec des interfaces: seules les interfaces sont exposées en dehors du contexte borné de façon à volontairement limiter les échanges entre contexte borné à ces interfaces. Cette limitation permet de contrôler et de maitriser les interfaces et donc les échanges.

Source: https://martinfowler.com/bliki/BoundedContext.html

Bounded context

Les contextes bornés donnent une solution efficace pour définir les frontières des différents microservices.

L’approche “Bounded Context” est intéressante pour les microservices car:

  • Les échanges sont plus nombreux entre services dans un même même domaine fonctionnel.
  • Elle évite d’exposer trop d’interfaces au-delà du domaine.

Ainsi cette approche minimise le couplage entre les contextes bornés et maximise la cohésion à l’intérieur d’un contexte borné.

Découpage en contextes bornés

Le découpage des contextes bornés et plus spécifiquement en microservices n’est pas anodin car il apporte certaines contraintes qui peuvent être plus difficilement surmontables que dans une application en monolithe.

En effet, une fois que le découpage en contextes bornés et en microservices est effectué, il est difficilement réversible. La séparation entre les contextes devient franche, et il sera plus difficile de partager du code entre ces contextes si on s’aperçoit qu’un fonctionnement est proche. De même, si on se rend compte d’une erreur dans le découpage des microservices, le code pourrait être plus difficilement déplaçable d’un service à l’autre.

Casser un monolithe

La plupart du temps, on ne part pas d’un projet “from scratch” et on peut être contraint d’adapter un projet existant. On l’a évoqué plus haut, un intérêt de l’approche en microservices est de permettre à une application d’évoluer en limitant sa complexité. Plus une application en monolithe augmente en taille et plus elle devient complexe. Avec le temps, cette complexité tend à rendre de plus en plus difficile la réalisation de nouvelles fonctionnalités.

Couche anticorruption

La première étape pour aller vers une application en microservices est de tenter d’arrêter de faire grossir l’application en monolithe. On peut, ainsi, tenter de développer des nouvelles fonctionnalités dans un service séparé et protégé par une couche anticorruption (i.e. anticorruption layer).

Cette couche anticorruption a pour but d’éviter de corrompre la partie dans laquelle est implémentée la logique fonctionnelle. La couche métier est la plus importante car c’est elle qui possède la valeur ajoutée de l’application, c’est la partie la plus susceptible d’être conservée si la technologie change. La couche anticorruption vise à servir “d’adaptateurs technologiques” à la couche métier.

Dans l’exemple suivant, l’objectif a été de séparer la couche de présentation “Presentation Layer” de la couche métier. La couche métier accède à la base de données par l’intermédiaire d’une couche “Data Access Layer” qui appartient à la couche anticorruption.
La couche de présentation fait appel à la couche métier pour tous ces traitements, en passant par l’intermédiaire d’appels REST. L’API REST permettant ces appels fait partie aussi de la couche anticorruption.


Extraire la logique fonctionnelle

Une fois qu’on a isolé la ou les couches de présentation de la couche métier. On peut tenter, dans un premier temps, de séparer la couche métier en modules. Cette séparation a pour but de préparer une séparation plus franche en contextes bornés. Le but est donc, de considérer ces modules comme s’ils étaient des contextes bornés.

Dans l’exemple suivant, on peut voir qu’un premier travail a consisté à séparer la couche métier en modules plus ou moins autonomes (par exemple, ils ne font pas tous appels à la base de données). Ces modules peuvent, ensuite, plus facilement être séparés de la couche métier pour former un contexte borné plus autonome. Les appels à la couche métier “historique” se fait par l’intermédiaire d’une API REST qui fait partie de la couche anticorruption.


Application de DRY

Lorsqu’on conçoit des microservices, on duplique souvent des traitements d’un service à l’autre. On peut être tenté d’appliquer le principe de programmation DRY pour éviter ces duplications.

De façon à éviter trop de duplications d’un service à l’autre, on peut mutualiser du code dans des bibliothèques techniques et mettre ces bibliothèques à disposition des développeurs des différents services. Ce type de procédé peut avoir quelques conséquences:

  • Elle amène les clients des services à s’adapter aux services puisqu’ils passent par l’intermédiaire d’une bibliothèque fournie par le service donné.
  • La bibliothèque fournie peut aussi contenir du code fonctionnel.
  • Une bibliothèque fournie peut involontairement augmenter le couplage entre des clients et un service car ils doivent utiliser cette bibliothèque pour s’interfacer avec le service.

Ainsi d’une façon générale, utiliser une bibliothèque fournie par les développeurs d’un service est une mauvaise pratique. Il est préférable d’utiliser des bibliothèques techniques générales, si possible, publiques:

  • Elles ne doivent pas imposer une technologie spécifique,
  • Elles doivent permettre aux clients d’être libre sur leur choix de technologie,
  • Elles ne doivent pas contenir d’implémentation fonctionnelle.

Ainsi, l’application de DRY doit se limiter à l’intérieur d’un service et il faut généralement éviter de l’appliquer entre services.

Utiliser une base de données à partir de microservices

Lorsqu’on conçoit plusieurs microservices qui font appels à une base de données, on peut se demander si on doit utiliser une seule base de données qui sera partagée entre tous les microservices ou plutôt avoir une base de données par service.

Avoir une base de données commune est plus rapide à implémenter. D’autre part, les services partageant un même domaine fonctionnel peuvent partager les mêmes tables. Cependant, en cas de modifications de la base pour convenir aux besoins d’un service, on peut impacter tous les services faisant appel à cette base. Avoir une base commune entre services, peut aussi nécessiter des mécanismes de synchronisation des services quand un objet a été mis à jour et qu’il faut le rafraîchir dans les autres services.

D’une façon générale, avoir une base de données commune augmente le couplage entre les services et affecte la cohésion.

Passer d’une base de données commune à une base séparée

Il n’y a pas de recettes miracles car chaque cas de figure est plus ou moins spécifique toutefois on peut tenter de séparer la base de données suivant les services qui l’utilisent:

  • Identifier le mapping entre les objets et les tables: de façon à pouvoir isoler chaque table et à les déplacer dans des bases séparées,
  • Identifier à quel contexte borné pourrait appartenir une table,
  • Casser les clés étrangères entre les tables: casser des clés étrangères rendra le contenu de la base de données moins cohérent. Ainsi, il faut que les futures services soient plus robustes aux incohérences qui pourraient survenir.
  • Différencier les données en lecture seule et en lecture/écriture.

De façon générale, tous les services ne font pas appels à toutes les tables:


Pour passer à une base séparée, on peut aussi s’aider d’identifiants uniques. Ces identifiants ne sont pas forcément des clés primaires dans les tables. L’intérêt de ces identifiants est que chacun d’entre eux désignent une entité précise. Une entité peut être créée dans un service, toutefois elle peut être identifiée de façon unique grâce à son identifiant et surtout le service peut échanger l’identifiant avec les autres services. Même si la représentation complète d’une entité reste dans un service précis, les autres services peuvent désigner cette entité au moyen de son identifiant.


Données statiques partagées

On peut se poser la question de savoir comment traiter le problème des données statiques ou des données référentielles. Ce sont des données qui changent rarement et qui sont consultées, la très grande majorité du temps, en lecture seule. Plusieurs solutions sont possibles:

  • Une table en lecture seule: tous les services accèdent à la même table et les accès à cette table sont en lecture seule. Cette solution est facile à implémenter toutefois, les services deviennent dépendants d’une même table. Une modification de la table impacte tous les services.
  • Dupliquer les données statiques pour tous les services: sachant que les données statiques changent rarement, on peut les dupliquer sur plusieurs tables. Chaque table étant requêtée par un seul service. Le gros problème de cette solution est la synchronisation entre les tables qui est nécessaire à chaque mise à jour des données. Il faut prévoir un mécanisme de synchronisation si cette solution est adoptée.
  • Stocker les données dans le code: cette solution convient dans le cas où les données statiques ne sont pas trop volumineuses et peuvent être stockées dans une assembly. L’intérêt de cette méthode est qu’on peut partager cette assembly et la consommer avec NuGet par exemple. En cas de mise à jour, il suffit de mettre à jour le package NuGet avec la nouvelle assembly.

Utiliser un service “proxy”

Que ce soit pour des données référentielles ou non, une solution peut consister à passer par un service spécialisé pour accéder à des données particulières. Ce service spécialisé accède seule à la table contenant les données. Cet espèce de service “proxy” est l’intermédiaire que doit obligatoirement utiliser les autres services pour accéder à ces données:


Le gros intérêt de cette méthode est que l’accès aux données n’est pas dupliqué sur les autres services. L’accès aux données en lecture et en écriture ne se fait que d’un seul service. Cette organisation n’est pas anodine et peut poser des problèmes en cas d’erreurs. Il faut prendre en compte ces erreurs possibles lors de la conception des services client.

Ainsi en cas d’erreurs dans le cas d’écriture de données en passant par un service “proxy”, il n’y a pas de notion de transactions comme dans le cas d’une base de données relationnelle. Il faut prévoir un comportement si le service “proxy” échoue à écrire le donnée:

  • Tolérer les données incohérentes: l’échec dans l’écriture d’une donnée ou l’absence de cette donnée en lecture ne doivent pas déstabiliser le service client. Il faut prendre en compte ces incohérences.
  • Essayer plus tard l’insertion d’une donnée: dans le cas d’un échec à l’insertion d’une donnée, on peut envisager un mécanisme de répétition de l’insertion un peu plus tard.
  • Vérifier que l’insertion s’est bien passée: pour être sûr de l’insertion de la donnée et de sa persistance, on peut effectuer une requête en lecture auprès du service “proxy” pour vérifier que la donnée a bien été insérée.
  • Prévoir les échecs répétés: dans le cas où le service “proxy” échoue à écrire une donnée et que les requêtes en lecture indique les échecs répétées des insertions, il faut prévoir un mécanisme d’abandon et permettre d’annuler complètement l’opération.
  • Transaction distribuée: une autre solution peut consister à utiliser des mécanismes de transactions distribuées. Ces mécanismes sont complexes à implémenter notamment dans le cadre de microservices, il est préférable de les éviter.

Accéder à des ressources provenant d’un autre service

D’une façon générale, les accès à des ressources situées dans un autre service doivent respecter certaines précautions car ces appels se font à travers le réseau. Le fait de passer à travers le réseau n’est pas anodin car il peut dégrader les performances dans le cas où il n’est pas rapide, voir il peut mener à des erreurs s’il est en échec.

En outre, des appels à d’autres microservices pour récupérer des données n’est pas aussi simple que de créer un objet lors de l’appel à une fonction dans une même application. Ainsi, il faut:

  • Interroger le service contenant la ressource quand on a besoin: ne pas trop anticiper la récupération d’une donnée car entre son accès et son utilisation, la donnée peut avoir changé.
  • Eviter de garder un objet provenant d’un autre service trop longtemps en mémoire: pour la même raison que précédemment, si on garde une donnée trop longtemps, on peut en détenir une version obsolète.
  • Eviter de récupérer l’intégralité d’une ressource: dans la majorité des cas, une version partielle de la ressource peut suffire. L’intérêt de la version partielle est qu’il est moins couteux de la récupérer par rapport à une ressource complète.

ACID vs BASE

ACID et BASE sont des acronymes utilisés pour indiquer des propriétés s’appliquant à des transactions effectuées sur des base de données. ACID est généralement appliqué aux bases de données relationnelles:

  • Atomicité (atomicity): chaque opération sur la base de données doit être atomique même si elle est formée de plusieurs petites opérations. Du point de vue de l’application qui effectue la requête, l’opération doit être annulée complètement si une petite opération a échoué.
  • Cohérence (consistency): cette propriété indique que toutes les transactions possibles provoquant un changement à la base de données doivent la laisser dans un état valide.
  • Isolation: les requêtes vers la base doivent être isolées c’est-à-dire qu’une application qui effectue une requête ne doit pas se rendre compte que d’autres applications effectuent des requêtes au même moment.
  • Durabilité (durability): les changements effectués sur la base de données dans le cas où ils sont confirmés doivent être permanent. Par exemple, une insertion d’une donnée doit être permanente si elle a été exécutée et confirmée.

Dans le cadre du théorème CAP énoncé par Eric Brewer, il n’est pas possible d’appliquer rigoureusement ACID dans le cas d’un système distribué:

“Un système distribué ne peut garantir que 2 des contraintes suivantes à la fois:

  • Cohérence (consistency),
  • Disponibilité (availability),
  • Tolérance au partionnement (partition tolerance).”

La propriété de disponibilité indique que toutes les requêtes doivent recevoir une réponse sans garantie que cette réponse contient l’écriture la plus récente.
La tolérance au partitionnement doit permettre à un système distribué de continuer à fonctionner même si quelques messages entre les nœuds du système ont été perdus à travers le réseau.

A cause du théorème CAP, les transactions dans les systèmes distribués tentent de respecter les propriétés BASE:

  • Basically Available: cette contrainte indique qu’un système doit garantir la disponibilité au sens du théorème CAP. Il doit toujours avoir une réponse à une requête, toutefois la réponse pourrait être un échec ou la réponse pourrait être incohérente.
  • Soft State: l’état du système peut changer au cours du temps même dans le cas où des données ne sont pas insérées. L’état du système peut être amené à changer pour garantir “éventuellement la cohérence”.
  • Eventually consistency: le système peut éventuellement être cohérent quand il n’y a pas de données insérées. Quand des données sont insérées, le temps de les propager, le système ne vérifie pas la cohérence de toutes les transactions.

Les propriétés BASE sont moins contraignantes que les propriétés ACID. Le fait de pouvoir relâcher quelques contraintes permet d’être plus adapté dans le cadre de système distribué et, par suite, dans le cas de l’architecture en microservices. Il faut donc concevoir des microservices en essayant de suivre une approche BASE plutôt qu’ACID.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Architecture en Microservices en 10 min

L’architecture microservice est un style d’architecture pour développer une application avec plusieurs petits services fonctionnant en utilisant des processus différents et utilisant des “moyens légers” pour communiquer. “Moyens légers” signifie, des moyens n’ayant pas une empreinte technique trop contraignante sur le code et sur les interactions entre les services.

L’approche consistant à séparer une application en plusieurs services n’est pas vraiment nouvelle, elle a déjà été tentée avec l’architecture orientée service (SOA pour “Service Oriented Architecture”):

  • Pourquoi l’approche SOA n’a pas vraiment fonctionné ?
  • Quelle est la différence entre l’approche SOA et l’approche Microservice ?
  • Pourquoi retenter une séparation en plusieurs services alors que cette approche n’a pas fonctionné avec SOA ?

Le but de cet article est d’expliquer l’architecture Microservice, de montrer les différences avec l’approche SOA et d’avoir un regard critique sur cette architecture.

Sommaire

Partie 1: Concevoir des microservices
Quelques définitions
  Architecture orientée service (SOA)
  Qu’est-ce que l’approche en microservices ?
  Les microservices ne sont pas la solution idéale

Concevoir des microservices
  Principe général
  Séparation de la logique fonctionnelle en contextes bornés
  Casser un monolithe
  Application de DRY
  Utiliser une base de données à partir de microservices

Partie 2: Appels entre Microservices
Choix des interfaces
  Utiliser le “semantic versioning”
  Créer un nouveau point d’accès en cas de “breaking changes”

Appels synchrones ou asynchrones
  Tolérer les échecs partiels
  Pattern “circuit breaker”

Chef d’orchestre ou chorégraphie
Service discovery
  Connaître la configuration des services en avance
  Pattern “client-side discovery”
  Pattern “server-side discovery”

Quelques technologies utilisées pour les appels
  Communications asynchrones par messagerie
  Communications synchrones

REpresentational State Transfer (REST)
  REST over HTTP
  API RESTful

Partie 3: Intégration continue et implémentation des tests
Intégration continue
  Effectuer une “build” pour tous les services
  Effectuer une “build” par service
  Plusieurs “build pipelines”

Appliquer des tests
  Pyramide des tests
  Tests unitaires
  Tester un service

Stratégies de déploiement
  Plusieurs services par hôte
  Un service par hôte
  Un service par “container”
  Déploiement sans serveur

Monitoring

Références

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Signature des assemblies par nom fort en 5 min

La signature des assemblies par nom fort est un procédé permettant d’assurer l’unicité d’une assembly. Il ne faut pas confondre la signature par nom fort (i.e. Strong Name signing) avec la signature électronique (i.e. digital signature).

Si on souhaite sécuriser le contenu d’une assembly et assurer qu’elle n’a pas été altérée de façon malveillante il faut plutôt se diriger vers des solutions de signature électronique comme Authenticode.

Explications

Intérêts de la signature par nom fort

Le but principal de la signature par nom fort est d’identifier une assembly de façon unique:

  • Nom unique: une assembly signée par nom fort possède un nom unique qui permet de la rendre unique. Elle peut, ainsi, être partagée entre plusieurs applications. Le but de ce nom unique est d’éviter des collisions de namespaces si accidentellement une autre assembly est nommée de la même façon. Si on a une dépendance vers une assembly avec un nom fort, cette dépendance ne pourra pas être confondue avec une autre assembly du même nom.
  • Ajouter l’assembly dans le GAC: pour ajouter une assembly dans le GAC (Global Assembly Cache), elle doit obligatoirement être signée. L’intérêt principal d’ajouter une assembly dans le GAC est de la partager entre plusieurs applications.

La signature par nom fort est insuffisante pour:

  • Garantir l’identité de l’éditeur de l’assembly: la signature par nom fort ne peut pas à elle-seule garantir l’identité de la personne qui fournit une assembly. Ce n’est pas parce qu’une assembly est signée de cette façon qu’on peut être sûr qu’elle provient de la bonne personne.
  • Empêcher la corruption de l’assembly: de la même façon la signature par nom fort ne permet pas de garantir qu’une assembly n’a pas été corrompue. Si au chargement de l’assembly, la vérification de la signature par nom fort échoue, on aura un indice que l’assembly n’est pas valide. Toutefois si la vérification réussie, on n’est pas sûr que ‘assembly n’a pas été modifiée.

Algorithme de cryptographie asymétrique

La signature par nom fort repose sur le principe de la cryptographie asymétrique. Ce principe se base sur 2 concepts: le hashage et la signature électronique.

Hashage

A partir du fichier correspondant à une assembly, l’algorithme de hashage est capable d’obtenir une donnée plus petite et de taille fixe. Le résultat du hashage appelé le hash ne permet pas de revenir à l’assembly d’origine, le hashage est irréversible.

Si 2 hash sont obtenus à partir de 2 assemblies et si ces hash sont identiques alors les 2 assemblies sont identiques. A l’opposé si les hash sont différents alors les assemblies ne sont pas semblables.

Dans le cas de .NET, l’algortihme de hashage utilisé est SHA1.

Signature électronique

La signature nécessite 2 clés: une clé publique et une clé privée. Ces 2 clés sont dépendantes:

  • Si une clé est utilisée pour encrypter des données, l’autre clé doit être utilisée pour décrypter les données. Il n’est pas possible d’utiliser une autre clé pour décrypter les données.
  • Si on a la clé publique, il n’est pas possible de trouver la clé privée correspondante.
  • Si un tiers possède la clé privée, il est capable de générer des données encryptées et éventuellement usurper l’identité de la personne à qui appartient les clés. La clé privée doit donc être gardée secrète et si elle est dévoilée, on ne peut plus utiliser la clé publique correspondante. La clé publique, quant à elle, peut être dévoilée mais ne suffit pas à elle seule à encrypter puis décrypter les données.
  • Il est difficile de générer une paire de clés identiques.

Procédé de signature des assemblies

Le but de ce procédé est de garantir que:

  • Le contenu d’une assembly n’a été modifiée,
  • L’identité de la personne qui fournit l’assembly.

Procédé de signature par nom fort

La personne qui fournit l’assembly effectue les étapes suivantes:

  1. Il génère une paire de clés: la clé privée et la clé publique.
  2. Quand l’assembly est générée, il exécute l’algorithme de hashage sur le fichier et obtient ainsi un hash.
  3. Le hash est encrypté en utilisant la clé privée.
  4. Il ajoute à l’assembly une signature électronique contenant le hash encrypté et la clé publique.
  5. Il envoie l’assembly. Cet envoie peut être fait par des moyens non sécurisés.

La personne qui reçoit l’assembly effectue les étapes suivantes:

  1. Il exécute l’algorithme de hashage sur l’assembly reçu et obtient un hash.
  2. Il décrypte le hash contenu dans la signature de l’assembly en utilisant la clé publique.
  3. Il compare le hash qu’il a calculé et le hash décrypté:
    • Si les 2 hash sont identiques alors l’assembly est valide. Toutefois ça ne veut pas dire que l’assembly n’a pas été modifiée et que la personne qui fournit l’assembly est bien celle qu’on croit.
    • Si les 2 hash sont différents, alors l’assembly n’est pas celle d’origine (elle n’est pas la même ou elle a été modifiée).

A ce stade, on ne peut pas conclure qu’une assembly valide du point de vue de ce procédé n’a pas été modifiée et que l’identité de la personne qui fournit l’assembly est bien identifiée pour plusieurs raisons:

  • Rien ne différencie l’assembly fournie par une personne identifiée d’une assembly fournie par un tiers: si la personne identifiée et tiers signent leur assembly de la même façon (même avec des clés différentes), elles seront toujours considérées comme valides par le receveur.
  • La signature peut avoir été modifiée: il est possible de récupérer l’assembly avant qu’elle ne parvienne au receveur, d’enlever la signature électronique (c’est-à-dire le hash ecnryptée et la clé publique) et d’effectuer une nouvelle signature. On envoie l’assembly avec la nouvelle signature et elle sera considérée comme valide alors qu’on peut très bien, avoir modifié cette assembly.

Juste avec ce type de procédé, on peut seulement garantir qu’une assembly livrée est unique car la paire de clés est unique. Ainsi une assembly signée avec une paire de clé sera différente de la même assembly signée avec une autre paire de clés.

Pour garantir que le contenu de l’assembly n’a pas été altéré et que l’identité de l’éditeur de l’assembly est bien identifié, il faut que la clé publique soit délivrée par une personne ou une autorité de confiance. Cette autorité de confiance certifie que:

  • La clé publique provient bien d’un éditeur précis.
  • Il fournit de façon sécurisée une clé publique non altérée.

Ces 2 éléments permettent de dire:

  • Si on arrive à décrypter le hash dans la signature électronique de l’assembly avec la clé publique alors la clé publique appartient bien à l’éditeur indiqué par l’autorité de confiance.
  • Si le hash décrypté et le hash obtenu après exécution de l’algorithme de hashage sur l’assembly reçue, sont identiques alors l’assembly provient bien de l’éditeur identifié et l’assembly n’a pas été altérée.

Le procédé de signature complête d’une assembly repose sur la confiance qu’on peut avoir envers l’autorité de confiance puisque c’est elle qui garantit l’intégrité de la clé publique.

La signature d’une assembly par nom fort s’arrête à garantir l’unicité d’une assembly car aucune autorité de confiance n’intervient.

Intérêts de la signature par nom fort par rapport à une signature complète

On peut se demander pourquoi utiliser une signature par nom fort si ce procédé ne permet pas de garantir davantage que l’unicité. La signature par nom fort a toutefois quelques avantages:

  • Elle est simple et peu couteuse à mettre en place puisqu’il n’est pas nécessaire de faire appel à une autorité de certification. On peut facilement signer une assembly et la publier sans coût supplémentaire.
  • Si on a pas la possibilité d’être connectée à une autorité de confiance et si on est sûr de la clé publique parce qu’elle a été identifiée au préalable, ce procédé peut suffire à être sûr de l’éditeur de l’assembly.
  • Si la clé publique ne change pas entre 2 versions d’une assembly, si on a confiance dans la première version de l’assembly et si on utilise la même clé publique pour une 2e version alors on peut être sûr que les 2 assemblies proviennent de la même personne. Ce cas de figure suffit dans la plupart des cas et permet, au moins, d’être sûr que l’éditeur d’une assembly n’est pas une autre personne.

Malgré tous ces points, la signature par nom fort est très critiqué notamment avec l’avènement de plateforme open-source comme GitHub. Beaucoup de gens s’interrogent sur l’utilité de ce type de signature alors qu’elle ne permet pas, toute seule, de garantir l’éditeur d’une assembly.
Certaines personnes considèrent qu’à partir du moment où on a le code source d’un projet, on doit pouvoir générer un nouveau projet sans être entravé par une signature par nom fort qui n’est pas très sure:
Still Strong-Naming your Assemblies? You do know it’s 2016, right?

Beaucoup de projets sont disponibles en open-source, par exemple, sur GitHub. Le source de ces projets sont donc facilement visible. Il y a des débats pour savoir s’il faut ou non inclure la clé privée dans les sources du projet:
Sur Stacloverflow: Why is it recommended to include the private key used for assembly signing in open-source repositories?.

Il n’y a pas de meilleures pratiques car on trouve plusieurs pratiques:

  • Inclure la clé privée dans les sources: cette approche signifie que n’importe qui peut signer l’assembly par nom fort avec la même clé que le responsable du projet. Avec cette approche, dans le cas où la clé publique ne change pas entre 2 versions, on n’a plus la garantie que l’assembly provient de la même personne.
    A l’opposé, en incluant la clé privée dans le projet, on peut partager facilement son projet, n’importe qui peut y contribuer, publier une nouvelle assembly et permettre que cette assembly soit éventuellement ajoutée dans le GAC.
  • Ne pas inclure de clé: cette approche est la plus simple, on laisse la possibilité de “forker” le projet et chaque personne est libre d’apposer une signature s’il le désire. De même que l’approche précédente, dans le cas où la clé publique ne change pas entre 2 versions, on perds la garantie de l’éditeur de l’assembly.
  • Utiliser la signature partielle: cette fonctionnalité permet de signer partiellement une assembly. Cette approche est explicitée plus bas.

Signature d’une assembly .NET par nom fort

Pour effectuer la signature, il faut générer une paire de clés.

L’outil utilisé est sn.exe qui est livré avec le developer pack du framework .NET: Developer Pack .NET Framwork 4.7.

Après installation, sn.exe se trouve dans un répertoire similaire à:

C:\Program Files\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe 

sn.exe est directement accessible à partir de la ligne de commande développeur: “Developer Command Prompt for VS 2017″.

Générer une paire de clés

Il faut exécuter à la ligne de commandes:

sn -k [nom du fichier .SNK]

Le fichier résultat contient la clé privée et la clé publique. Il ne doit pas être dévoilé sinon la signature n’a plus aucun intérêt.

Extraire le clé publique

sn -p [fichier source .SNK] [fichier .SNK résultat avec la clé publique] 

Signer une assembly à la compilation

Pour signer une assembly, on peut ajouter l’attribut suivant dans le fichier AssemblyInfo.cs (le fichier doit être dans le répertoire du fichier .csproj):

[assembly: AssemblyKeyFile(@"privateKey.snk")] 

Ajouter une option au compilateur

Une autre méthode consiste à ajouter l’option suivante au compilateur:

/keyfile:privateKey.snk 

On peut le faire à partir des propriétés du projet dans Visual Studio:

  1. Clique droit sur le projet, puis cliquer sur “Properties”.
  2. Aller dans l’onglet “Signature”
  3. Cliquer sur “Sign the assembly”
  4. Sélectionner le fichier ayant la clé privée

A la compilation, l’assembly sera signée.


Onglet “Signature” dans Visual Studio des propriétés d’un projet

Vérifier la signature d’une assembly

Avec sn.exe
Avec sn.exe, on peut taper la commande suivante pour le jeton public:

sn –T [chemin de l´assembly]

Pour afficher le jeton public et la clé publique:

sn –Tp [chemin de l´assembly] 

Pour vérifier simplement la signature:

sn –vf [chemin de l´assembly] 

Avec ildasm.exe
On peut aussi voir la clé publique avec ILDASM. ILDASM est un outil Microsoft qui permet de désassembler une assembly. Cette outil est livré avec le developer pack. Après installation, il se trouve dans un répertoire similaire à:

C:\Program Files\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\ildasm.exe

Pour l’utiliser il faut:

  1. Lancer ildasm.exe
  2. Cliquer sur “File” puis ouvrir le fichier à vérifier
  3. Cliquer sur MANIFEST
  4. La ligne .publickey permet de voir la clé publique:

    Extrait du MANIFEST d’une assembly signée

Conséquences de la signature

Lorsqu’une assembly est signée, toutes ces dépendances doivent être signée. Dans le cas contraire, à l’exécution lorsque le CLR va essayer de charger la dépendance et qu’elle ne possède pas de signature par nom fort, il va lancer une exception:

Unhandled exception : 
System.IO.FileLoadException: Could not load file or assembly ´ClassLibrary1,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null´ or one of its dependencies.
Strong name signature could not be verified. The assembly may have been
tampered with, or it was delay signed but not fully signed with the correct
private key. (Exception from HRESULT: 0x80131045)

Pour corriger ce problème, il faut aussi signer par nom fort les dépendances de l’exécutable.

A l’opposé, le CLR ne lance pas d’exception si une dépendance est signée et que l’assembly qui l’utilise ne l’est pas.

Enregistrement dans le GAC

Le GAC (i.e. Global Assembly Cache) est un registre dans Windows qui permet de stocker des assemblies qui peuvent être partagées. Si une assembly est enregistrée de façon globale dans le GAC, il n’est plus nécessaire qu’elle soit à coté de l’exécutable au runtime pour être utilisée. A l’exécution, le CLR va vérifier si l’assembly est présente dans le GAC et l’utilise si nécessaire.

Pour enregistrer une assembly dans le GAC elle doit obligatoirement être signée par nom fort.

La manipulation du GAC se fait avec l’utilitaire gacutil livré avec le developer pack. Après installation, il se trouve dans un répertoire similaire à:

C:\Program Files\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\gacutil.exe

Il est directement disponible à partir de la ligne de commande développeur: “Developer Command Prompt for VS 2017″.

Il faut exécuter toutes ces commandes avec les droits administrateur.

Enregistrer une assembly dans le GAC:

gacutil /i [chemin de l´assembly] 

Lister les assemblies enregistrées:

gacutil /l 

Désinstaller une assembly du GAC:

gacutil /u [nom de l´assembly] 

Signature partielle par nom fort (delay signing)

Lorsqu’on expose un projet dans une organisation, par exemple dans une entreprise ou sur GitHub, on peut ne pas vouloir montrer la clé privée de façon à garantir qu’on est bien le même éditeur de l’assembly livrée dans le cas où la clé publique reste inchangée.

Si on n’inclue pas la clé privée, quelqu’un qui compile le projet ne pourra pas signer l’assembly. Si cette personne consomme notre assembly comme dépendance et que cette personne a une assembly signée, elle ne pourra pas exécuter le code de notre assembly car le CLR va lancer une exception au chargement.

Une solution à ce problème est de laisser la personne signer elle-même notre assembly. De cette façon elle est libre de signer l’assembly si nécessaire. Une autre approche est de signer partiellement notre assembly pour éviter au consommateur de l’assembly de devoir effectuer ce travail.

La signature partielle doit être utilisée seulement en développement pour faciliter l’utilisation d’une assembly par d’autres développeurs sans compromettre la clé privée. Une assembly partiellement signée ne doit pas être déployée car sur un poste normal elle ne pourra pas être exécutée.

Utiliser seulement la clé publique

Pour signer partiellement une assembly, la clé privée n’est pas nécessaire. La clé privée ne doit pas être exposée donc elle ne doit pas être inclue dans le projet. La signature partielle nécessite seulement la clé publique. C’est la clé publique qui sera inclue, seule, dans le projet.

Pour obtenir la clé publique à partir d’une paire de clé:

sn -p [fichier source .SNK] [fichier .SNK résultat avec la clé publique] 

Signer partiellement une assembly

On peut ajouter les lignes suivantes dans le fichier AssemblyInfo.cs du projet:

[assembly: AssemblyKeyFile(@"publicKey.snk")] 
[assembly: AssemblyDelaySign(true)] 

On utilise seulement la clé publique à ce stade. La clé privée n’est pas nécessaire et ne doit pas être inclue dans le projet.

Une autre possibilité pour signer partiellement à partir de Visual Studio est d’aller dans les propriétés du projet:

  1. Clique droit sur le projet, puis cliquer sur “Properties”.
  2. Aller dans l’onglet “Signature”
  3. Cliquer sur “Sign the assembly”
  4. Sélectionner le fichier ayant la clé publique
  5. Cocher “Delay sign only”:

    Onglet “Signature” dans Visual Studio d’une assemlby signée partiellement

Comportement vis-à-vis des dépendances

Quand une assembly est partiellement signée, toutes ces dépendances doivent être, au moins, partiellement signées c’est-à-dire qu’il est aussi possible d’utiliser des dépendances complètement signées.
En revanche on ne peut pas utiliser des dépendances non signées.

Pour vérifier la signature partielle d’une assembly, on peut utiliser sn.exe:

sn –vf [chemin de l´assembly] 

Exécuter une assembly partiellement signée

La signature partielle par nom fort ne doit être utilisée que lors du développement. Une assembly partiellement signée ne doit pas être déployée c’est la raison pour laquelle l’exécution d’une assembly partiellement signée est, par défaut, interdite.

Si à ce stade, on essaie d’exécuter une assembly partiellement signée, on aura une exception indiquant que le chargement de l’assembly a échoué car la signature par nom fort n’a pas pu être vérifiée:

Unhandled exception : 
System.IO.FileLoadException: Could not load file or assembly ´ConsoleApp1,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=80c65ee63fb3ed24´ or one 
of its dependencies. Strong name signature could not be verified. The assembly may have 
been tampered with, or it was delay signed but not fully signed with the correct private key.
(Exception from HRESULT: 0x8013141A)

Pour résoudre ce problème, il faut autoriser l’exécution des assemblies partiellement signées. Cette autorisation est valable pour une assembly spécifique identifiée par une clé publique.

Les droits administrateurs sont nécessaires pour l’exécution des commandes suivantes.

Pour enregistrer l’assembly pour que la vérification de nom fort soit ignorée:

sn –Vr [chemin de l´assembly] 

L’assembly est identifiée avec sa signature publique donc il n’est pas nécessaire qu’elle soit dans le même répertoire que celui utilisé lors de l’enregistrement.

Dépendances de l’assembly

Pour exécuter une assembly partiellement signée, si elle possède des dépendances partiellement signées, il faut aussi enregistrer les dépendances pour que la vérification de nom fort soit ignorée aussi pour les dépendances.

Pour lister les assemblies pour lesquelles la vérification de nom fort est ignorée:

sn –Vl  

Pour supprimer l’enregistrement de l’assembly pour que la vérification de nom fort soit de nouveau effectuée:

sn –Vu [chemin de l´assembly] 
Différences entre les assemblies 32 et 64 bits

Si on enregistre une assembly compilée pour l’architecture x86, il faut utiliser la version 32 bits de sn.exe. De même pour une assembly compilée en x64, il faut utiliser la version 64 bits de sn.exe.

Sur une machine 64 bits, sn.exe se trouve dans:

  • En 32 bits: C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe
  • En 64 bits: C:\Program Files\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\sn.exe

Si on compile une assembly avec le paramètre “AnyCPU” dans Visual Studio, il faut penser à regarder le paramètre “Prefer 32-bit” dans l’onglet “Build” de Visual Studio pour savoir si l’exécutable sera lancé en 32 ou 64 bits:


Paramètre “Prefer 32-bit” dans Visual Studio

Signer complètement une assembly partiellement signée

Pour déployer une assembly partiellement signée, on peut la signer complètement sans avoir à la recompiler. Cette signature se fait avec la clé privée.

Pour signer une assembly partiellement signée, on peut exécuter la commande:

sn –R [chemin de l´assembly] [chemin de la clé privée] 

Pour vérifier si l’assembly a été correctement resignée, on peut exécuter:

sn –vf [chemin de l´assembly] 
Références:
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Comprendre .NET Standard en 5 min

Les technologies Microsoft adressent un grand nombre de plateformes différentes allant de systèmes d’exploitation comme Windows à des appareils mobiles comme les tablettes. D’autres parts, depuis quelques années, Satya Nadella a impulsé une “ouverture” des technologies Microsoft vers 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.

Cet article a pour but d’expliquer l’intérêt du .NET Standard par rapport aux autres approches de Microsoft pour rendre du code Microsoft plus générique et déployable sur plusieurs plateformes.

Explications

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.

Portable class library (PCL)

Une première tentative pour résoudre ce problème a été d’introduire les Portable Class Library (PCL). Si on sélectionne ce type de projet dans Visual Studio, on peut créer une bibliothèque que l’on pourra déployer sur plusieurs plateformes. En sélectionnant différentes plateform targets (comme .NET Framework, Windows Phone, Xamarin…) dans Visual Studio, on est capable de compiler un même code pour plusieurs plateformes.

Ainsi après compilation, on obtiendra un répertoire différent pour chaque plateforme sélectionnée.

L’approche des PCL rend plus facile le déploiement de bibliothèques sur des plateformes différentes, toutefois elle présente quelques inconvénients:

  • Plus le nombre de plateformes augmente et plus il faudra de compilation spécifiques pour chacune de ces plateformes. Il y aura plus de répertoires de sortie concernant ces plateformes ce qui augmente considérablement la complexité de ce type d’approche car il y a presqu’une dizaine de plateformes possibles (.NET Framework, Silverlight, Windows 8.0, Windows Store Apps, Windows Phone 8.1, Xamarin.iOS, Xamarin.Android).
  • Toutes les API ne sont pas disponibles pour toutes les plateformes. Suivant la plateforme sur laquelle on souhaite déployer, certaines fonctionnalités .NET peuvent ne pas être disponibles.
  • Si on ajoute une nouvelle plateforme, il faudra recompiler la bibliothèque pour qu’elle soit disponible pour cette plateforme.

.NET Standard

Pour éviter de généraliser la démarche des PCL pour un grand nombre de plateformes différentes et pour encapsuler la complexité d’un déploiement sur plusieurs plateformes, la 2e approche de Microsoft a été d’introduire une abstraction supplémentaire avec le .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èques 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 le plus efficace possible, il faut donc placer un maximum de code dans des bibliothèques qui se basent .NET Standard. Le reste du code, étant plus spécifique, se basera sur une plateforme précise.

Pour résumer, on a donc 2 approches possibles:

  • Une bibliothèque de classes,
  • Un exécutable.

Cas d’une bibliothèque de classes

On peut donc implémenter une bibliothèque de classes .NET Standard en se basant sur:

  • Le .NET Standard qui se base sur des assemblies d’un framework (par exemple .NET Framework ou .NET Core),
  • Eventuellement une autre bibliothèque de classes .NET Standard,
  • Eventuellement un bibliothèque PCL.

Cas d’un exécutable

Un exécutable est plus spécifique à une plateforme, il se base donc directement sur les assemblies d’un framework (par exemple .NET Framework ou .NET Core). Il peut aussi éventuellement avoir des dépendances vers une bibliothèque de classes .NET Standard (qui elle-même se base sur le .NET Standard).

Dépendances à partir de .NET Standard 2.0

A partir de .NET Standard 2.0, une bibliothèque de classes basée sur .NET Standard pourra référencer des assemblies spécifiques au Framework .NET en plus des références vers d’autres bibliothèques .NET Standard. Ces référencements seront possibles à l’aide de compatibility shims. Plus de détails sont disponibles sur .NET Standard Assembly Unification.

Plateforme cible

Comme indiquer plus haut, .NET Standard permet d’indiquer un ensemble d’API utilisables par une bibliothèque de classes. Toutefois, il n’indique pas directement la plateforme cible sur laquelle la bibliothèque sera déployée. Toutes les versions de .NET Standard ne sont pas compatibles avec toutes les plateformes et toutes les versions de plateformes.

Versions de .NET Standard

Ainsi, une plateforme est compatible avec une version précise de .NET Standard. Si on veut déployer une même bibliothèque sur plusieurs plateformes, il faut choisir la version de .NET Standard qui le permet.

On peut résumer la compatibilité entre les frameworks et les versions de .NET Standard dans le tableau suivant:

(1): ces compatiblités sont valables pour le CLI 1.x mais ne seront plus valable quand CLI 2.x sortira.

Quelques remarques sur la version de .NET Standard:

  • Plus la version de .NET Standard augmente et plus des API et des fonctionnalités sont rajoutées au standard. Si une plateforme respecte .NET Standard 1.6 alors elle respecte toutes les versions précédant la 1.6 (de 1.0 à 1.6).
  • Plus la version de .NET est basse et plus de frameworks sont supportés, toutefois plus la version de .NET Standard est basse et moins il y a de fonctionnalités.
Confusions liées à .NET Standard 2.0 (pas de breaking changes)

Il y a eu une grande confusion avec .NET Standard 2.0 car Microsoft a changé son approche entre 2 pre-releases.

1ère approche (valable pour le CLI 1.x):
Dans un premier temps, Microsoft avait envisagé un breaking change entre .NET Standard 1.6 et 2.0. De la version 1.0 à la version 1.6, chaque incrément de version correspond à l’ajout de nouveau élément dans le standard. Pour passer de la version 1.6 à 2.0 de .NET Standard, Microsoft voulait enlever des éléments d’API. La version 2.0 comportait donc moins d’éléments que la version 1.6.

Cette suppression d’éléments entraînait un “breaking change” entre la version 1.6 et 2.0. Ainsi, une bibliothèque de classes compatible avec la version 2.0 n’était pas compatible avec la version 1.6 et 1.5.

De même, le Framework .NET 4.6.1 était compatible de .NET Standard 1.0 à 1.4 puis était compatible avec .NET Standard 2.0. En revanche il n’était pas compatible avec .NET Standard 1.5 et 1.6.
Cette approche est seulement valable pour la version du CLI (Command Line Interface) 1.x.

2e approche (valable pour le CLI 2.x):
Dorénavant, il n’y a pas de breaking changes entre .NET Standard 1.6 et 2.0. La version 2.0 de .NET Standard comporte plus d’éléments d’API que la version 1.6. Ainsi, une bibliothèque de classes compatible avec .NET Standard 2.0 est compatible de 1.0 à 2.0.

De même le Framework .NET 4.6.1 est compatible du .NET Standard 1.0 à 2.0.
A la sortie de la version 2.x du CLI (Command Line Interface), cette approche sera définitive.

Pour avoir la liste plus complête de toutes les plateformes, on peut se reporter à la documentation de .NET Standard sur Github.

Fonctionnalités de .NET Standard

Comme indiqué plus haut, plus la version de .NET Standard augmente et plus le standard comporte de fonctionnalités. Le choix de la version de .NET Standard sur laquelle on va baser une bibliothèque de classes doit se faire suivant les fonctionnalités disponibles.

Les fonctionnalités présentent dans .NET Standard sont les suivantes:

  • .NET Standard 1.0:
    • types primitifs,
    • Reflection dans System.Reflection,
    • Task Parallel Library (TPL) dans System.Threading.Tasks,
    • Collections dans System.Collections,
    • Linq dans System.Linq.
  • .NET Standard 1.1:
    • Les éléments du .NET Standard 1.0,
    • Les structures de collections concurrentes dans System.Collections.Concurrent,
    • Les services d’interopérabilité COM et platform invoke dans System.Runtime.InteropServices,
    • HttpClient dans System.Net.Client.
  • .NET Standard 1.2:
    • Les éléments du .NET Standard 1.1,
    • La classe Timer dans System.Threading,
    • Ajout du paramétrage LargeObjectHeapCompactMode pour le garbage collector dans System.Runtime.
  • .NET Standard 1.3:
    • Les éléments du .NET Standard 1.2,
    • Les classes pour s’interfacer avec le système de fichiers comme File ou Directory dans System.IO,
    • S’interfacer avec une Console avec la classe Console dans System,
    • Gestion des sockets dans System.Net,
    • Ajout d’éléments pour s’interfacer avec l’environnement avec la classe Envionment dans System,
    • Ajout de la classe AsyncLocal dans System.Threading,
    • La classe StringBuilder dans System.Text.
  • .NET Standard 1.4:
    • Les éléments du .NET Standard 1.3,
    • Ajout de l’algorithme ECDSA dans la classe ECDsa dans System.Security.Cryptography.
  • .NET Standard 1.5:
    • Les éléments du .NET Standard 1.4,
    • Des éléments liés à la Reflection dans la classe Activator dans System et ajout de la classe Assembly, Module dans System.Reflection.
    • Ajout de la classe BufferedStream dans System.IO,
  • .NET Standard 1.6:
    • Les éléments du .NET Standard 1.5,
    • Possibilité de compiler les Expression Trees dans la classe Expression dans System.Linq.Expressions.
    • Ajout de la classe ECCurve dans System.Security.Cryptography,
    • Ajout de plus de fonctionnalités dans la classe Regex dans System.Text.RegularExpressions.

Pour .NET Standard 2.0, on peut avoir une vue d’ensemble des fonctionnalités et des API qui sont prises en compte:

On peut avoir une liste exhaustive des fonctionnalités par version de .NET Standard en allant sur: https://github.com/dotnet/standard/tree/master/docs/versions.

.NET API Browser

La disponibilité de classes dépends de la version et de la plateforme cible. Etant donné la multitude de plateformes cible, on peut avoir des difficultés à savoir si une classe existe pour la version de Framework utilisée et pour la plateforme sur laquelle on va déployer.

Pour rendre plus facile cette vérification, Microsoft a mis en place le .NET API Browser.

On peut avoir une documentation précise des classes utilisables en fonction de la plateforme sur le .NET API Browser.

Support des fonctionnalités suivant la plateforme

On peut penser que si une classe est présente pour une version donnée de .NET Standard alors toutes les plateformes supporteront l’implémentation de cette classe. C’est vrai la plupart du temps mais pas dans tous les cas.

Certaines classes sont spécifiques à une plateforme. Ainsi pour assurer une homogénéité des classes définies dans .NET Standard, Microsoft peut les avoir inclus alors qu’elles ne s’appliquent pas pour certaines plateformes. Plusieurs comportements sont possibles, suivants les cas, Microsoft implémente 2 approches:

  • Lancer une exception de type PlatformNotSupportedException à l’exécution quand une classe est incompatible avec la plateforme sur laquelle elle est exécutée.
  • Emuler le comportement de la classe même si la plateforme n’est pas adaptée: par exemple il est possible d’émuler l’accès à la base de registres sur d’autres plateformes que Windows.
Il faut tester les fonctionnalités suivant toutes les plateformes

Certaines classes de .NET Standard peuvent être spécifiques pour certaines plateformes. Ainsi si on base une bibliothèque sur une version de .NET Standard comprenant ces classes, la compilation peut réussir alors qu’à l’exécution, l’utilisation de ces classes peut mener à une exception de type PlatformNotSupportedException. On est donc pas sûr, après compilation, d’avoir un code exécutable sur toutes les plateformes.

Il faut donc appliquer des tests qui s’exécuteront pour les plateformes sur lesquelles ont veut déployer de façon à être sûr de ne pas déclencher des exceptions à l’exécution de certaines fonctionnalités.

Comprendre le fonctionnement de .NET Standard avec NuGet

Comme indiquer plus haut, .NET Standard permet d’encapsuler la complexité de la gestion de plusieurs plateformes en ajoutant une abstraction. Des bibliothèques de classes peuvent se baser sur un .NET Standard et éviter ainsi de se baser sur une plateforme spécifique.

On a l’impression que les mêmes fichiers seront utilisés pour toutes les plateformes. A vrai dire c’est impossible puisque les plateformes sont trop hétérogènes:

  • Pour une bibliothèque de classes .NET Standard lors du développement: les projets de ce type contiennent des références vers une ou plusieurs assemblies contenues dans des packages NuGet de .NET Standard. Ces packages ne contiennent pas vraiment des assemblies avec une implémentation. Ce sont, soit des métapackages (qui référencent d’autres packages NuGet), soit des packages contenant des assemblies avec des fonctions sans implémentation. Le but de ces assemblies est de simplement permettre la compilation. Elles ne seront pas utilisées à l’exécution.
  • Pour un exécutable consommant une bibliothèque de classes .NET Standard: l’exécutable doit s’exécuter sur une plateforme spécifique. Ainsi, lors de l’exécution de tests ou en débug ce sont des assemblies correspondant à la plateforme de développement qui sont utilisées. De même, à l’exécution de l’exécutable sur une plateforme spécifique, ce sont les assemblies contenant du code adaptés à la plateforme qui seront utilisées.

Il y a donc une logique qui associe certaines assemblies suivant la plateforme sur laquelle sera déployée une bibliothèque de classes .NET Standard. Cette logique est gérée par le compilateur et la plateforme de développement quel que soit le framework utilisé au déploiement (Framework .NET, .NET Core etc…).

Le fonctionnement de .NET Standard est différent entre les versions 1.x et 2.x:

  • Pour les versions 1.x: .NET Standard est consommé sous forme d’un “meta-package” NuGet permettant de télécharger d’autres packages plus spécifiques à une plateforme.
  • Pour les versions 2.x: le contenu du package est différent est différent, ils contiennent une assembly monolitique netstandard.dll.

Pour les versions 1.x

Quand on base une bibliothèque de classes sur .NET Standard 1.x, on consomme un package NuGet appelé NETStandard.Library.

Ce package est un “meta-package” qui ne contient aucune assembly. Il permet seulement de télécharger d’autres packages.

Si on regarde le contenu du package .NETStandard.Library 1.6.1:

  1. Télécharger .NETStandard.Library 1.6.1 sur NuGet.org:
  2. Renommer le fichier de .nupkg à .zip
  3. Décompresser le package pour en voir le contenu

En regardant le contenu du répertoire du package, on se rends compte qu’il n’y aucune assembly. Si on lit le fichier NETStandard.Library.nuspec: on trouve des informations relatives au .NET Standard 1.0, .NET Standard 1.1, .NET Standard 1.2 et .NET Standard 1.3:

<?xml version="1.0" encoding="utf-8"?> 
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> 
  <metadata minClientVersion="2.12"> 
    <id>NETStandard.Library</id> 
    <version>1.6.1</version> 
    <title>NETStandard.Library</title> 
    <!-- ... --> 
    <dependencies> 
      <group targetFramework=".NETStandard1.0"> 
        <!-- ... --> 
      </group> 
      <group targetFramework=".NETStandard1.1"> 
        <!-- ... --> 
      </group> 
      <group targetFramework=".NETStandard1.2"> 
        <!-- ... --> 
      </group> 
      <group targetFramework=".NETStandard1.3"> 
        <!-- ... --> 
      </group> 
    </dependencies> 
  </metadata> 
</package>

Il n’y a pas d’informations relatives aux .NET Standard suivants. Ceci doit s’expliquer par le fait que Microsoft a changé d’approche pour le .NET Standard 2.0 et est revenu sur les packages livrés pour .NET Standard 1.5 et .NET Standard 1.6.

Si on regarde le nœud pour le .NET Standard 1.3:

<group targetFramework=".NETStandard1.3"> 
  <dependency id="Microsoft.NETCore.Platforms" version="1.1.0" /> 
  <dependency id="Microsoft.Win32.Primitives" version="4.3.0" /> 
  <dependency id="System.AppContext" version="4.3.0" /> 
  <dependency id="System.Collections" version="4.3.0" /> 
  <dependency id="System.Collections.Concurrent" version="4.3.0" /> 
  <!-- ... --> 
  <dependency id="System.Security.Cryptography.Algorithms" version="4.3.0" /> 
  <dependency id="System.Security.Cryptography.Encoding" version="4.3.0" /> 
  <dependency id="System.Security.Cryptography.Primitives" version="4.3.0" /> 
  <dependency id="System.Security.Cryptography.X509Certificates" version="4.3.0" /> 
</group>
2 types de dépendances existent pour les frameworks:
  • Des dépendances non spécifiques vers des bibliothèques d’un framework: par exemple pour .NET Core, une partie de ces bibliothèques est fournie sous forme d’assemblies à l’installation de .NET Core. Les assemblies fournies de cette façon sont communes à plusieurs plateformes. Par exemple, les assemblies System.Collections et System.Linq ne sont pas spécifiques à une plateforme.
  • Des dépendances vers des bibliothèques d’un framework spécifiques à une plateforme: par exemple, les assemblies System.IO et System.Security.Cryptography.Algotithms sont spécifiques à une plateforme.

    Dans le cas du Framework .NET, toutes les bibliothèques sont dépendantes de la plateforme puisque le Framework .NET n’est déployable que sur un système Windows. D’autre part, toutes les bibliothèques du Framework .NET sont fournies sous la forme d’assemblies qui sont livrées avec le Framework .NET à son installation.

Pour plus de détails sur .NET Core, voir la page suivante: https://docs.microsoft.com/en-us/dotnet/articles/core/.

Dans le fichier NETStandard.Library.nuspec, on considère les dépendances aux packages suivants:

  • Microsoft.NETCore.Platforms,
  • System.Collections et
  • System.Security.Cryptography.Algorithms

Le package NETStandard.Library ne contient pas d’assemblies.

Microsoft.NETCore.Platforms

Si on télécharge le package Microsoft.NETCore.Platforms en version 1.1.0:

En regardant le contenu, on s’aperçoit qu’il n’y a pas non plus d’assemblies. Par contre 3 fichiers sont importants:

  • Microsoft.NETCore.Platforms.nuspec: il contient la description du package.
  • Dans lib\netstandard1.0\_._: ce fichier est vide.
  • Runtime.json: ce fichier contient des Runtime IDentifiers (RID) qui permettent d’identifier les systèmes d’exploitation cibles sur lesquels une application sera exécutée. Par exemple, pour le système Windows 10 en 64 bits, on peut lire la ligne suivante:
            "win10-x64": { 
                "#import": [ "win10", "win81-x64" ]
    

    Ce fichier indique que NuGet peut restaurer les packages ayant besoin de win10 et win81-x64. Par suite, pour win10, NuGet peut restaurer des packages ayant besoin de win81 et ainsi de suite.

    L’arbre de dépendances est:

    • win10-x64 => win10 => win81 => win8 => win7 => win => any => base et
    • Win10-x64 => win81-x64 => win8-x64 => win7-x64 => win-x64 => any => base.

Pour plus de détails sur le RID: Catalogue d’identificateurs de runtime (RID) .NET Core.

Il permet d’indiquer quelles sont les RID à utiliser pour une plateforme cible donnée.

System.Collections

Si on regarde le package System.Collections en version 4.3.0, il contient:

  • System.Collections.nuspec: ce fichier indique d’autres packages suivant la plateforme cible choisie:
    <?xml version="1.0" encoding="utf-8"?> 
    <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> 
      <metadata minClientVersion="2.12"> 
        <id>System.Collections</id> 
        <version>4.3.0</version> 
        <title>System.Collections</title> 
        <! -- ... --> 
        <dependencies> 
          <! -- ... --> 
          <group targetFramework=".NETStandard1.3"> 
            <dependency id="Microsoft.NETCore.Platforms" version="1.1.0" /> 
            <dependency id="Microsoft.NETCore.Targets" version="1.1.0" /> 
            <dependency id="System.Runtime" version="4.3.0" /> 
          </group> 
          <! -- ... --> 
      </metadata> 
    </package>
    
  • Dans le répertoire lib: on trouve d’autres répertoires suivant les plateformes cibles. Mais ces répertoires contiennent des fichiers vides nommés _._.
  • Dans le répertoire ref: se trouve quelques assemblies suivant les plateformes cibles mais l’implémentation de ces assemblies est vide.
    • Si on prends le répertoire ref\netstandard1.0: on trouve l’assembly System.Collections.dll, toutefois en regardant avec DotPeek, il n’y a pas d’implémentation dans le corps des fonctions.
    • Si on prends le répertoire ref\net45: on trouve encore des fichiers vides nommés _._.

Il n’y pas réellement d’assemblies avec une implémentation dans ce package. Ceci s’explique par le fait que System.Collections n’est pas spécifique à une plateforme. L’implémentation correspondant à ce namespace est fournie par les frameworks. Il n’est donc pas nécessaire de fournir une implémentation particulière.

System.Security.Cryptography.Algorithms

Si on regarde le contenu de System.Security.Cryptography.Algorithms en version 4.3.0:

  • System.Security.Cryptography.Algorithms.nuspec contient d’autres indications sur des dépendances de packages NuGet.
  • Les répertoires lib et ref contiennent d’autres répertoires pour les plateformes cibles. Ces répertoires contiennent une assembly quand le framework ne fournit pas lui-même une implémentation.
  • Le répertoire runtime contient des répertoires désignant un contenu spécifique à un runtime indiqué par le package Microsoft.NETCore.Platforms. Par chaque runtime, il y a aussi une assembly spécifique.

Contrairement à System.Collection, le package System.Security.Cryptography.Algorithms contient des assemblies car il fournit une implémentation spécifique pour chaque plateforme.

Si on prends la référence de package Microsoft.NETCore.Targets dans le fichier System.Collections.nuspec en continuant l’exploration.

En résumé

Pour les versions 1.x, le .NET Standard est composé d’une multitude de packages NuGet contenant:

  • D’autres dépendances vers des packages NuGet,
  • Des fichiers vides lorsque l’implémentation n’est pas nécessaire parce-qu’elle est fournie par un framework (Framework .NET, .NET Core etc…).
  • Des assemblies spécifiques à une plateforme lorsque c’est nécessaire.

Ainsi, si une bibliothèque de classes se base sur .NET Standard, elle n’a pas de dépendances directes vers des packages et par suite des assemblies. Ces dépendances sont indiquées par le package .NET Standard suivant la ou les plateformes cibles.

Lien entre .NET Standard et .NET Core

Un des inconvénients de cette approche est le lien entre les packages NuGet de .NET Core et .NET Standard. En fait pour une bibliothèque de classes basée sur .NET Standard, les packages non spécifiques à une plateforme utilisés proviennent de .NET Core. Le package NuGet .NETStandard.Library permet d’implémenter ce lien entre la bibliothèque de classes et les packages .NET Core.

C’est en partie pour casser ce lien qu’une approche différente a été utilisée pour les versions 2.x de .NET Standard.

Pour les versions 2.x

Il n’y a pas encore de version stable de .NET Standard 2.0 et Microsoft a plusieurs fois changé son approche pour les packages correspondant à cette version.

Microsoft a voulu casser la complexité de la gestion de packages pour les versions 1.x en fournissant un package contenant tout le .NET Standard sans avoir une multitude de packages NuGet comme pour les versions 1.x.

L’approche actuelle utilise plusieurs redirections de type (i.e. type forwarding).

Si on regarde le contenu du package NETStandard.Library 2.0:

  • Une assembly nommée netstandard.dll qui contient tous les types du .NET Standard. Ce fichier se trouve dans build\netstandard2.0\ref. L’assembly netstandard.dll ne contient aucune implémentation mais seulement avec des redirections de types vers des assemblies des frameworks.
  • Des assemblies qui se trouvent dans build\netstandard2.0\ref qui correspondent à des types utilisés par le .NET Standard sans implémentation contenant des redirections de type vers l’assembly netstandard.dll.

Les assemblies qui contiennent ces redirections de types sont des assemblies système comme mscorlib.dll, System.Runtime.x et d’autres assemblies non spécifiques à une plateforme.

Par exemple, en regardant le contenu de mscorlib.dll (qui se trouve dans le package) avec DotPeek, on voit qu’elle ne contient que des redirections vers netstandard.dll:

Redirection de type (Type forwarding)

Ce mécanisme fonctionne en utilisant l’attribut TypeForwardedTo (System.Runtime.CompilerServices.TypeForwardedToAttribute) qui permet de mapper un type dans une assembly source vers un type dans une autre assembly.

Ce mécanisme permet de créer des assemblies avec seulement des types contenant des type forward vers d’autres assemblies, rendant ainsi transparent l’utilisation d’un type en fonction d’une plateforme. Ce sont les outils de développement et le compilateur qui prennent en compte les attributs TypeForwardedTo pour savoir quelle assembly utiliser.

Pour comprendre l’utilisation de la redirection de type avec l’assembly netstandard.dll, il y a plusieurs cas de figure:

  • Une bibliothèque de classes utilisant d’autres bibliothèques .NET Standard
  • Un exécutable qui consomme des bibliothèques de classes .NET Standard.

Une bibliothèque de classes .NET Standard

Dans ce cas, la bibliothèque de classes .NET Standard a une dépendance vers:

  • L’assembly netstandard.dll qui se trouve dans le package NuGet NETStandard.Library
  • Eventuellement des assemblies qui ont elles-mêmes des dépendances vers les assemblies d’un framework (Framework .NET, .NET Core, etc…).

Comme indiqué plus haut, le package NETStandard.Library contient des assemblies avec les mêmes types que le framework mais sans implémentation. Ces assemblies font de la redirection de type vers l’assembly netstandard.dll.

Un exécutable consommant une bibliothèque .NET Standard

Dans le cas d’un exécutable qui consomme une bibliothèque .NET Standard, les dépendances sont:

  • Les assemblies du framework puisque comme indiqué plus haut, un exécutable est spécifique à une plateforme et ne peut pas uniquement se baser sur le .NET Standard.
  • La bibliothèque de classes .NET Standard qui elle-même a une dépendance vers l’assembly netstandard.dll (le cas de cette bibliothèque correspond au cas précédent).
  • Eventuellement des assemblies ayant des dépendances vers les assemblies du framework.

Par suite l’assembly netstandard.dll effectue des redirections de types vers les assemblies spécifiques d’un framework.

Compatibilité entre la version 2.x et les versions 1.x

Dans le cas où on se base sur le .NET Standard 2.0 et qu’on utilise des packages.NET Standard 1.x, on peut se retrouver avec plusieurs assemblies avec le même nom: des assemblies provenant du .NET Standard 1.x et les assemblies contenant les type forward. Pour résoudre les éventuels problèmes de conflits, une adaptation a été nécessaire dans MSBuild pour résoudre les conflits.

Il est possible que Microsoft change encore son approche pour la version définitive de .NET Standard 2.0. Pour davantage d’informations sur ce sujet: Packaging for .NET Standard 2.0.

En résumé

Dans la même façon que pour les versions 2.x du .NET Standard, la redirection de type permet d’avoir des bibliothèques de classes qui se basent seulement sur le .NET Standard et n’ont aucune dépendance directe vers des assemblies spécifiques d’un framework.
D’autres part, par rapport aux versions 1.x, le gros avantage de la redirection de type est qu’elle permet d’éviter d’avoir beaucoup de packages NuGet quasiment vides pour la plupart des types non spécifiques à une plateforme. L’utilisation du .NET Standard devient plus directe puisqu’elle se base, en grande partie, sur une seule assembly netstandard.dll.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

NuGet en 5 min

NuGet est un outil facilitant le téléchargement de dépendances externes. Ces dépendances peuvent être téléchargées et ajoutées à un projet Visual Studio ou indépendamment de l’IDE, directement dans un répertoire.
NuGet peut aussi créer et uploader des packages sur un repository.

La documentation de NuGet est plutôt bien faite:

Le but de cet article n’est pas de paraphraser la documentation mais d’être un aide-mémoire sur les principales fonctionnalités de NuGet.

Installation de Nuget

Dans Visual Studio

Pour les versions précédant Visual Studio 2012, il est nécessaire d’installer NuGet après avoir installé Visual Studio: Install Nuget.

A partir de Visual Studio 2012, NuGet est livré avec l’IDE. On peut vérifier la version en cliquant sur le menu Help puis About Microsoft Visual Studio. Il faut ensuite regarder la version de NuGet Package Manager.

On peut toutefois mettre à jour la version livrée en faisant:

  1. Cliquer dans le menu sur Tools puis sur Extensions and Updates
  2. Dans la boite de dialogue, sélectionner l’onglet Tab
  3. Sélectionner Visual Studio Gallery
  4. Sélectionner NuGet Package Manager for Visual Studio
  5. Cliquer sur Update.

Voici les versions du NuGet livrées avec Visual Studio:

Visual Studio 2012 (11.0) NuGet 2.0
Visual Studio 2013 (12.0) NuGet 2.7
Visual Studio 2015 (14.0) NuGet 2.8.6
Visual Studio 2017 (15.0) NuGet 4.0.0
NuGet 4.1.0

La version inclue de NuGet dans Visual Studio n’est accessible qu’à partir du Package Manager ou du Package Manager Console.

Téléchargement direct de nuget.exe

Nuget.exe peut être directement téléchargé sur nuget.org/downloads.

NuGet à la ligne de commandes

Il est possible d’utiliser NuGet à la ligne de commandes sans Visual Studio. On peut le télécharger directement sur: NuGet CLI

Le fichier téléchargé possède une extension .nupkg. NuGet.exe se trouve dans ce package:

  1. Modifier l’extension .nupkg en .zip
  2. Décompresser l’archive zip.
  3. NuGet.exe se trouve dans [Nom du package]/tools/NuGet.exe

En ajoutant le répertoire où se trouve NuGet.exe dans la variable d’environnement PATH, on peut utiliser facilement NuGet.exe dans une invite de commandes.

Avec Chocolatey

Il est possible d’installer l’exécutable NuGet.exe en utilisant Chocolatey. L’intérêt de cette méthode est d’avoir une procédure facilement scriptable dans un script de déploiement (avec Puppet, Ansible ou Chef).

Après avoir installé Chocolatey (https://chocolatey.org/install), il suffit de taper la commande suivante pour installer Nuget.CommandLine:

choco install nuget.commandline

Configuration

Emplacement

La configuration NuGet se trouve dans un ficher XML NuGet.config. Il peut exister plusieurs versions de ce fichier suivant les différentes versions de NuGet et suivant la portée du paramétrage:

  • Dans le répertoire d’un projet, le paramétrage s’appliquera seulement dans le cadre du projet.
  • L’emplacement par défaut est: %APPDATA%\NuGet\NuGet.Config. La portée de ce fichier est globale.
  • A l’échelle de la machine, on peut utiliser un fichier dont la portée sera la machine ou une version particulière de Visual Studio. Ce fichier se trouve dans %ProgramData%\NuGet\Config.

    Suivant la version de Visual Studio le répertoire peut être:

    • %ProgramData%\NuGet\Config\[IDE]\[Version]\[SKU]\*.config
    • %ProgramData%\NuGet\Config\[IDE]\[Version]\*.config
    • %ProgramData%\NuGet\Config\[IDE]\*.config
    • %ProgramData%\NuGet\Config\*.config

    [IDE] correspond à VisualStudio; [Version] correspond à celle de Visual Studio (la version de Visual Studio 2015 est 14.0); [SKU] correspond à Community, Pro ou Enterprise.

Plus de détails sur l’emplacement du fichier de configuration dans Configuring NuGet behavior.

Contenu

La prise en compte de la configuration se fait en commençant par le fichier le plus global (i.e. fichier dans %ProgramData%) et en allant vers le fichier le plus spécifique (par exemple dans le répertoire d’un projet).

Les éléments de configuration sont surchargés par addition, ainsi si on indique des sources dans un fichier global et d’autres sources dans un fichier plus spécifique, les sources seront prises en compte par addition. Pour ne pas avoir ce comportement, il faut utiliser le nœud <clear />.

Par exemple un fichier NuGet.config peut contenir les nœuds suivants:

<?xml version="1.0" encoding="UTF-8"?> 
<configuration> 
    <config> 
        <add key="DefaultPushSource" value="https://example.com/packages/" /> 
    </config> 
    <packageSources> 
        <add key="Example Package Source" value="https://example.com/packages/" /> 
        <add key="nuget.org" value="https://www.nuget.org/api/v2/" /> 
    </packageSources> 
    <disabledPackageSources> 
        <add key="nuget.org" value="true" /> 
    </disabledPackageSources> 
</configuration>

Le détail de la configuration est:

  • Le nœud config permet d’indiquer les repositories vers lesquels on peut uploader des packages (cf. Nuget push).
  • Le nœud packageSources indique les sources à partir desquels les packages peuvent être téléchargés.
  • Le nœud disabledPackageSources contient les sources que l’on souhaite désactiver. Le nom de la source est indiquée avec le clé. Quand le booléen est à true la source est désactivée (c’est-à-dire que les packages ne pourront plus être téléchargés à partir de cette source).

Clear

Dans le nœud packageSources, il est possible d’indiquer <clear />:

<packageSources> 
    <clear />  
    <add key="otherSource" value="https://other/nuget/source" /> 
</packageSources>

<clear /> permet d’indiquer que les sources renseignées dans des fichiers plus haut dans la hiérarchie de prise en compte de la configuration ne seront pas ajoutées. Seulement la source otherSource sera prise en compte.

Configuration dans Visual Studio

Le plus souvent, il n’est pas nécessaire d’éditer les fichiers de configuration de NuGet à la main, il suffit d’aller dans Visual Studio dans:

  1. Cliquer sur le menu Tools puis Options
  2. Etendre le nœud Nuget Package Manager
  3. Sélectionner Package Sources
  4. On peut rajouter des sources en indiquant leur adresse. Il est aussi possible de désactiver des sources particulières.

Télécharger un package

Plusieurs méthodes sont possibles pour télécharger un package avec NuGet. La méthode la plus courante est de le faire avec Visual Studio en utilisant le Package Manager. Toujours dans Visual, si on souhaite ajouter des options particulières, on peut utiliser le Package Manager Console. On peut aussi utiliser NuGet en dehors de l’IDE à la ligne de commandes, avec des outils comme Package Explorer ou directement sur nuget.org.

Informations liées à un package

Pour un package donné, plusieurs informations sont disponibles et visibles à partir du Package Manager, avec Package Explorer ou sur le site nuget.org:

  • Le nom du package
  • La version du package MAJOR.MINOR.PATCH (cf. semantic versioning) suivi éventuellement d’une mention -alpha1 ou -beta1 pour les packages “prerelease”.
    La version d’un package NuGet et la version des assemblies ne correspondent pas forcément

    La version d’un package NuGet peut ne pas correspondre avec la version de ou des assemblies qui s’y trouvent. Il n’y a pas d’obligations que les versions correspondent. Ces différences peuvent mener à des confusions.

  • Les dépendances du package : ce sont les packages nécessaires au package courant. Généralement NuGet télécharge les dépendances quand il télécharge un package.

Dans Visual Studio

Package Manager

Dans Visual Studio, l’intérêt du Package Manager est d’avoir une interface graphique facile  à utiliser. Pour y accéder:

  1. Faire un clique droit sur le projet pour lequel on peut télécharger des packages (il est aussi possible de télécharger des packages à l’échelle de la solution en effectuant un clique droit sur la solution).
  2. Cliquer sur Manage NuGet Packages… (pour une solution, il faut cliquer sur Manager NuGet Packages for Solution…).
  3. L’interface permet de chercher facilement le package qu’on souhaite télécharger

L’interface permet de:

  • Renseigner le nom du package,
  • Indiquer si on souhaite effectuer un nouveau téléchargement (onglet Browse), lister les packages déjà téléchargés (onglet Installed) ou effectuer des mises à jour de packages déjà installés (onglet Updates): en sélectionnant directement le bon onglet, on peut effectuer les étapes plus rapidement.
  • Sélectioner la source à partir de laquelle on veut télécharger le package: si on ne préciser rien, toutes les sources seront prises en compte. Il est parfois plus efficace d’indiquer directement la source quand on la connaît pour que la recherche soit plus rapide.
  • Renseigner la version voulue.
  • Indiquer si on veut une version “prerelease”: dans le cas où cette case n’est pas cochée, les versions “prerelease” n’apparaîtront pas ce qui ce qui peut mener à quelques confusions.
La liste des packages affichés prend en compte la version du framework renseignée dans les paramètres du projet

Si le package n’est compatible qu’avec une version plus récente du framework que celle du projet, il ne sera pas affiché. Ce comportement peut mener à des confusions car on ne verra pas un package qui se trouve bien dans le repository de la source.
On peut s’aider d’outils comme le Package Explorer pour voir la liste des packages sans filtres.

Installation de package au niveau d’une solution

Seules les versions 2.x supportent cette fonctionnalité. Pour les versions ultérieures, il n’est pas possible d’installer un package au niveau d’une solution.

Package Manager Console

Le Package Manager Console permet aussi de télécharger des packages NuGet en utilisant des commandes Powershell. Pour ouvrir le Package Manager Console dans Visual Studio, il faut cliquer sur Tools, sur NuGet Package Manager puis sur Package Manager Console.

Pour que les commandes s’exécutent sur un projet spécifique et pour une source spécifique, il faut le préciser dans l’entête de la console:

Commandes principales de la Package Manager Console

Les commandes les plus courantes sont:
Pour chercher un package (find-package):
A partir de NuGet 3.0, on peut utiliser la commande find-package:

find-package [nom du package]

Pour avoir une version particulière d’un package:

find-package [nom du package] -version [version du package]

Parfois il y a trop de packages qui correspondent pour une recherche, pour restreindre la recherche exactement au nom du package, on peut utiliser l’option -ExactMatch:

find-package [nom du package] -ExactMatch

Pour installer un package (install-package):

Install-package [nom du package] {-Version [numéro de version]}

Pour une version prérelease:

Install-package [nom du package] {-Version [numéro de version]} -Pre

Pour lister des packages (get-package):
Lister les packages dans la solution:

get-package {[nom du package]}

Lister les packages disponibles dans les repositories:

get-package {[nom du package]} -ListAvailable

Lister les version d’un package disponible dans les repositories:

get-package [nom du package] -ListAvailable -AllVersions

Pour supprimer un package (uninstall-package):

Uninstall-package [nom du package]

Pour mettre à jour un package (update-package):
Met à jour le package et ses dépendances dans tous les projets:

update-package [nom du package]

Met à jour un package dans un projet particulier:

update-package -ProjectName [nom du projet]

Met à jour tous les packages dans tous les projets:

update-package

Réinstalle un package dans tous les projets:

update-package [nom du package] -reinstall

Examine toutes les assemblies d’un projet et ajoute des binding redirects si nécessaire:

Add-BindingRedirect

Etapes effectuées à l’installation d’un package

Lorsqu’un package est installé dans Visual Studio, il y un certain nombre d’opérations qui sont effectuées sur le projet:

  1. NuGet crée un répertoire packages dans le répertoire de la solution.
  2. Il télécharge le package ainsi que ses dépendances dans le cache de NuGet (cf. emplacement du cache NuGet).
  3. Il décompresse le package ainsi que les dépendances dans le répertoire packages dans le répertoire de la solution. Ce répertoire contiendra les éléments suivants:
    • Dans packages\[{nom du package espacé avec "."}.{version du package}]: se trouve le package .nupkg compressé et le fichier .nuspec contenant les métadonnées du package.
    • Dans packages\[{nom du package espacé avec "."}.{version du package}]\lib: se trouve toutes les assemblies suivant leur target framework. Par exemple pour le framework .NET 4.6.2, le target framework sera net462.
    • Dans packages\[{nom du package espacé avec "."}.{version du package}]\lib\[target framework]: se trouve les assemblies correspondant au target framework.
  4. Il ajoute le fichier packages.config s’il n’existe pas dans le ou les projets où les assemblies provenant du package sont rajoutées. Si packages.config existe, NuGet rajoute les lignes correspondant aux packages rajoutés.
    Ce fichier contient le nom des packages NuGet, leur version et le target framework correspondant. Par exemple, un fichier packages.config peut contenir les informations suivantes:

    <?xml version="1.0" encoding="utf-8"?>
    <packages>
      <package id="MongoDB.Bson" version="2.0.0" targetFramework="net45" />
      <package id="MongoDB.Driver" version="2.0.0" targetFramework="net45" />
      <package id="MongoDB.Driver.Core" version="2.0.0" targetFramework="net45" />
    </packages>
    

Précisions sur le target framework:

  • Pour le framework .NET, target framework sera du type net462, net46, net45, net40 etc…
  • Pour .NET Core: netcore, netcore451 etc…
  • Pour Universal Windows Platform: uap, uap10 etc…
  • Pour .NET Standard: netstandard1.0, netstandard1.6 etc…

On peut trouver une liste exhaustive de ces versions sur : Target Frameworks:

  1. NuGet rajoute les dépendances dans les références du projet (cf. ces références sont visibles dans le répertoire References sur projet dans l’explorateur de solution dans Visual.
  2. NuGet peut exécuter un script Powershell provenant du package s’il y en a un (cette fonctionnalité n’est plus disponible à partir de la version 3.x).
  3. NuGet peut modifier des fichiers de code source, des fichiers de configuration app.config ou web.config (cf. transformations appliquées par NuGet sur des fichiers) par exemple pour rajouter un élément de configuration lié au binding redirect (cf. BindingRedirect en 5min).

Activer la suggestion de packages dans Visual Studio 2017

Cette fonctionnalité est désactivée par défaut et elle n’est disponible que sur Visual Studio 2017. Elle permet de suggérer un package NuGet lorsqu’on écrit un namespace et les objets de ce namespace sont définis dans un package qui n’est pas installé:

Pour activer la fonctionnalité, il faut cliquer dans:

  1. Options du menu Tools
  2. Dans la partie Text Editor -> C# et Advanced
  3. Cocher Suggest using for types in NuGet packages et Suggest usings for types in reference assemblies.

La suggestion de package est accessible au moment de l’édition en plaçant le curseur sur le type inconnue et en tapant Alt + Maj + F10.

Avec Package Explorer

Package Explorer est outil qui permet de gérer des packages NuGet. Le gros intérêt de Package Explorer est qu’il est indépendant de Visual Studio et de la configuration de NuGet dans les fichiers NuGet.config. Il propose une interface simple pour:

  • Parcourir les packages disponibles dans le repository d’une source.
  • Avec accès aux détails d’un package (en particulier les target framework ou les dépendances).
  • Télécharger un package sans tirer toutes les dépendances.
  • Voir le contenu d’un package.
  • Construire un packages.

Package Explorer se trouve sur GitHub et sur CodePlex.

A partir de la ligne de commandes

Pour installer un package dans le répertoire courant:

nuget install [nom du package]

Cette commande ne modifie pas le fichier packages.config ou le fichier du projet.

Quelques options utiles:

  • -PreRelease : autoriser de télécharger une version “pre-release”.
  • -Version [numéro de version]: télécharge une version spécifique.
  • -NoCache: effectue le téléchargement sans utiliser le contenu du répertoire du cache NuGet.
  • -Source [URL de la source]: télécharge le package à partir d’une source précise.

Emplacement du cache

Quand NuGet télécharge des packages, il les place dans un répertoire de cache. Ce cache permet d’éviter d’effectuer des téléchargements du même package quand celui-ci a déjà été installé pour un projet.

Le gros inconvénient de ce cache est qu’il peut mener à des comportements inattendus dans certains cas:

  • Si un package se trouve dans le cache et si il a été supprimé de la source: le package s’installera normalement sur le poste où il est présent dans le cache. En revanche sur une machine où le package n’est pas présent dans le cache, NuGet ne pourra pas l’installer. Il y aura donc, 2 comportements différents sur les 2 machines pour un même package.
  • Si un package a été modifié dans une source mais que la version n’a pas changé: si ce package est déjà présent dans la cache, NuGet utilisera toujours la version qui se trouve dans le cache et non la nouvelle version se trouvant sur la source. Ce comportement entraîne l’utilisation de l’ancien package.

Pour éviter ces comportements, on peut utiliser les options -NoCache lorsqu’on utilise NuGet à la ligne de commande.

On peut vérifier le contenu du cache en exécutant (à partir de la version 3.3):

nuget locals all -list

On peut vider le cache en exécutant (à partir de la version 3.3):

nuget locals all -clear

Pour les versions antérieures à la version 3.3, on peut supprimer le cache directement à partir des répertoires:

  • %LocalAppData%\NuGet\Cache
  • %UserProfile%\.nuget\packages

Creation de package

La création de package se fait au moyen d’un fichier .nuspec qui est un fichier texte indiquant comment générer le package .nupkg et indique des informations sur le package.

Les fichiers ajoutés à un package:

  • peuvent se baser sur d’autres package NuGet, ce sont des dépendances. Ces dépendances seront ajoutées au projet de la même façon que le package lui-même.
  • peuvent être autonomes c’est-à-dire que l’exécution de ces assemblies ne nécessite pas d’autres assemblies ou fichiers.

S’il y a des dépendances sous forme de packages NuGet, il est préférable d’indiquer ces dépendances dans le fichier .nuspec plutôt que d’inclure les assemblies des dépendances directement dans le package que l’on souhaite créer.

Fichier .nuspec

Ce fichier sert à générer un package NuGet, il comprends les fichiers à inclure dans le package et la description du package.

Exemple de fichier .nuspec:

<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
  <metadata>
    <id>MyPackage</id>
    <version>2.5.1.23</version>
    <authors>Robert Mitchoum</authors>
    <owners>Robert Mitchoum</owners>
    <licenseUrl>http://opensource.org/licenses/MS-PL</licenseUrl>
    <projectUrl>http://github.com/MyProject</projectUrl>
    <iconUrl>http://github.com/MyProject/nuget_icon.png</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <releaseNotes>Bug fixes and performance improvements</releaseNotes>
    <description>Description of MyPackage</description>
    <copyright>Copyright  ©2016 Mitchoum Corp.</copyright>
    <tags>web utility</tags>
    <dependencies>
        <dependency id="Newtonsoft.Json" version="9.0" />
    </dependencies>
  </metadata>
  <files>
    <file src="readme.txt" target="" />
    <file src="c:\docs\bin\*.xml" target="lib" /> 
    <file src="bin\Debug\*.dll" target="lib" /> 
    <file src="bin\Debug\*.pdb" target="lib" /> 
    <file src="tools\**\*.*" />
    </files>
</package>

Ce fichier comprends les informations suivantes:

  • dans le nœud package\metadata: on indique les informations du package: nom, version, auteurs, description…
  • dans le nœud files\file: on indique où se trouve les fichiers à inclure dans le package. Pour chaque fichier, l’attribut src indique le chemin du fichier à inclure et l’attribut target indique où le fichier sera rangé dans le package NuGet. Le répertoire du fichier dans le package NuGet indiquera quelle sera la fonction du fichier.
  • dans le nœud files\dependencies: on indique quels sont les dépendances NuGet du package à créer. Les dépendances sont reconnues avec le nom du package et des conditions sur sa version.

    Par exemple, on peut indiquer des dépendances de cette façon:

    <dependencies>
      <dependency id="NomPackage1" version="3.0.0" />
      <dependency id="NomPackage2"/>
    </dependencies>
    

    Un syntaxe particulière peut être utilisée pour indiquer les conditions appliquées sur la version des dépendances (cf. Indication de la version des dépendances).

  • dans le nœud frameworkAssemblies\frameworkAssembly: on peut indiquer les assemblies du framework qui sont nécessaires à l’exécution des assemblies du package. Les assemblies du framework ne seront pas inclues dans le package. Ces précisions permettent d’indiquer, par exemple, à quelle version du framework s’applique le package.
    Par exemple:

    <frameworkAssemblies>
      <frameworkAssembly assemblyName="System.ComponentModel.DataAnnotations" targetFramework="net40" />
      <frameworkAssembly assemblyName="System.ServiceModel" targetFramework="net40" />
    </frameworkAssemblies>
    

Nom d’un package

Pour éviter les confusions, il est préférable d’utiliser un nom de package qui n’est pas déjà utilisé même si le package sera utilisé seulement dans un repository privé. Utiliser des noms de package spécifique permettra d’éviter le téléchargement par NuGet de mauvais packages.

Il est préférable d’utiliser des noms de package similaires à ceux des namespaces c’est-à-dire utiliser des “.” plutôt que des “-” ou des “_”.

Version du package

Il est préférable d’utiliser la même version entre celle du package et celle de l’assembly principale du package. Il n’y a pas d’obligation que ces versions soient similaires toutefois, utiliser un numéro de version couplé permet d’éviter des confusions quand le package sera téléchargé et installé.

Indication de la version des dépendances

Les contraintes sur les versions peuvent s’indiquer de cette façon:

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

Pour plus de précisions : Specifying dependency versions for NuGet Packages.

Organisation d’un fichier .nupkg

Précisions sur la fonction d’un fichier suivant son chemin dans un package NuGet:

  • dans le répertoire tools: contient des scripts powershell et des programmes exécutables dans la Package Manager Console.
  • dans le répertoire lib se trouvent les assemblies à inclure au projet, la documentation liée aux assemblies (fichier .xml) et les fichiers symboles (.pdb).
  • dans le répertoire content: ces fichiers seront copiés à la racine du projet Visual dans lequel sera installé le package.
  • dans le répertoire build se trouvent des fichiers de targets ou de propriétés de MSBuild. Ces fichiers sont automatiquement rajoutés au projet Visual.

Comment utiliser un même package pour plusieurs versions de framework ?

A partir du framework .NET 4.0, il est possible d’appliquer plusieurs targets pour un projet de façon à ce qu’il soit déployé sur plusieurs plateformes (par exemple framework .NET 4.0 ou 4.5 ou suivant le .NET Standard 1.6 etc…).

Pour adresser les différents frameworks avec un même package, il suffit de placer les différentes assemblies dans des répertoires suivant une structure avec la version du framework.

Par exemple, si on veut supporter le framework .NET 4.6.2 et .NET Standard 1.6, il faut placer les assemblies suivant la structure:

\lib
    \net462
        \Assembly.dll
    \netstandard1.6
        \Assembly.dll

Si une même assembly est utilisée pour 2 target platforms, il faut obligatoirement copier cette assembly dans les 2 répertoires correspondant à ces target platforms.

Le nom des répertoires des target platforms (par exemple net462 et netstandard1.6) doit respecter une convention permettant d’indiquer précisément une plateforme.

Les plateformes les plus courantes sont :

Framework Abréviation Target framework moniker
.NET framework net net11
net40
net45
net451
net452
.NET Core App netcoreapp netcoreapp1.0
netcoreapp1.1
.NET Core netcore netcore [netcore45]
netcore45 [win, win8]
netcore451 [win81]
netcore50
Universal Windows Platform uap uap [uap10.0]
uap10.0
.NET Standard netstandard netstandard1.0
netstandard1.3
netstandard1.6

On peut trouver une liste exhaustive des target platforms sur Target frameworks.

Il ne faut pas confondre .NET Core App et .NET Core

NET Core App correspond à la première version du .NET Core qui a été renommé en janvier 2016 pour passer de .NET Core 5 à .NET Core 1.0 (voir Introducing .NET Core 1.0).
Pour les versions actuelles de .NET Core, il faut utiliser les abréviations netcoreapp.

Générer un fichier .nuspec

On peut créer un fichier .nuspec directement à la main avec un éditeur de texte mais il est aussi possible de le générer.

Pour générer un fichier générique avec les nœuds XML:

nuget spec [nom du package]

Pour générer un .nuspec à partir d’une assembly:

nuget spec [chemin de l'assembly]

Dans ce cas les informations du fichier seront complétées en fonction des informations de l’assembly.

Pour générer un .nuspec à partir de Visual Studio, il faut se placer dans le répertoire du projet (i.e. du fichier .csproj) et exécuter:

nuget spec

Le fichier généré sera un template contenant des tokens qui seront remplacés par les valeurs à la création du package. Pour que les tokens soient remplacés par les valeurs, il faut que le fichier .nupkg soit généré avec nuget pack [chemin du fichier .csproj].

Ajouter un fichier README

Le fichier sera affiché dans Visual Studio à l’installation du package.
Il suffit d’indiquer le chemin du fichier dans le nœud package\files, le nom du fichier doit être readme.txt:

<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
    <!-- ... -->
    <files>
      <file src="readme.txt" target="" />
    </files>
</package>

Ajouter des propriétés et des targets MSBuild

Ajouter des targets ou des propriétés MSBuild dans un package permet de rajouter ces fichiers directement dans un répertoire \build d’un projet. Ces fichiers doivent s’appeler:

  • [nom du package].targets pour les targets,
  • [nom du package].props pour les propriétés.

Si ces fichiers se trouvent dans le répertoire build, ils seront ajoutés pour tous les projets pour lesquels on installe la package quelque soit la version du framework cible. Si on souhaite que les fichiers soient installés pour des frameworks cible spécifiques, il faut ranger les fichiers dans un répertoire utilisant le nom de la plateforme cible.

Par exemple, si les fichiers doivent être installés pour des projets ayant pour plateforme cible le framework .NET 4.5, il faudra placer les fichiers suivant la hiérarchie:

  • build\net45\[nom du package].props et
  • build\net45\[nom du package].targets.

Il faut inclure ensuite des indications sur ces fichiers dans le fichier .nuspec au moment de la création du package.

Suivants les versions de NuGet, le comportement sera différent à l’installation d’un package lorsque celui-ci comprends des targets ou des propriétés MSBuild:

  • pour les versions Nuget 2.x: NuGet rajoute un nœud <Import> dans le fichier .csproj pour inclure les targets et propriétés.
  • pour les versions Nuget 3.x: NuGet ne rajoute pas les “targets” dans le fichier .csproj.

Créer un package .nupkg

Pour créer un package avec le fichier .nuspec:

nuget pack [fichier .nuspec]

Pour créer un package sur un projet Visual Studio:

nuget pack [nom du projet .csproj]

Quelques options peuvent être utiles:

  • -IncludeReferencedProjects: si un projet dépend d’autres projets, cette option permettra d’inclure dans le package, les fichiers générés par les autres projets. Cette option s’utilise en exécutant:
    nuget pack [chemin du fichier .csproj] -IncludeReferencedProjects
    
  • -properties: permet d’utiliser les fichiers provenant d’une configuration de build particulière. Par défaut, nuget pack [chemin du .csproj] utilise la configuration de build par défaut, on peut préciser une configuration spécifique en écrivant:
    nuget pack [chemin du fichier .csproj] -properties Configuration=[nom de la configuration de build]
    

    La configuration de build peut être, par exemple, Release ou Debug.

  • -symbols: cette option permet d’inclure dans le package généré les fichiers .pdb. Elle s’utilise en exécutant:
    nuget pack [nom du projet .csproj] -symbols
    

Nuget push

Permet d’uploader un package sur une source:

nuget push [chemin du fichier .nupkg] -Source [URL de la source]

On peut omettre l’option -Source à partir de NuGet 3.4.2 si on précise une valeur DefaultPushSource dans NuGet.config.

L’authentification auprès de la source se fait en utilisant une Api Key. Cette Api Key est fournie par la source. Avec l’Api Key:

nuget push [chemin du fichier .nupkg] [ApiKey de la source] -Source [URL de la source]

Modifier des fichiers du projet à l’installation du package

A partir de NuGet 3.x, on ne peut plus inclure des scripts d’installation Powershell dans un package NuGet. Il existe, en revanche, des méthodes pour créer ou modifier des fichiers dans un projet Visual.

Les modifications à appliquer sur les fichiers seront effectuées à l’installation du package et quand on désinstallera le package, certaines de ces modifications peuvent être annulées.

Modifier un fichier de code source

Les modifications peuvent être appliquées au projet à l’installation mais elles ne seront pas annulées à la désinstallation.

Il faut effectuer les étapes suivantes:

  • Pour ajouter un fichier dans un répertoire: par exemple pour ajouter le fichier dans le répertoire Controller, il faut placer le fichier dans le répertoire content\Controller du package NuGet.
  • Le nom du fichier doit comporter le suffixe .pp pour indiquer que le fichier doit être modifié à l’installation.

    Par exemple si on place un fichier ProductController.cs.pp dans le répertoire content\Controller du package NuGet, le fichier Controller\ProductController.cs sera ajouté dans le projet Visual.

  • Le contenu du fichier avec l’extension .pp peut comporter des tokens indiqués sous la forme $[nom du "token"]$ qui seront remplacés par leur valeur à l’installation.
    Par exemple : si on place la ligne : namespace $RootNamespace$.Controllers dans le fichier avec l’extension .pp et le RootNamespace est ProductApi dans le projet, le résultat après installation sera:

    namespace ProductApi.Controllers
    

    On trouver une la liste exhaustive des tokens utilisables dans ProjectProperties.

Modifier un fichier de configuration

Les modifications apportées à ces fichiers peuvent être annulées à la désinstallation.
2 méthodes sont possibles:

  • Appliquer une transformation XML,
  • Utiliser un fichier XDT.

Appliquer une transformation XML
Pour modifier un fichier de configuration App.config ou Web.config, il faut:

  • Ajouter le suffixe .transform au nom du fichier à modifier.
    Par exemple : pour un fichier App.config, il faut renommer le fichier en App.config.transform.
  • Placer ce fichier dans le répertoire content du package NuGet.
  • Ajouter des nœuds XML <add ... /> pour ajouter les éléments dans le fichier final.
    Par exemple : si on écrit dans le fichier Web.config.transform:

    <configuration>
      <system.webServer>
        <nodeToAdd value="false" />
        <modules>
          <add name="module1" type="Example.module1, Example" />
          <add name="module2" type="Example.module2, Example" />
        </modules>
      </system.webServer>
      <customNode>
        <!-- Custom text -->
        <innerCustomNode allowRemoteAccess="false" />
      </customNode>
    </configuration>
    

Dans le fichier Web.config:

  • Le nœud configuration/system.webServer/nodeToAdd sera rajouté.
  • Les nœuds configuration/system.webServer/nodeToAdd/modules/<add name="module1"/> et <add name="module2" />
  • Les commentaires <!-- Custom text --> seront ajoutés en suivant la hiérarchie XML.
  • etc…

A la désintallation du package, ces modifications sont annulées seulement si les lignes ajoutées n’ont pas été modifiées après l’installation.

Appliquer une modification par fichier XDT
Cette fonctionnalité est disponible à partir de NuGet 2.6.
On peut appliquer des transformations à un fichier XML en utilisant la syntae XDT. Pour plus de détails, voir Web.config Transformation Syntax for Web Application Project Deployment Using Visual Studio.

Pour appliquer ces modifications, il faut:

  • Ajouter le suffixe .install.xdt au nom du fichier à modifier.

    Par exemple : pour un fichier App.config, il faut renommer le fichier en App.config.install.xdt.

  • Placer ce fichier dans le répertoire content du package NuGet.
  • On peut utiliser des tokens indiqués sous la forme $[nom du "token"]$ qui seront remplacés par leur valeur à l’installation (voir ProjectProperties pour la liste des tokens).

Par exemple, si on utilise le fichier Web.config.install.xdt:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
    <nodeToAdd value="false" xdt:Transform="Insert"/>
    <modules>
      <add name="module1" type="Example.module1, Example" xdt:Transform="Insert"/>
      <add name="module2" type="Example.module2, Example" xdt:Transform="Insert"/>
    </modules>
  </system.webServer>
  <customNode>
    <innerCustomNode allowRemoteAccess="false" xdt:Transform="Insert"/>
  </customNode>
</configuration>

Les mêmes modifications seront appliquées au fichier Web.config d’origine que l’exemple précédent.

Pour que les modifications soient annulées à la désintallation, il faut ajouter au package NuGet un fichier avec l’extension .uninstall.xdt.

Par exemple, pour annuler les modifications précédentes, on peut ajouter un fichier Web.config.uninstall.xdt avec le contenu:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
    <nodeToAdd value="false" xdt:Transform="Remove"/>
    <modules>
      <add name="module1" type="Example.module1, Example" xdt:Transform="Remove"/>
      <add name="module2" type="Example.module2, Example" xdt:Transform="Remove"/>
    </modules>
  </system.webServer>
  <customNode>
    <innerCustomNode allowRemoteAccess="false" xdt:Transform="Remove"/>
  </customNode>
</configuration>

Appliquer un script d’installation Powershell

Cette fonctionnalité n’est plus disponible à partir de NuGet 3.x. Elle permet d’exécuter un script Powershell à l’installation et la désinstallation d’un package NuGet.

Pour que ces scripts soient exécutés, il faut qu’ils soient nommés suivant la convention suivante:

  • Init.ps1: il est exécuté à la 1ère installation du package,
  • Install.ps1: ce script est exécuté à l’installation du package,
  • Uninstall.ps1: il est exécuté à la désinstallation du package.

Il faut placer ses scripts dans le répertoire tools du package NuGet.

Le script Powershell doit commencer avec la ligne:

param($installPath, $toolsPath, $package, $project)

avec:

  • $installPath: path où le projet se trouve.
  • $toolsPath: path où le contenu du répertoire tools est décompressé à l’installation.
  • $package: informations sur le package qui est installé.
  • $project: informations sur le projet dans lequel l’installation est effectuée. Ces informations sont de type EnvDTE.

Nuget restore

Nuget restore est une fonctionnalité qui permet d’ajouter les assemblies provenant de packages NuGet dans les projets d’une solution juste avant la compilation. Si ces packages n’ont pas été téléchargés. NuGet les télécharge, les décompresse et les ajoute aux projets en fonction du fichier packages.config de chaque projet.

Nuget restore peut aussi restaurer dans le projet des packages si certains sont manquants.

L’intérêt de cette fonctionnalité est l’ajout automatique des packages sans que le développeur ne s’en préoccupe.
3 méthodes existent pour activer la fonctionnalité restore:

  • au niveau de Visual Studio: à partir de Nuget 2.7.
  • au niveau de MSBuild: pour les versions antérieures à Nuget 2.6.
  • en appelant une commande à la ligne de commandes.

Activer la fonctionnalité

Avec Visual Studio

Pour activer Nuget restore dans Visual studio: Tools -> Options -> Nuget package Manager -> General

Il faut activer Allow Nuget to download missing package.

L’option Automatically check for missing packages during build in Visual Studio permet d’effectuer le vérification des packages à chaque build.

Avec les fichiers de configuration

Au niveau global:
Il faut modifier %AppData%\Nuget\Nuget.config:

<configuration>
  <packageRestore>
    <add key="enabled" value="False" />
  </packageRestore>
</configuration>

Avant Nuget 2.6, il faut effectuer la modification dans .nuget\nuget.config dans chaque projet.

Pour vérifier la présence des packages à chaque build, il faut modifier le fichier: %AppData%\Nuget\Nuget.config:

<configuration>
  <packageRestore>
    <add key="automatic" value="False" />
  </packageRestore>
</configuration>

L’activation de ces options peuvent être globale en modifiant les fichiers dans:

%ProgramData%\Nuget\Config\[IDE]\[Version]\[SKU]

On peut aussi utiliser la variable d’environnement EnableNuGetPackageRestore en affectant la valeur true ou false pour surcharger les paramètres indiqués dans les fichiers de configuration. La valeur de la variable d’environnement sera utilisée par Visual Studio à condition qu’elle soit affectée avant le démarrage.

La priorité des fichiers de configuration se fait dans l’ordre indiqué précédemment.

Activer Nuget restore au niveau de MSBuild

Nuget restore existe sous cette forme pour les versions précédant Nuget 2.6. Les versions suivant Nuget 2.7 sont compatibles avec ce paramétrage toutefois il est déconseillé de l’utiliser.

L’activation se fait dans Visual Studio en effectuant un clique droit sur la solution et en sélectionnant Enable Nuget Package Restore.

A l’activation de cette option:

  • NuGet crée un répertoire .nuget au niveau de la solution et y ajoute Nuget.exe, un fichier nuget.config et un fichier nuget.targets.
  • NuGet met à jour tous les projets en ajoutant un nœud <RestorePackages>true<RestorePackages> et importe les targets se trouvant dans nuget.targets.
Breaking changes entre NuGet 2.6 et 2.7

Le paramétrage de Nuget restore au niveau de MSBuild pour les versions précédant Nuget 2.6 est incompatible avec les versions 2.7 et suivantes et peut mener à des comportements inattendus.

Quand Nuget restore est activée au niveau de MSBuild, Nuget 2.6 et précédents crée un répertoire .nuget avec un exécutable de Nuget.exe, un fichier Nuget.config et un fichier Nuget.targets. Avec Nuget 2.7 et suivant, la présence du répertoire .nuget peut causer ndes erreurs car la version de Nuget.exe correspondra à une ancienne version.

D’autre part si il existe un fichier .nuget\Nuget.targets dans le répertoire d’un projet, la restauration automatique (activée au niveau de Visual studio) sera ignorée pour ces projets.

Pour passer de Nuget restore au niveau de MSBuild à la restauration automatique, il faut d’abord d’abords supprimer tous les répertoires .nuget se trouvant dans les répertoires des projets et de la solution.

Il faut ensuite éditer les fichiers .csproj en supprimant les nœuds <RestorePackages> et supprimer toutes les références aux fichiers .nuget.targets.

Étapes effectuées par NuGet lorsque la restauration automatique est activée

Si NuGet restore est activée au niveau de Visual Studio:

  • un fichier .nuget\nuget.config est créé,
  • avant la compilation proprement dite, NuGet parcours tous les fichiers packages.config pour télécharger les packages à partir du cache ou à partir de la source.

Restauration automatique à partir de la ligne de commandes

Pour les versions 2.7 et suivantes, on peut exécuter Nuget restore au niveau d’une solution en exécutant dans le répertoire de la solution:

nuget restore

Ou

nuget restore [fichier .sln]
Références:
  • Repository nuget: https://www.nuget.org/
  • Extension NuGet Package Manager pour Visual Studio: https://marketplace.visualstudio.com/items?itemName=NuGetTeam.NuGetPackageManager
  • Consume NuGet packages in Visual Studio: https://www.visualstudio.com/en-us/docs/package/nuget/consume
  • Configuring NuGet behavior: https://docs.microsoft.com/fr-fr/nuget/consume-packages/configuring-nuget-behavior
  • NuGet File Locations: http://lastexitcode.com/projects/NuGet/FileLocations/
  • Target frameworks: https://docs.microsoft.com/en-us/nuget/schema/target-frameworks
  • NuGet Package Manager UI: https://docs.microsoft.com/en-us/nuget/tools/package-manager-ui
  • Installing NuGet: https://docs.microsoft.com/fr-fr/nuget/guides/install-nuget
  • Release Notes NuGet: https://docs.microsoft.com/en-us/nuget/release-notes/index
  • Creating NuGet packages: https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package
  • Installing and reinstalling packages with package restore: https://docs.microsoft.com/en-us/nuget/consume-packages/package-restore
  • Transforming source code and configuration files: https://docs.microsoft.com/en-us/nuget/create-packages/source-and-config-file-transformations
  • Supporting multiple .NET framework versions: https://docs.microsoft.com/en-us/nuget/create-packages/supporting-multiple-target-frameworks
  • NuGet CLI reference: https://docs.microsoft.com/fr-fr/nuget/tools/nuget-exe-cli-reference
  • NuGet.VisualStudio 4.0.0 : https://www.nuget.org/packages/NuGet.VisualStudio
  • Visual Studio 2017 can automatically recommend NuGet packages for unknown types: https://www.hanselman.com/blog/VisualStudio2017CanAutomaticallyRecommendNuGetPackagesForUnknownTypes.aspx
  • ASP.NET 5 is dead – Introducing ASP.NET Core 1.0 and .NET Core 1.0: https://www.hanselman.com/blog/ASPNET5IsDeadIntroducingASPNETCore10AndNETCore10.aspx
  • Installing Chocolatey: https://chocolatey.org/install
  • Nuget.CommandLine 3.5.0 with Chocolatey: https://chocolatey.org/packages/NuGet.CommandLine
  • Running PowerShell scripts during NuGet package installation and removal: https://everydaylifein.net/netframework/running-powershell-scripts-during-nuget-package-installation-and-removal.html
  • NuGet Package explorer sur Github: https://github.com/NuGetPackageExplorer/NuGetPackageExplorer
  • Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

    Routed commands en WPF en 2 min

    Les commandes routées WPF (i.e. routed commands) correspondent à une fonctionnalité permettant de découpler les éléments qui déclenchent une commande des éléments cibles sur lesquels les commandes vont s’exécuter.

    D’autres part les commandes routées permettent d’associer facilement des actions provenant de raccourcis clavier, d’actions réalisées avec la souris etc…
    Enfin elles peuvent activer ou désactiver un control graphique en fonction de la disponibilité de la commande.

    Caractéristiques des commandes routées

    Interface ICommand

    La fonctionnalité des commandes routées est accessible par l’intermédiare de l’interface System.Windows.Input.ICommand:

    public interface ICommand 
    { 
        event EventHandler CanExecuteChanged; 
     
        bool CanExecute(object parameter); 
         
        void Execute(object parameter); 
    }
    

    Ainsi:

    • Execute: cette méthode contient le code qui sera exécuté au déclechement de la commande.
    • CanExecute: cette méthode sera exécutée avant Execute() pour déterminer si la commande peut être exécutée. Elle renvoie true si l’exécution est possible, false sinon.
    • CanExecuteChanged: cette évènement se déclenche quand la valeur de CanExecute change.

    De nombreux éléments de base WPF fournissent cette interface de façon à y affecter une implémentation.

    Par exemple:

    • ButtonBase.Command (dans System.Windows.Controls.Primitives)
    • MenuItem.Command (dans System.Windows.Controls)
    • CheckBox.Command (dans System.Windows.Controls)
    • etc…

    De nombreux autres éléments de base proposent cette interface.

    L’intérêt de cette propriété pour ces différents éléments est de pouvoir affecter une implémentation qui définit la commande qui sera déclenchée par l’élément.

    Aspect routé

    Les commandes routées ont la particularité d’être “routées” c’est-à-dire qu’elles sont associées à des évènements routés (cf. Routed events en WPF en 3 min).

    Ainsi lorsque la commande est déclenchée, l’élément qui a déclenché cette commande ainsi que la commande elle même ignorent complêtement l’élément qui va exécuter réellement la commande. A vrai dire, il peut ne pas y avoir d’éléments qui va exécuter la commande.

    Au déclenchement de la commande, des évènements routés seront déclenchés et se propageront le long de l’arbre visuel (cf. Arbre logique et arbre visuel WPF en 2 min) de façon à savoir si la commande peut être exécutée et ensuite pour l’exécuter réellement.

    La propagation se fait de la même façon qu’un évènement routé normal. 2 séries de 2 évènements sont ainsi lancées. La première serie permet à un élément se trouvant dans l’arbre visuel s’il peut exécuté la commande:

    • PreviewCanExecute: évènement “tunnel” qui se propage de l’élément racine vers l’élément source (celui qui référence la commande).
    • CanExecute: évènement “bubble” qui se propage en sens inverse de la source vers l’élément racine de l’arbre.

    Si un élément indique qu’il peut exécuter la commande, la 2e série d’évènements routés se déclenche:

    • PreviewExecuted: évènement “tunnel” se propageant de la racine de l’arbre vers l’élément source.
    • Executed: évènement “bubble” se propageant de l’élément source vers la racine de l’arbre.

    Ainsi n’importe quel éléments capable d’intercepter ces évènements sera en mesure d’exécuter une action correspondant à la commande.

    Routage des commandes

    Lors du parcours des évènements le long de l’arbre visuel, les handlers (c’est-à-dire le code exécuté au déclenchement de la commande) sont exécutés successivement à la suite en fonction de leur position dans cet arbre.

    Le parcours de fait jusqu’à l’élément qui a le focus. Dans le cas où cet élément se trouve dans un “container” avec FocusManager.IsFocusScope à true le parcours se fait de façon plus complexe que celui évoqué précédement. Pour plus de détails sur ce point, se reporter à The Truth about Routed Commands Routing.

    Implémentations des commandes routées

    Il existe des implémentations différentes permettant d’utiliser les commandes routées, toutefois dans tous les cas, les mêmes instantications sont nécessaires:

    • Définir la commande routée en elle-même: cette commande est une instance de System.Windows.Input.RoutedCommand. Cette définition permet d’indiquer la classe propriétaire de la commande et le nom de la commande.
      Il est aussi possible de définir des commandes étant des instances de System.Windows.Input.RoutedUICommand (RoutedUICommand dérive de RoutedCommand).
    • Définir un binding entre la commande et des handlers. Ces handlers correspondent à des implémentations pour CanExecute() et Execute(), ils seront exécutés lorsque les évènements correspondant seront déclenchés.
    • On indique l’élément qui va déclencher la commande c’est-à-dire l’invoker. Par exemple, ça peut être le bouton sur lequel on devra cliquer pour déclencher la commande.

    Commandes prédéfinies

    Un certain nombre de commandes sont déjà définies et peuvent être utilisées directement. On peut trouver une définition de ces commandes dans:

    • System.Windows.Input.ApplicationCommands,
    • System.Windows.Input.ComponentCommands,
    • System.Windows.Input.MediaCommands,
    • System.Windows.Input.NavigationCommands,
    • System.Windows.Documents.EditingCommands,

    Par exemple, les commandes “Cut”, “Copy” et “Paste” se trouvent dans ApplicationCommands. On peut en voir la définition dans le code source de WPF: le fichier ApplicationCommands.cs

    Pour utiliser ces commandes, il suffit de définir un binding et l’invoker.

    Par exemple, si on veut exécuter une commande à l’exécution de la commande ApplicationCommands.Paste, il faut définir le binding avec un CommandBinding dans un user control:

    <Window x:Class="RoutedCommandExample.Window1" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:local="clr-namespace:RoutedCommandExample" 
        Title="Window1" Height="300" Width="300"> 
      <Window.CommandBindings> 
        <CommandBinding 
          Command="ApplicationCommands.Paste" 
          CanExecute="PasteCommandHandler_CanExecute" 
          Executed="PasteCommandHandler_Executed" /> 
      </Window.CommandBindings> 
      <!-- ... --> 
    </Window>
    

    Le code behind contiendra l’implémentation correspondant aux handlers:

    public partial class Window1 : Window 
    { 
      public Window1() 
      { 
          InitializeComponent(); 
      } 
     
      private void PasteCommandHandler_CanExecute(object sender, CanExecuteRoutedEventArgs e) 
      { 
          e.CanExecute = true; 
      } 
     
      private void PasteCommandHandler_Executed(object sender, ExecutedRoutedEventArgs e) 
      { 
          // Code exécuté au déclenchement de la commande 
      } 
    }
    

    Les handlers se déclencheront à chaque fois que la commande ApplicationCommands.Paste est exécutée.

    On peut complêter cet exemple en ajoutant un bouton pour déclencher l’évènement:

    <Button  
          Command="ApplicationCommands.Paste"  
          Content="Raise Paste" />
    

    Définir une commande

    Si on souhaite définir sa propre commande sans passer par une commande prédéfinie, il suffit d’ajouter dans le code behind:

    private static RoutedCommand customCommand =  
        new RoutedCommand("CustomCommand", typeof(Window1)); 
     
    public static RoutedCommand CustomCommand 
    { 
        get { return customCommand; } 
    }
    

    Le binding et l’invoker sont similaires à l’exemple précédent et font référence à cette commande:

    <Window.CommandBindings> 
        <CommandBinding  
            Command="{x:Static local:Window1.CustomCommand}" 
            CanExecute="PasteCommandHandler_CanExecute" 
            Executed="PasteCommandHandler_Executed" /> 
    </Window.CommandBindings> 
    <Grid> 
        <Button Command="{x:Static local:Window1.CustomCommand}" 
            Name="myButton" Content="Raise Custom command"/> 
    </Grid>
    

    RoutedUICommand

    A la différence de RoutedCommand, une RoutedUICommand possède une propriété Text sur laquelle il est possible de binder des éléments. La définition d’une commande de ce type est semblable:

    private static RoutedUICommand _pressMeCommand =  
        new RoutedUICommand("Custom Command", "CustomCommand", typeof(Window1));
    

    On peut, par exemple, binder le texte du bouton sur le texte de la commande:

    <Button Command="{x:Static local:Window1.CustomCommand}" 
            Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}" />
    

    Effectuer le binding dans le code behind

    Au lieu de définir le binding dans le code XAML, on peut le faire dans le code behind:

    public partial class Window1 : Window  
    { 
      public readonly RoutedCommand customCommand; 
      public readonly CommandBinding binding; 
     
      public Window1()  
      { 
          customCommand = new RoutedCommand("CustomCommand", typeof(Window1)); 
     
          InitializeComponent(); 
          this.DataContext = this; 
     
          this.binding = new CommandBinding(customCommand); 
          this.CommandBindings.Add(this.binding); 
     
          this.binding.Executed += PasteCommandHandler_Executed; 
          this.binding.CanExecute += PasteCommandHandler_CanExecute; 
      } 
     
      private void PasteCommandHandler_CanExecute(object sender, CanExecuteRoutedEventArgs e)  
      { 
          e.CanExecute = true; 
      } 
     
      private void PasteCommandHandler_Executed(object sender, ExecutedRoutedEventArgs e)  
      { 
          // Code exécuté au déclenchement de la commande 
      } 
    }
    

    CommandTarget

    Lors du déclenchement de la commande:

    • La source est l’élément qui a déclenché la commande,
    • Le “sender” est la fenêtre sur laquelle est définie le binding.

    On peut changer la source de la commande en utilisant l’argument System.Windows.Input.ICommandSource.CommandTarget. La plupart des éléments implémentent cette propriété.

    Dans l’exemple précédent, si on ajoute une TextBox:

    <Window.CommandBindings> 
        <CommandBinding  
            Command="{x:Static local:Window1.CustomCommand}" 
            CanExecute="PasteCommandHandler_CanExecute" 
            Executed="PasteCommandHandler_Executed" /> 
    </Window.CommandBindings> 
    <Grid> 
        <TextBox Name="newTextBox" Width="200" Height="40" /> 
        <Button Command="{x:Static local:Window1.CustomCommand}" 
            Name="myButton" Content="Raise Custom command"/> 
    </Grid>
    

    On peut paramétrer l’argument CommandTarget du bouton pour que la source soit la TextBox:

    <Button Command="{x:Static local:Window1.CustomCommand}" 
          CommandTarget="{Binding ElementName=newTextBox}" 
          Name="myButton" Content="Raise Custom command"/>
    

    A l’exécution, dans le handler, la source de la commande sera la TextBox.

    CommandParameter

    On peut ajouter un paramètre qui sera renseigné au déclenchement de la commande en utilisant System.Windows.Input.ICommandSource.CommandParameter. La plupart des éléments de base implémentent cette propriété.

    Si on l’utilise dans le cadre d’un bouton:

    <Button Command="{x:Static local:Window1.CustomCommand}" 
          CommandParameter="Parameter1" 
          Name="myButton" Content="Raise Custom command"/>
    

    Ce paramètre pourra être utilisé au déclenchement de la commande dans le handler:

    public void PasteCommandHandler_Executed(object sender, ExecutedRoutedEventArgs e) 
    { 
        MessageBox.Show(e.Parameter); 
    }
    

    KeyBinding

    Les key bindings permettent d’indiquer des raccourcis clavier ou des actions à la souris qui déclencheront des commandes.

    On peut définir ces key bindings dans le code XAML:

    <Window x:Class="RoutedCommandExample.Window1" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:local="clr-namespace:RoutedCommandExample" 
        Title="Window1" Height="300" Width="300"> 
      <Window.CommandBindings> 
        <CommandBinding 
          Command="ApplicationCommands.Paste" 
          CanExecute="PasteCommandHandler_CanExecute" 
          Executed="PasteCommandHandler_Executed" 
          /> 
      </Window.CommandBindings> 
      <Window.InputBindings> 
        <KeyBinding Command="ApplicationCommands.Paste" 
          Gesture="Ctrl+M"/> 
      </Window.InputBindings> 
      <!-- ... --> 
    </Window>
    

    Il est possible de définir un key binding pour une commande qui n’est pas pré-définie:

    <KeyBinding Command="{x:Static local:Window1.CustomCommand}" Gesture="Ctrl+M"/>
    

    On peut aussi utiliser la syntaxe suivante pour définir le key binding:

    <KeyBinding Command="{x:Static local:Window1.CustomCommand}" Modifiers="Control" Key="M"/>
    

    On peut aussi définir ce key binding dans le code behind:

    public Window1()  
    { 
        InitializeComponent(); 
     
        this.DataContext = this; 
      
        this.InputBindings.Add(new KeyBinding(this.customCommand, 
            new KeyGesture(Key.M, ModifierKeys.Control))); 
    }
    
    Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

    Git en 5 min

    Git est un gestionnaire de code source initialement créé et développé par Linus Torvarlds. Même si la signication du mot Git est plutôt péjorative, il s’agit très puissant qui propose plus de fonctionnalités que la plupart des autres gestionnaires de code source.

    Comparaison avec les autres gestionnaires de code source

    Il est rare d’aborder Git sans jamais avoir touché à un autre gestionnaire de code source (i.e. VCS pour Version Control System). Toutefois même on étant aguérri à l’utilisation de logiciels de gestion de source, il est préférable d’aborder Git sans essayer de transposer ses connaissances aux commandes. Par exemple même si certaines commandes sont communes entre SVN et Git, leur fonction est pratiquement toujours très différente.

    Il faut donc aborder Git en essayant d’avoir un œil neuf et s’intéresser à la fonction réelle de chaque commande.

    Les principales caractéristiques de Git sont:

    • Git est un système de contrôle de version distribué: chaque client qui se connecte à Git possède une copie locale du dépôt distant. Si ce dernier disparait, il peut être restoré à partir d’une des copies des clients.
    • Le stockage des fichiers est différent de la plupart des autres VCS. Au lieu de stocker des copies différentes des fichiers pour chaque version, Git stocke des versions de fichier seulement s’ils sont modifiés. Chaque version du dépôt est un “instantané” contenant un ensemble de fichiers dans une version précise. Il n’y a pas de copies inutiles lorsqu’un fichier n’est pas modifié.
    • La plupart des actions de l’utilisateur sont exécutées localement, ce qui limite la propagation d’erreurs dans le versionnement. Il est facile de revenir en arrière puisque la plupart de ces commandes sont locales et n’affectent pas d’autres utilisateurs.

    La plus grosse différence entre Git et les autres VCS, est que le répertoire de travail ne change pas si on passe d’une branche à l’autre.

    Par exemple, dans SVN si on souhaite avoir la branche principale (i.e. le trunk) et une autre branche, il faut faire un checkout dans 2 répertoires différents. Avec Git, on travaille toujours dans le même répertoire, quelque soit la branche sur laquelle on travaille. C’est Git qui modifie les fichiers de ce répertoire en fonction de la branche sur laquelle on travaille.

    Quelques éléments sont caractéristiques de Git:

    • Clé SHA-1: tous les commits sont identifiés avec une clé SHA-1 unique. Cette clé peut être utilisée pour récupérer un commit particulier.
    • HEAD: il s’agit d’une référence vers le nœud vers lequel pointe le répertoire de travail.
    • Dépôt distant (i.e. remote repository): il s’agit du répertoire de travail distant qui est partagé. Chaque client possède une copie locale de ce répertoire. Les clients ne travaillent pas directement à partir du dépôt distant, ils travaillent sur le dépôt local. Au moment de la livraison d’une modification sur le code source, le développeur envoie ces modifications sur le dépôt distant de façon à partager la modification avec d’autres développeurs.
    • Dépôt local (i.e. local repository): il s’agit de la copie locale du dépôt distant. Le développeur travaille généralement sur cette copie. A la différence des autres VCS, le développeur peut effectuer des commits, modifier ses commits, créer des branches sur son dépôt local. Ces modifications sont locales et ne sont pas visibles des autres développeurs. Au moment de la livraison de son travail, le développeur peut décider d’envoyer une partie de ses modifications de son dépôt local vers le dépôt distant, et ainsi partager son travail.

    Installation

    Sur windows, on peut installer Git for windows qui propose un bash pour utiliser les commandes Git et quelques outils graphiques accessibles avec le bouton droit de la souris (une fenêtre pour effectuer des commits, une fenêtre pour changer de branche, affichage des logs, etc…).

    Git for windows se télécharge sur: https://git-for-windows.github.io/.

    Premières étapes

    Renseigner quelques paramètres

    Il peut être nécessaire de préciser certains paramètres qui seront utilisés par la suite pour l’exécution de chaque commande: nom de l’utilisateur, adresse mail, coloration syntaxique du bash etc…

    Pour préciser ces paramètres, on utilise la commande git config:

    $ git config --global user.name "Robert Mitchoum" 
    $ git config --global user.email robertmitchoum@caramail.com
    

    Ces paramètres sont rajoutés dans le fichier .gitconfig. Pour savoir où se trouve ce fichier, on peut taper:

    $ git -c 'user.cmdline=true' config --list --show-origin
    

    On peut lister tous les paramètres qui se trouvent dans ce fichier en faisant:

    $ git config --list
    

    Pour indiquer la valeur d’un paramètre:

    $ git config user.email 
    

    Initialiser le répertoire de travail (git init)

    Comme indiqué plus haut, avec Git, on reste toujours dans le même répertoire quelque soit la branche, le tag ou la version sur laquelle on travaille.

    Pour initialiser le répertoire de travail:

    $ mkdir working_dir 
    $ cd working_dir 
    $ git init 
    Initialized empty Git repository in /Users/mitchoum/working_dir/.git/ 
    

    A ce stade le répertoire est presque vide. En réalité il contient un répertoire appelé “.git” qui contient les fichiers de travail de Git.

    Cloner un dépôt distant (git clone)

    Dans le cas où un dépôt distant existe et contient déjà des fichiers versionnés, on peut récupérer le contenu de ce dépôt sur son dépôt local en effectuant une étape de “cloning” (cette étape est l’équivalent du checkout sur SVN). Cette étape effectue seulement une copie du dépôt distant sur le dépôt local.

    On se place dans le répertoire de travail (après avoir exécuté git init) et on exécute l’instruction suivante:

    $ git clone [adresse du dépôt distant]  
    

    L’adresse du dépôt distant est généralement du type: https://github.com/repository/test.git

    Par exemple:

    $ git clone https://github.com/repository/test.git 
    $ Cloning into 'test'... 
    $ remote: Counting objects: 12, done. 
    $ remote: Compressing objects: 100% (4/4), done. 
    $ remote: Total 12 (delta 1), reused 11 (delta 0), pack-reused 0 
    $ Unpacking objects: 100% (12/12), done. 
    

    Afficher l’état du dépôt (git status)

    L’état du dépôt est une information importante car elle permet d’indiquer dans quelle branche on se trouve et l’état des fichiers qui s’y trouvent.

    Pour afficher l’état, on écrit:

    $ git status 
    On branch master 
     
    Initial commit 
     
    nothing to commit (create/copy files and use "git add" to track) 
    

    Effectuer des commits

    Quand le dépôt distant est copié localement (cf. git clone), le developeur peut modifier le code source et ensuite commiter ses modifications. A la fin de cette étape, les modifications restent locales et ne sont pas partagées.

    Workflow pour effectuer un commit

    Le workflow n’est pas tout à fait similaire aux autres VCS. Pour commiter dans Git, il faut effectuer les étapes suivantes:

    • Ajouter les fichiers à commiter: cette étape ajoute les fichiers à versionner mais elle est aussi nécessaire pour livrer une modification. Elle consiste à indexer certains fichiers (i.e. Stage) pour les commiter ensuite dans le HEAD.
    • Commiter les fichiers sur le dépôt local.

    Ajouter les fichiers à commiter (git add)

    Cette étape est nécessaire pour indiquer les fichiers à versionner mais elle permet aussi d’indiquer quels sont les fichiers à commiter lorsqu’ils sont déjà versionnés. En pratique, cette étape ajoute un fichier dans un “index”. Dans l’arbre Git, un “index” est un espace de transit où les fichiers sont stockés provisoirement avant d’être commités.

    git add [chemin du fichier à ajouter] 
    

    Enlever un fichier des fichiers à commiter

    Si on a exécuté un git add par mégarde pour un fichier, on peut revenir en arrière en faisant:

    git reset HEAD [chemin du fichier] 
    

    Commiter des fichiers “stagés”

    A la fin de cette étape, le commit est local et non visible des autres développeurs:

    • git commit pour commiter et ouvrir un éditeur de texte pour indiquer un commentaire.
    • git commit -a pour commiter tous les fichiers se trouvant dans le répertoire (il n’est pas nécessaire de les “stager” avant.
    • git commit -m pour commiter directement avec un message.

    Supprimer des fichiers versionnés

    Il faut d’abord supprimer le fichier de Git:

    git rm [chemin du fichier à supprimer] 
    

    Il faut ensuite commiter pour que la suppression de Git soit effective. Si le fichier a été modifié depuis sa version commitée, pour le supprimer, il faut forcer sa suppresion:

    git rm -f [chemin du fichier à supprimer] 
    

    On peut aussi supprimer un fichier parmi les fichiers “stagés” sans le supprimer complétement:

    git rm --cached [chemin du fichier] 
    
    Utilisation de vi

    Pour chaque commande où il est nécessaire d’ajouter des commentaires (comme git commit ou git rebase) ou pour les commandes affichant beaucoup de lignes (comme git log), le bash ouvre un fenêtre vi. L’utilisation de cet éditeur n’est pas toujours facile.
    Quand vi est ouvert, il est en mode commande, il faut indiquer des commandes pour modifier le fichier:

    • Pour passer en mode édition et modifier le fichier: taper "i", effectuer les modifications et taper “Echap” pour sortir du mode édition.
    • Pour passer en mode remplacement: taper "R", effectuer le remplacement et taper “Echap” pour sortir de ce mode.
    • Pour supprimer un ligne: taper "dd". "D" permet de supprimer jusqu’à la fin du fichier.
    • Pour annuler les dernières modifications: taper "u".
    • Pour chercher dans le fichier: taper "/" suivi de la chaîne à chercher. En tapant "n" on peut passer à la chaine suivante.
    • Pour enregistrer les modifications: taper "w".
    • Pour quitter sans enregistrer: taper ":q" si le fichier n’a pas été modifié sinon ":q!".
    • Pour quitter en enregistrant: taper ":wq".
    • Pour faire un copier/coller: taper "Y" permet de copier une ligne dans le tampon. "nY" permet de copie "n" lignes. "P" colle les lignes avant le curseur et "p" colle les lignes après le curseur.

    Sur linux-france, on peut avoir une liste plus complête des commandes de vi.

    Afficher les modifications d’un fichier avant de commiter (git diff)

    Lorsqu’on modifie un fichier versionné et que l’on souhaite vérifier les modifications apportées avant de le “stager” ou de le commiter, on peut utiliser git diff de cette façon:

    git diff [chemin du fichier]

    Avec la commande précédente, on affichera les modifications pour un fichier n’étant pas encore “stagé”.

    On peut aussi comparer les modifications par rapport au HEAD du dépôt local:

    git diff HEAD [chemin du fichier]

    On peut comparer le contenu de l’index avec le dépôt local:

    git diff --cache [chemin du fichier]

    Au lieu d’afficher les modifications pour un seul fichier, on peut aussi le faire pour tous les fichiers qui se trouvent dans le répertoire de travail. La plupart du temps, l’affichage pour tous les fichiers n’est pas forcément très lisible. Pour afficher ces modifications, il suffit de ne pas préciser le chemin d’un fichier, par exemple:

    git diff

    Afficher l’historique (git log)

    On peut afficher l’historique en utilisant git log:

    $ git log 
    [22a0ff3ccccdd9a94f5e0fb59b6307ebee0c8d98] 
    Author: Robert Mitchoum 
    Date:   Sat Feb 11 19:02:29 2017 +0100 
        2e commit 
     
    [24d49ab46717545a0c0f9f94ee83ac0d60808d3f] 
    Author: Robert Mitchoum 
    Date:   Sat Feb 11 18:57:36 2017 +0100 
        1er commit
    

    Les logs sont affichés avec la clé SHA-1 et le nom de la personne qui a commité.

    Par exemple, si on commence avec un dépôt local à jour par rapport au dépôt distant, le graphe se présente de cette façon:

    Après 2 commits, le graphe devient:

    On peut afficher les 5 derniers commits avec:

    $ git log -5
    

    On peut afficher l’historique sous forme de graphe simple:

    $ git log --graph --oneline 
    

    On peut complêter l’historique de la branche en cours avec les autres branches:

    $ git log --graph –oneline --all 
    

    Envoyer ses modifications vers le dépôt distant

    Pour envoyer des modifications effectuées sur le dépôt local vers le dépôt distant, il faut exécuter:

    $ git push origin master
    

    origin est le nom du dépôt distant. Il s’agit du nom par défaut généralement utilisé lorsqu’on a qu’un seul dépôt distant.
    master est le nom de la branche sur laquelle on travaille. master est l’équivalent du trunk sous SVN.

    Pour que Git accepte de pousser les modifications vers le dépôt distant, il faut que le dépôt local soit à jour (avec git pull par exemple).

    A la fin de cette étape, les modifications sont partagées et d’autres développeurs peuvent y accéder.

    Si le dépôt local ne provient pas d’une copie d’une dépôt distant (cf. Clone), on peut connecter son dépôt local au dépôt distant en faisant:

    git remote add origin [nom du serveur]  
    

    Si la branche est nouvelle, on peut indiquer que cette branche est la branche “upstream” c’est-à-dire la branche par défaut lorsqu’on précise pas de paramètres avec git push.
    Pour indiquer que la branche est la branche “upstream”, on exécute:

    git push --set-upstream origin [nom de la branche] 
    

    Après il suffit d’exécuter sans argument la commande suivante pour pousser dans la bonne branche:

    git push 
    

    Dans le cas où le message suivant s’affiche quand on essaie de pousser:

    fatal: The current branch master has no upstream branch. 
    To push the current branch and set the remote as upstream, use 
       git push --set-upstream origin master
    

    On est pas obligé de paramétrer la branche en tant que branche “upstream”. On peut aussi pousser en indiquant dans quelle branche:

    git push origin master
    

    Dans l’exemple précédent, si on pousse 2 modifications sur le dépôt distant, le graphe devient:

    Branche

    Les branches sous Git sont similaires aux branches dans les autres VCS. Toutefois il est courant et plus pratique d’utiliser des branches sous Git car:

    • Elles ne sont pas coûteuses, seules les modifications sont sauvegardées, il n’y a pas de copie complète de tous les fichiers du projet.
    • D’autres part, on peut facilement travailler sur une branche localement sans livrer son travail sur une branche dans le dépôt distant. Les branches locales permettent de travailler sur des sujets différents en même temps (par exemple corrections de bugs ou implémentations de nouvelles fonctionnalités) et de livrer sur la branche principale ou une autre branche sur le dépôt distant.
    • Enfin on peut facilement modifier ces commits sur sa branche locale, les supprimer ou en modifier les commentaires (avec commit amend ou rebase).

    Créer une branche et passer directement dessus:

    git checkout -b [nom de la branche]
    

    Répercuter la création de la branche sur le dépôt distant (le cas échéant):

    git push origin [nom de la branche]  
    

    Passer sur une branche lorsqu’elle est déjà créée:

    git checkout [nom de la branche]  
    

    Créer une branche sans passer dessus:

    git branch [nom de la branche]  
    

    Lister les branches locales:

    git branch --list  
    

    Si on crée la branche “new_branch” sur le dépôt local, le graphe est:

    Après 2 commits dans la branche “new_branch”, le graphe devient:

    Si on retourne dans la branche principale (cf. master) et qu’on effectue 2 commits dans la branche principale:

    Supprimer une branche

    D’abord supprimer la branche localement en faisant:

    git branch --delete [nom de la branche]  
    

    Ensuite pour pousser la suppression sur le dépôt distant (le cas échéant):

    git push origin :[nom de la branche]  
    

    Renommer une branche

    D’abord localement:

    git branch -m [nom existant de la branche] [nouveau nom de la branche]  
    

    Pousser le renommage sur le dépôt distant:

    git push origin :[ancien nom de la branche]  
    

    Effectuer des merges

    Mettre à jour le dépôt local (git pull)

    La façon la plus simple de rapatrier localement les modifications se trouvant sur le dépôt distant est d’exécuter:

    git pull
    
    PULL = FETCH + MERGE

    Cette étape ne récupére pas seulement les modifications sur dépôt distant, elle effectue un merge des branches distantes avec les branches locales. Ces merges modifient les fichiers locaux et peuvent mener à des conflits. Avant de pouvoir commiter, il faudra résoudre ces conflits.

    Une méthode plus progressive pour mettre à jour le dépôt local est d’effectuer:

    git fetch
    

    Cette commande récupére seulement les modifications distantes sans effectuer de merge.

    Après un git fetch, on peut soit merger en exécutant git merge. On peut aussi utiliser git rebase qui une commande plus sure pour éviter les conflits.

    Merger 2 branches (git merge)

    Pour fusionner 2 branches, on peut les merger comme avec les autres VCS (ce n’est pas la seule méthode, on peut aussi utiliser rebase qui est une méthode provoquant moins d’erreurs et moins de conflits).

    Pour merger 2 branches:
    Il faut se placer sur la branche de destination (avec git checkout) puis exécuter:

    git merge [nom de la branche à merger]
    

    Des conflits peuvent résulter de ce merge, il faut les résoudre avant de commiter. Les conflits sont indiquées dans les fichiers avec des chevrons comme pour les autres VCS. Pour résoudre les conflits, il faut supprimer les chevrons avec un éditeur de texte en indiquant la partie du fichier à conserver.
    Après résolution du conflit, on peut ajouter le fichier à l’index pour le commiter en faisant:

    git add [nom du fichier]  
    

    Dans l’exemple précédent, si on merge la branche principale (cf. master) avec la branche “new_branch”:

    Tags

    Les tags sont aussi très similaires aux tags des autres VCS. Ils permettent d’apporter un nom à un commit particulier. Il faut avoir en tête que tous les commits dans Git sont identifiés avec une clé SHA-1.

    Il existe 2 types de tag:

    • Les tags légers (i.e. lightweight tag): ce sont des tags censés être provisoires. Ils ne contiennent qu’une clé SHA-1 vers un commit.
    • Les tags annotés (i.e. annotated tag): ils contiennent un SHA-1 comme les tags légers et aussi un objet qui est une copie de ce qui est taggué.

    Tags légers

    Créer un tag léger (i.e. lightweight tag):

    git tag [nom du tag] [clé SHA-1 du commit à tagguer]
    

    Pour tagguer le dernier commit:

    git tag [nom du tag] -1
    

    Pour “pousser” le tag créé vers le dépôt distant:

    git push origin –tags
    

    Supprimer un tag local:

    git tag –d [nom du tag] 
    

    Supprimer un tag distant (après l’avoir supprimé localement):

    git push origin :refs/tags/[nom du tag]
    

    Liste les tags existants:

    git tag --list 
    

    Récupérer un tag se trouvant sur le dépôt distant:

    git checkout tags/[nom du tag] 
    

    Récupérer un tag et créer une branche (locale):

    git checkout tags/[nom du tag] -b [nom de la branche]
    

    Tags annotés

    Créer un tag annoté (i.e. Annotated tag):

    git tag –a [nom du tag] -m [commentaire sur le tag] 
    

    Pour “pousser” le tag annoté vers le dépôt distant:

    git push origin [nom du tag] 
    

    Voir le détail d’un tag:

    git show [nom du tag] 
    

    Pour récupérer un tag annoté se trouvant sur dépôt distant, la syntaxe est la même que pour les tags légers:

    git checkout tags/[nom du tag]
    

    Rebase

    git rebase est une commande très puissante capable d’effectuer de nombreuses opérations sur le dépôt local:

    • Modifier des commits
    • Réunir 2 branches

    Le grand intérêt de git rebase est de faire en sorte d’avoir un historique plus linéaire et d’éviter de laisser apparaître des commits inutils qui n’apportent pas d’informations.

    Modifier des commits

    Modifier des commits localement

    git rebase peut être utilisé d’abord pour modifier facilement des commits dans son dépôt local. L’intérêt est de ne laisser apparaître que les commits utiles.

    Par exemple, si on effectue un premier commit dans son dépôt local modifiant un fichier et qu’on se rende compte d’une erreur avant d’effectuer un git push. Les modifications sont encore dans le dépôt local. On corrige cette erreur et on effectue un nouveau commit. Sachant que les 2 modifications concernent la même fonctionnalité, il n’y a pas d’intérêt que les 2 commits apparaissent de façon distincte. Un seul commit suffit.

    Ainsi, si on affiche l’historique d’un fichier en affichant les 3 derniers commits:

    $ git log -3 
    [e70d0a17f93e2609462db5593046ff1b2a7eb738] 
    Date:   Sat Feb 11 19:05:05 2017 +0100 
        2e commit 
     
    [2f222d1ad01a36ac02a8e9cc9359a81217644f69] 
    Date:   Sat Feb 11 19:02:29 2017 +0100 
        1er commit 
     
    [24d49ab46717545a0c0f9f94ee83ac0d60808d3f] 
    Date:   Sat Feb 11 18:57:36 2017 +0100 
        Modification
    

    Si on veut modifier les 2 derniers commits pour qu’ils n’apparaissent que sur la forme d’un seul commit:

    $ git rebase -i HEAD~2 
    

    Cette commande permet de modifier les 2 derniers commits par rapport à la référence (HEAD~2) en utilisant l’invite de commande interactive (avec "-i").
    La commande ouvre une fenêtre vi qu’il est possible d’éditer pour écrire des instructions:

    pick 2f222d1 Autre modif 
    pick e70d0a1 Modif dans le master 
     
    # Rebase 24d49ab..e70d0a1 onto 24d49ab (2 commands) 
    # 
    # Commands: 
    # p, pick = use commit 
    # r, reword = use commit, but edit the commit message 
    # e, edit = use commit, but stop for amending 
    # s, squash = use commit, but meld into previous commit 
    # f, fixup = like "squash", but discard this commit's log message 
    # x, exec = run command (the rest of the line) using shell 
    # d, drop = remove commit 
    # 
    # These lines can be re-ordered; they are executed from top to bottom. 
    # 
    # If you remove a line here THAT COMMIT WILL BE LOST. 
    # 
    # However, if you remove everything, the rebase will be aborted. 
    # 
    # Note that empty commits are commented out
    
    Ordre d’affichage inversé

    Les commits sont indiqués du plus vieux au plus récent (l’ordre est inversé).

    Les pick sont des commandes à effectuer sur les lignes de commit. D’autres instructions sont possibles:

    • pick: permet de garder le commit inchangé,
    • reword: permet d’indiquer qu’on souhaite modifier le commentaire du commit. Il n’est pas nécessaire de modifier le texte directement dans cette fenêtre. Il faut juste remplacer pick par reword. Après fermeture de la fenêtre vi, une autre fenêtre vi s’ouvrira pour modifier le commentaire.
    • squash: le commit sera regroupé avec le commit précédent. Il n’y aura pas de pertes dans le contenu des fichiers. Au lieu d’avoir 2 commits, il n’en restera qu’un seul. Il n’est pas nécessaire de modifier le texte directement dans cette fenêtre. Il faut juste remplacer pick par squash. Après fermeture de la fenêtre vi, une autre fenêtre vi s’ouvrira pour modifier le commentaire.
    • fixup: même modification que squash sans la prise en compte du commentaire.
    • drop: supprime le commit. Les modifications effectuées par ce commit seront perdues.
    • edit: cette commande permet d’afficher par la suite une invite de commande qui sera appliquée seulement au commit pour lequel on a indiqué edit.

    Pour réunir le 2e commit au 1er, on remplace pick par squash. On passe en mode édition dans vi en tapant "i", on replace les termes et on enregistre en tapant ":wq":

    pick 2f222d1 1er commit 
    squash e70d0a1 2e commit 
     
    # Rebase 24d49ab..e70d0a1 onto 24d49ab (2 commands) 
    # 
    # ...
    

    Un écran suivant propose de modifier le commentaire puis:

    [detached HEAD 22a0ff3] 1er commit 
     Date: Sat Feb 11 19:02:29 2017 +0100 
     1 file changed, 2 insertions(+) 
    Successfully rebased and updated refs/heads/master.
    

    Si on affiche l’historique:

    $ git log -2 
    Seulement 2 lignes apparaissent: 
    [22a0ff3ccccdd9a94f5e0fb59b6307ebee0c8d98] 
    Date:   Sat Feb 11 19:02:29 2017 +0100 
        Autre modif 
        Modif dans le master 
     
    [24d49ab46717545a0c0f9f94ee83ac0d60808d3f] 
    Date:   Sat Feb 11 18:57:36 2017 +0100 
        Commentaire de test.
    

    Changer l’histoire dans le dépôt distant

    Si on modifie l’histoire en utilisant git rebase, on peut pousser les modifications sur le dépôt distant en forçant avec "-f":

    $ git push -f
    

    Cette étape peut compliquer les merges futures pour les autres développeurs puisqu’on peut changer l’historique du dépôt distant.

    Réunir 2 branches

    Branche ayant un nœud commun avec le master

    Le terme “réunir” est utilisé içi à la place de merger pour éviter confusion avec git merge. git rebase est complêtement différent de git merge:

    • git merge réunit 2 branches en mergeant les fichiers. Dans l’historique, la branche principale (i.e. branche “master”) et la branche mergée dans la branche principale sont visibles (cf. git merge).
    • git rebase ne merge pas 2 branches mais rejoue les commits de la branche à la suite du dernier commit du “master”. Git cherche alors, le nœud commun entre la branche et le master puis il rejoue les commits à la suite du master à partir de ce nœud. L’historique apparaît linéaire puisque les commits sont simplement ajoutés à ceux du master.

    Si on considère le master avec une branche appelée “featureBranch”, on a un graphe de ce type après quelques commits:

    Dans un premier temps, on se synchroniser par rapport à la branche master et on rejoue les commits à la fin du master:

    $ git checkout featureBranch 
    $ git rebase master
    

    Le graphe devient:

    Ensuite, on se place dans la branche master:

    $ git checkout master
    

    Le graphe est alors:

    On merge la branche “featureBranch” dans le master qui ne va rajouter que les nouveaux commits:

    $ git merge featureBranch 
    

    Le graphe devient:

    Comme on peut le voir, l’historique de la branche “master” reste linéaire et les commits sont ajoutés à la fin des commits du master.
    Pour réunir 2 branches, il est préférable d’utiliser git rebase par rapport à git merge pour minimiser les conflits et avoir un historique plus clair.

    Branche séparée du master

    Dans le cas où la branche est complêtement séparée du master c’est-à-dire qu’il n’y a pas des nœuds commun avec le master. L’utilisation de git rebase est un peu différente car il ne pourra pas trouver de nœud commun avec le master.

    Par exemple, si on considère les branches suivantes:

    La branche “otherBranch” n’a pas de nœud commun avec le master. “otherBranch” a un nœud commun “featureBranch”.

    Pour effectuer un “rebase” sur le master, il faut rajouter l’argument --onto:

     $ git rebase –onto master featureBranch otherBranch 
    

    Le graphe devient:

    Il suffit ensuite de merger la branche “otherBranch”:

    $ git checkout master
    

    Le graphe est:

    Enfin après le merge:

    $ git merge otherBranch
    

    Le graphe après le merge:

    Si on supprime la branch “featureBranch”, l’historique de la branche “master” est linéaire:

    $ git branch –d featureBranch
    

    Le graphe après suppresion de la branche “featureBranch”:

    Annuler des modifications

    Il est possible d’annuler la plupart des opérations effectuées avec Git.

    Avant d’effectuer un commit

    Si un fichier a été modifié et qu’on ne désire pas le commiter, on peut annuler les modifications en exécutant:

    git checkout -- [chemin du fichier]
    
    Les modifications seront perdues

    Après cette étape, les modifications effectuées sur le fichier seront perdues.

    On peut aussi ramener un fichierà un commit précédent:

    git checkout [SHA-1 du commit] -- [chemin du fichier]
    

    Pour supprimer tous les fichiers qui ne sont pas trackés (i.e. fichiers non versionnés):

    git clean -f
    

    Pour annuler, supprimer tous les changements locaux et récupérer les modifications effectuées sur le dépôt distant:

    git fetch origin 
    git reset --hard origin/master 
    

    Modifier le dernier commit

    Il est possible de modifier facilement le dernier commit effectué même dans le cas il a été poussé vers le dépôt distant avec la commande git commit --amend.

    Par exemple pour modifier pour le commentaire utilisé pour le commit:

    git commit --amend –m [nouveau commentaire du commit]
    

    On peut pousser la modification vers le dépôt distant en faisant:

    git push origin master –f 
    

    On peut aussi effectuer cette étape en utilisant git rebase.

    On peut utiliser plus généralement git commit –amend par exemple pour rajouter un fichier au dernier commit:

    $ git commit -m [commentaire du premier commit] 
    $ git add [fichier à rajouter au commit] 
     
    $ git commit --amend
    

    La dernière ligne va commiter l’ajout du fichier.

    Annuler un commit

    On peut annuler un commit, en replaçant le HEAD sur un commit particulier en utilisant la clé SHA-1 de ce commit:

    git reset [label ou SHA-1 du commit]
    

    Pour pousser, il faut ajouter l’argument “-f” pour forcer:

    git push –f 
    

    Il est préférable que personne n’ait récupéré le dépôt distant avant d’avoir effectué cette étape. Si un développeur a effectué un git pull entre le dernier commit et le git push, il travaillera avec un HEAD qui n’est plus le HEAD du dépôt distant.

    Cherry-pick

    git cherry-pick est une commande utile pour récupérer un commit effectué sur une autre branche complêtement séparée de la branche courante. Elle permet d’appliquer les modifications correspondant à un seul commit sans effectuer de merge. On utilise la clé SHA-1 pour identifier ce commit et on l’applique sur la branche courante. Cette opération peut mener le cas échéant à des conflits.

    Pour utiliser git cherry-pick, il suffit de:

    • se placer dans la branche sur laquelle on veut appliquer le commit,
    • il faut avoir le SHA-1 du commit

    On exécute ensuite:

    $ git cherry-pick [SHA-1 du commit]
    

    Il n’est pas nécessaire d’effectuer un commit après cette étape, git cherry-pick effectue lui-même un commit.

    Stash

    git stash permet de mettre de coté des fichiers lorsqu’ils sont modifiés mais qu’ils ne sont pas placés dans l’index et qu’ils ne sont pas encore commités. Dans une branche donnée, on va donc mettre ces fichiers de coté de façon à effectuer d’autres modifications. On pourra ensuite récupérer les fichiers mis de coté. L’historique n’est pas affecté lorsqu’on utilise git stash. D’autre part, cette opération s’effectue seulement sur le dépôt local.

    Si le dépôt local comprends des fichiers modifiés:

    $ git status 
    On branch master 
    Your branch is up-to-date with 'origin/master'. 
    Changes not staged for commit: 
      (use "git add <file>..." to update what will be committed) 
      (use "git checkout -- <file>..." to discard changes in working directory) 
     
    modified:   test.txt 
     
    no changes added to commit (use "git add" and/or "git commit -a")
    

    Si on met de coté les modifications en “stashant”:

    $ git stash 
    Saved working directory and index state WIP on master: e9bc55e Merge branch 'master' of https://github.com/repository/test 
    HEAD is now at e9bc55e Merge branch 'master' of https://github.com/repository/test 
    

    Le répertoire ne contient plus de modifications:

    $ git status 
    On branch master 
    Your branch is up-to-date with 'origin/master'. 
    nothing to commit, working tree clean 
    

    On peut voir la liste des stash en exécutant:

    $ git stash list 
     stash@{0}: WIP on master: e9bc55e Merge branch 'master' of https://github.com/repository/test 
    

    Le nom du stash est “stash@{0}”.

    On peut appliquer le premier stash sans le supprimer de la liste en exécutant:

    $ git stash apply
    

    Pour appliquer un stash particulier sans le supprimer de la liste:

    $ git stash apply [nom du stash] 
    

    Pour appliquer le premier stash en le supprimant de la liste, on peut exécuter:

    $ git stash pop 
    

    De même pour appliquer un stash particulier en le supprimant de la liste:

    $ git stash pop [nom du stash]
    
    Références

    Pour s’exercer:

    Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

    “Mocker” une dépendance statique

    Les objets statiques sont souvent utilisés pour mutualiser rapidement l’implémentation d’un comportement ou plus rarement pour partager des instances d’objets entre plusieurs classes.
    L’utilisation d’objets statiques peut être un choix d’architecture maitrisé. Dans ce cas, on a la possibilité de modifier l’implémentation des objets statiques ainsi que des objets consommateurs.

    Dans d’autres cas, l’utilisation d’objets statiques peut être imposée par l’implémentation d’une bibliothèque par exemple. C’est le plus souvent dans ce cas qu’il est plus difficile de mocker les objets statiques pour tester l’implémentation des classes consommatrices.

    Cet article présente quelques méthodes pour mocker des objets statiques:

    • Dans le cas où l’implémentation de l’objet statique est maitrisé: en injectant une dépendance,
    • Dans le cas où l’implémentaiton est contrainte.

    On considère l’exemple suivant permettant de calculer l’âge en fonction de la date actuelle:

    public class AgeCalculator 
    { 
        public int GetAge(DateTime dateOfBirth) 
        { 
            DateTime now = DateTime.Now; 
            if (dateOfBirth > now) 
                throw new InvalidOperationException(
                   "Date of birth shall be before current date."); 
         
            return now.Year - dateOfBirth.Year; 
        } 
    }
    

    Cette implémentation très simpliste ne prends pas en compte les mois et donc peut s’avèrer faux toutefois si on souhaite tester AgeCalculator.GetAge(), on pourrait écrire le test suivant:

    [Test] 
    public void Age_Shall_Be_14_When_DateOfBirth_Is_In_2002() 
    { 
        DateTime fixedDateOfBirth = new DateTime(2002, 06, 06); 
     
        var ageCalculator = new AgeCalculator(); 
        int age = ageCalculator.GetAge(fixedDateOfBirth); 
         
        Assert.AreEqual(14, age); 
    }
    

    Ce test fonctionnera jusqu’au 1 janvier 2017. A partir de cette date, il sera faux. On peut donc modifier le test pour être moins dépendant de la date en cours.

    [Test] 
    public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago() 
    { 
        DateTime now = DateTime.Now; 
        DateTime dateOfBirth = new DateTime(now.Year - 14, now.Month, now.Day); 
     
        var ageCalculator = new AgeCalculator(); 
        int age = ageCalculator.GetAge(dateOfBirth); 
     
        Assert.AreEqual(14, age); 
    }
    

    Ce test fonctionnera tout le temps mais peut échouer à certaines occasions. Par exemple, si le test est exécuté le 31 décembre 2016 à 23h59min59sec999ms. La date de naissance 14 ans plus tôt sera le 31 décembre 2002 à 23h59min59sec999ms. Toutefois pendant l’exécution de AgeCalculator.GetAge(), l’horloge continue d’avancer et au moment d’exécuter System.DateTime.Now dans la fonction AgeCalculator.GetAge(), la date actuelle devient: 1er janvier 2017 à 0h00min00sec001ms. L’âge calculé devient 15 et le test échoue.

    Pour que le test réussisse toujours et ne soit plus dépendant de la date en cours, il faudrait pouvoir maitriser la date renvoyée par la propriété statique DateTime.Now. Or DateTime est une classe du framework.

    Injecter une dépendance

    La première solution consiste à injecter une dépendance dans la classe consommatrice plutôt que de permettre d’utiliser directement la propriété statique. L’injection de la dépendance permettra, ainsi, de casser la dépendance vers la propriété statique.

    L’implémentation de AgeCalculator devient:

    public class AgeCalculator 
    { 
        private ICurrentDateHandler currentDateHandler; 
     
        public AgeCalculator(ICurrentDateHandler currentDateHandler) 
        { 
            this.currentDateHandler = currentDateHandler; 
        } 
     
        public int GetAge(DateTime dateOfBirth) 
        { 
            DateTime now = this.currentDateHandler.GetCurrentDate(); 
            if (dateOfBirth > now) 
                throw new InvalidOperationException(
                   "Date of birth shall be before current date."); 
         
            return now.Year - dateOfBirth.Year; 
        } 
    }
    

    Avec:

    public interface ICurrentDateHandler 
    { 
        DateTime GetCurrentDate(); 
    }
    

    Et:

    public class CurrentDateHandler : ICurrentDateHandler 
    { 
        public DateTime GetCurrentDate() 
        { 
            return DateTime.Now; 
        } 
    }
    

    On casse le couplage entre AgeCalculator et System.DateTime en déportant la dépendance à System.DateTime dans une autre classe:
    Cette implémentation est plus en accord avec le principe SOLID (http://cdiese.fr/principe-de-developpement-oriente-objet-solid/) puisqu’elle sépare les responsabilités.
    Elle facilite les tests unitaires puisqu’on peut maintenant maitrisé la date actuelle fournie à AgeCalculator.

    On peut facilement mocker ICurrentDateHandler, par exemple, avec Moq:

    using Moq; 
    // ...
    
    DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
     
    Mock<ICurrentDateHandler> currentDateHandlerMock = new Mock<ICurrentDateHandler>(); 
    currentDateHandlerMock.SetUp(h => h.GetCurrentDate()).Returns(expectedCurrentDate);
    

    Le test devient:

    [Test]  
    public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()  
    {  
        DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
     
        Mock<ICurrentDateHandler> currentDateHandlerMock = new Mock<ICurrentDateHandler>(); 
        currentDateHandlerMock.SetUp(h => h.GetCurrentDate()).Returns(expectedCurrentDate); 
     
        DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,  
            expectedCurrentDate.Month, expectedCurrentDate.Day);  
      
        var ageCalculator = new AgeCalculator(currentDateHandlerMock.Object);  
        int age = ageCalculator.GetAge(dateOfBirth);  
      
        Assert.AreEqual(14, age);  
    }
    

    Le test ne dépend plus de la date actuelle et réussit toujours.

    Utiliser une Factory

    L’inconvénient de l’exemple précédent est que l’injection de ICurrentDateHandler se fait dans le constructeur de AgeCalculator ce qui contraint l’extensibilité de la classe AgeCalculator.

    Si on doit créer un objet ICurrentDateHandler suivant certains critères connus au moment de l’exécution de AgeCalculator.GetAge(), on peut passer par l’intermédiaire d’une Factory. Cette Factory est injectée par le constructeur toutefois, elle permet de créer l’objet ICurrentDateHandler suivant une logique implémentée directement dans la Factory. La création peut aussi être exécutée au moment de l’appel à AgeCalculator.GetAge() et non à la construction de AgeCalculator.

    Par exemple, si on doit introduire une notion de fuseaux horaires sans modifier la signature de AgeCalculator.GetAge(), on peut le faire par l’intermédiaire de la Factory:

    public class DateHandlerFactory : IDateHandlerFactory 
    { 
        private TimeZoneInfo sourceTimeZone; 
        private TimeZoneInfo destinationTimeZone; 
     
        public CurrentDateHandlerFactory(TimeZoneInfo sourceTimeZone,  
            TimeZoneInfo destinationTimeZone) 
        { 
             this.sourceTimeZone = sourceTimeZone; 
             this.destinationTimeZone = destinationTimeZone; 
        } 
     
        public ICurrentDateHandler GetCurrentDateHandler() 
        { 
            return new CurrentDateHandler(this.sourceTimeZone, this.destinationTimeZone); 
        } 
    }
    

    Avec:

    public interface IDateHandlerFactory 
    { 
        ICurrentDateHandler GetCurrentDateHandler(); 
    }
    

    urrentDateHandler devient:

    public class CurrentDateHandler : ICurrentDateHandler 
    { 
        private TimeZoneInfo sourceTimeZone; 
        private TimeZoneInfo destinationTimeZone; 
     
        public CurrentDateHandlerFactory(TimeZoneInfo sourceTimeZone,  
            TimeZoneInfo destinationTimeZone) 
        { 
             this.sourceTimeZone = sourceTimeZone; 
             this.destinationTimeZone = destinationTimeZone; 
        } 
     
        public DateTime GetCurrentDate() 
        { 
            return TimeZoneInfo.ConvertTime(DateTime.Now, this.sourceTimeZone,  
                this.destinationTime); 
        } 
    }
    

    De même AgeCalculator prend en compte la Factory toutefois son implémentation ne contient aucune modifications relatives au fuseau horaire:

    public class AgeCalculator 
    { 
        private IDateHandlerFactory dateHandlerFactory; 
     
        public AgeCalculator(IDateHandlerFactory dateHandlerFactory)  
        { 
            this.dateHandlerFactory = dateHandlerFactory; 
        } 
     
        public int GetAge(DateTime dateOfBirth) 
        { 
            var currentDateHandler = this.dateHandlerFactory.GetCurrentDateHandler(); 
     
            DateTime now = currentDateHandler.GetCurrentDate(); 
            if (dateOfBirth > now) 
                throw new InvalidOperationException(
                    "Date of birth shall be before current date."); 
         
            return now.Year - dateOfBirth.Year; 
        } 
    }
    

    On peut toujours mocker ICurrentDateHandler ainsi que la Factory IDateHandlerFactory pour maitriser la date actuelle dans le test:

    [Test]   
    public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()   
    {   
        DateTime expectedCurrentDate = new DateTime(2016, 6, 6);  
      
        Mock<ICurrentDateHandler> currentDateHandlerMock = new Mock<ICurrentDateHandler>();  
        currentDateHandlerMock.SetUp(h => h.GetCurrentDate()).Returns(expectedCurrentDate);  
     
        Mock<IDateHandlerFactory> dateHandlerFactoryMock = new Mock<IDateHandlerFactory>();  
        dateHandlerFactoryMock.SetUp(f => f.GetCurrentDateHandler()) 
            .Returns(currentDateHandlerMock.Object); 
      
        DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,   
            expectedCurrentDate.Month, expectedCurrentDate.Day);   
       
        var ageCalculator = new AgeCalculator(dateHandlerFactoryMock.Object);   
        int age = ageCalculator.GetAge(dateOfBirth);   
       
        Assert.AreEqual(14, age);   
    }
    

    Utiliser une classe statique Proxy

    L’inconvénient majeur des méthodes précédentes est qu’elles imposent de modifier en profondeur la classe AgeCalculator car:

    • Il faut modifier le constructeur pour permettre l’injection de la dépendance,
    • Il faut modifier la fonction AgeCalculator.GetAge() pour qu’elle utilise ICurrentDateHandler.GetCurrentDate() plutôt que DateTime.Now.

    On pourrait explorer d’autres méthodes pour injecter la dépendance:

    • Injecter la dépendance par appel d’une méthode pour éviter la modification du constructeur, par exemple SetCurrentDateHandler():
      public class AgeCalculator 
      { 
          private ICurrentDateHandler currentDateHandler; 
       
          public AgeCalculator() {} 
       
          public void SetCurrentDateHandler(ICurrentDateHandler currentDateHandler) 
          { 
              this.currentDateHandler = currentDateHandler; 
          } 
       
          // ... 
      }
      
    • Injection par un accesseur:
      public class AgeCalculator 
      { 
          public AgeCalculator() {} 
       
          public ICurrentDateHandler CurrentDateHandler { get; set; }  
       
          public int GetAge(DateTime dateOfBirth) 
          { 
              DateTime now = this.CurrentDateHandler.GetCurrentDate(); 
              if (dateOfBirth > now) 
                  throw new InvalidOperationException(
                      "Date of birth shall be before current date."); 
           
              return now.Year - dateOfBirth.Year; 
          } 
      }
      
    • Injecter directement dans la fonction AgeCalculator.GetAge():
      public class AgeCalculator 
      { 
          public AgeCalculator() {} 
       
          public int GetAge(ICurrentDateHandler currentDateHandler, DateTime dateOfBirth) 
          { 
              DateTime now = currentDateHandler.GetCurrentDate(); 
              if (dateOfBirth > now) 
                  throw new InvalidOperationException(
                      "Date of birth shall be before current date."); 
           
              return now.Year - dateOfBirth.Year; 
          } 
      }
      

    Ces solutions sont très mauvaises car elles imposent que l’appelant de AgeCalculator.GetAge() crée une instance d’un objet ICurrentDateHandler et qu’il l’injecte au bon moment dans AgeCalculator. On va donc explorer une autre possibilité qui consiste à continuer à utiliser une classe statique dans la fonction AgeCalculator.GetAge().

    L’intérêt de cette approche est:

    • Elle ne modifie pas le constructeur de AgeCalculator,
    • Elle ne nécessite pas une injection de ICurrentDateHandler,
    • La modification n’implique que la fonction AgeCalculator.GetAge().

    Ainsi si on considère la classe suivante:

    public static class DateHandler 
    { 
        public static Func<DateTime> Now = () => DateTime.Now; 
    }
    

    On peut l’utiliser directement dans AgeCalculator sans injection:

    public class AgeCalculator 
    { 
        public AgeCalculator() {} 
     
        public int GetAge(DateTime dateOfBirth) 
        { 
            DateTime now = DateHandler.Now; 
            if (dateOfBirth > now) 
                throw new InvalidOperationException(
                    "Date of birth shall be before current date."); 
         
            return now.Year - dateOfBirth.Year; 
        } 
    }
    

    On peut toujours maitriser la date actuelle dans les tests. Notre test devient:

    [Test]  
    public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()  
    {  
        DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
        DateHandler.Now = () => expectedCurrentDate; 
     
        DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,  
            expectedCurrentDate.Month, expectedCurrentDate.Day);  
      
        var ageCalculator = new AgeCalculator();  
        int age = ageCalculator.GetAge(dateOfBirth);  
      
        Assert.AreEqual(14, age);  
    }
    

    Le constructeur de AgeCalculator et la signature de AgeCalculator.GetAge() ne sont pas modifiés.

    Smocks

    Une dernière possibilité consiste à ne pas modifier du tout la classe AgeCalculator et continuer à utiliser la propriété statique DateTime.Now. On peut contrôler la valeur de DateTime.Now dans les tests en utilisant des bibliothèques comme Smocks.

    Le gros intérêt de ce type de bibliothèque est de modifier directement les valeurs renvoyées par une propriété ou des fonctions statiques, par exemple pour le test précédent:

    [Test]  
    public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()  
    {  
        Smock.Run(context => 
        { 
            DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
     
            context.Setup(() => DateTime.Now).Returns(expectedCurrentDate); 
     
           DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,  
                expectedCurrentDate.Month, expectedCurrentDate.Day);  
      
            var ageCalculator = new AgeCalculator();  
            int age = ageCalculator.GetAge(dateOfBirth);  
      
            Assert.AreEqual(14, age); 
         }); 
    }
    

    Le code de AgeCalculator n’est pas modifié:

    public class AgeCalculator 
    { 
        public AgeCalculator() {} 
     
        public int GetAge(ICurrentDateHandler currentDateHandler, DateTime dateOfBirth) 
        { 
            DateTime now = DateTime.Now; 
            if (dateOfBirth > now) 
                throw new InvalidOperationException(
                    "Date of birth shall be before current date."); 
         
            return now.Year - dateOfBirth.Year; 
        } 
    }
    

    Smocks est disponible sur Nuget: https://www.nuget.org/packages/Smocks/.

    Microsoft Fakes

    Une autre alternative est d’utiliser Microsoft Fakes.
    L’inconvénient de Microsoft Fakes est qu’il n’est disponible que dans Visual Studio Enterprise ce qui en fait un produit cher.
    Le projet d’origine Moles était gratuit toutefois depuis Visual Studio 2010, il n’a pas évolué au profit de Microsoft Fakes.

    Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

    Télécharger le contenu d’un répertoire listé par Apache

    Pour télécharger le contenu d’un répertoire sur un serveur Apache, on peut s’aider d’un outil provenant d’Unix: wget. Les gros intérêts de cet outil sont:

    • Son installation est rapide,
    • On peut l’utiliser à partir d’une ligne de commandes, dans un script batch ou dans un script powershell,
    • wget dipose de beaucoup d’options, par exemple, pour parcourir récursivement un répertoire HTTP

    A l’origine wget est un outil Unix mais il existe une implémentation pour Windows téléchargeable sur: http://gnuwin32.sourceforge.net/packages/wget.htm.

    Pour télécharger un répertoire HTTP récursivement

    Par exemple, si le répertoire est http://mysite.org/internal_directory/subfolder/, on peut écrire à la ligne de commandes:

    wget -r -np http://mysite.org/internal_directory/subfolder/

    Les options sont:

    • -r: permet de parcourir récursivement tous les répertoires, y compris les répertoires situés au-dessus du répertoire subfolder comme, par exemple, internal_directory.
    • -np: permet d’empêcher le parcours des répertoires au-dessus du répertoire subfolder. Le parcours sera donc limité au répertoire subfolder et à ses sous-répertoires.

    D’autres options peuvent être utiles comme:

    • -nH: permet de ne pas préfixer le répertoire de destination avec le host name du site http://mysite.org/
    • -cut-dirs: cette option permet d’éviter de ranger le contenu des répertoires dans les mêmes répertoires de destination. Ainsi --cut-dirs=1 rangera le contenu de http://mysite.org/internal_directory/subfolder/ dans mysite.org/subfolder/. Par suite, --cut-dirs=2 rangera le contenu dans mysite.org/.
    • -R: permet d’excluse des fichiers suivant leur nom. -R index.html évitera de récupérer les fichiers index.html; -R html permettra d’éviter de récupérer tous les fichiers html.
    • -A: permet de récupérer seulement les fichiers avec un nom particulier. -A html permet de ne récupérer que les fichiers html.

    Toute la documentation se trouve sur: http://www.gnu.org/software/wget/manual/wget.html.

    VisualWget

    Sur Windows, on peut s’aider d’une interface graphique pour utiliser wget: VisualWget.

    Références
    Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page