Utilisation des fichiers XSD en 5 min

Les fichiers XML Schema Definition (XSD) permettent de décrire la structure d’un document XML. Le grand intérêt de ce fichier est de servir à la validation du document XML en définisant des règles.

Génération automatique d’un fichier XSD à partir d’un fichier XML

Avec Xsd.exe

Cet outil fait partie du SDK Visual Studio. Il est accessible en utilisant une ligne de commandes dans le répertoire du SDK, par exemple:

C:\Program Files\Microsoft Visual Studio 9.0\SDK\v2.0\Bin

Pour générer le fichier XSD, il suffit d’écrire:

xsd [chemin du fichier XML] /outputdir:[répertoire où générer le fichier XSD]

Par exemple:

xsd fichier.xml /outputdir:"C:\MonRepertoire"

Par programmation

Le code suivant permet de générer un fichier XSD par programmation:

using System.Xml;
using System.Xml.Schema;
...

public void WriteXsd(string xmlFilePath)
{
    XmlReader reader = XmlReader.Create(xmlFilePath);
    XmlSchemaInference schema = new XmlSchemaInference();
    XmlSchemaSet schemaSet = schema.InferSchema(reader);
    
    foreach (XmlSchema s in schemaSet.Schemas())
    {
        using (var stringWriter = new StringWriter())
        {
            using (var writer = XmlWriter.Create(stringWriter))
            {
                s.Write(writer);
            }
    
            textbox.text = stringWriter.ToString();
        }
    }
}

Validation d’un fichier XML

Par programmation

Avec XmlSchemaSet

L’avantage de cette méthode est d’être utilisable à partir du Framework 2.0.
Un fichier XML peut être lu et validé de cette façon:

using System;
using System.Xml;
using System.Xml.Schema;

...

public void ValidateXmlFile(string schemaNamespace, string xsdFilePath, string xmlFilePath)
{
  XmlReaderSettings settings = new XmlReaderSettings();
  settings.Schemas.Add(schemaNamespace, xsdFilePath);
  settings.ValidationType = ValidationType.Schema;
  settings.ValidationEventHandler += new ValidationEventHandler(
     validationCallBack);
  XmlReader readItems = XmlReader.Create(xmlFilePath, settings);
  while (readItems.Read()) { }
}

private void validationCallBack(object sender, ValidationEventArgs e)
{
  if (e.Severity.Equals(XmlSeverityType.Warning))
  {
    Console.Write("WARNING: ");
    Console.WriteLine(e.Message);
  }
  else if (e.Severity.Equals(XmlSeverityType.Error))
  {
    Console.Write("ERROR: ");
    Console.WriteLine(e.Message);
  }
}

Ce code permet de valider un fichier XML suivant le schéma défini dans un fichier XSD. La validation s’effectue dans le namespace indiqué par le paramètre “schemaNamespace”.
Plus de détails sur MSDN.

Il est possible d’indiquer des critères suivant lesquels la validation sera effectuée.
Par exemple:

XmlReaderSettings settings = new XmlReaderSettings();
settings.ValidationType = ValidationType.Schema;
settings.ValidationFlags |= XmlSchemaValidationFlags.ProcessInlineSchema;
settings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings;
settings.ValidationEventHandler += new ValidationEventHandler (ValidationCallBack);

XmlSchemaValidationFlags.ProcessInlineSchema indique la validation traite des schémas inline trouvés. Les schémas inline étant des définitions inclues directement à l’intérieur de l’instance du fichier XML.
XmlSchemaValidationFlags.ReportValidationWarnings permet de signaler les avertissements de validation.
D’autres critères sont possibles pour l’enum XmlSchemaValidationFlags.

Plus de détails à propos de XmlReaderSettings.ValidationFlags sur MSDN.

Avec XDocument (LinQ to XML)

Cette méthode est possible à partir du Framework 3.5:

using System;
using System.Xml;
using System.Xml.Linq;

...

public void ValidateXmlFile(string schemaNamespace, string xsdFilePath, string xmlFilePath)
{
  XmlSchemaSet schemas = new XmlSchemaSet();
  schemas.Add(schemaNamespace, xsdFilePath);
  
  XDocument doc = XDocument.Load(xmlFilePath);
  string validationFeedbackMessage = string.Empty;
  doc.Validate(schemas, (objectSender, validationEventArgs) => {
      validationFeedbackMessage += validationEventArgs.Message + Environment.NewLine;
  });
  
  Console.WriteLine(msg == "" ? "Document is valid" : "Document invalid: " + validationFeedbackMessage);
}

Plus de détails à propos de ce type de validation sur MSDN.

A la compilation dans Visual Studio

Il est possible d’effectuer la validation d’un fichier XML à la compilation dans Visual Studio. Cette validation peut être pratique, par exemple, après avoir édité un fichier de configuration.
La validation s’effectue aussi lorsqu’on édite le fichier.

Cette fonctionnalité est disponible à partir de Visual Studio 2008. Pour l’utiliser, il faut:
1. Ajouter le fichier XML au projet
2. Ajouter le fichier XSD au projet
3. Faire un clique droit sur le fichier XML puis "Properties".
4. Cliquer sur les "…" du paramètre "Schemas".
5. Cliquer dans la colonne "Use" pour les lignes correspondant aux fichiers XSD à utiliser.

Attention aux "namespaces"

Voir plus bas pour plus de détails

Explication sur les "namespaces"

Les fichiers de schéma XSD utilisent des espaces de noms (i.e. "namespaces") pour distinguer les éléments appartenant au langage XSD et les éléments et attributs définis pour un schéma donné.
Dans l’en-tête d’un fichier XSD, on précise:
– le namespace des éléments appartenant au langage XSD
– si besoin, le namespace des éléments et attributs que l’on s’apprête à définir.

Namespace des éléments appartenant au langage XSD

On définit le namespace des éléments appartenant au langage XSD, on indique dans le fichier XSD:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  ...
</xs:schema>

xmlns signifie "xml namespace". xs est le préfixe utilisé pour éléments XSD.
Parfois on peut utiliser le préfixe xsd, dans ce cas le schéma sera défini par:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  ...
</xsd:schema>

Définir le namespace des éléments du schéma

On peut indiquer un namespace pour les éléments que l’on va définir dans le fichier XSD. Cette indication est facultative mais elle permet d’éviter les confusions. Ainsi on rajoute les attributs:
targetNamespace="http://yourdomain.org/namespace/" xmlns:this="http://yourdomain.org/namespace/".
targetNamespace est le namespace et xmlns:this précise le préfixe utilisé (qui sera “this”).
Si on définit le fichier XSD suivant:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://yourdomain.com/namespace/"
    xmlns:this="http://yourdomain.com/namespace/">
    ...
</xs:schema>

Alors pour faire référence à des éléments définis, il faut utiliser le préfixe "this". Par exemple, si on définit l’élément "Row" de cette façon:

<xs:complexType name="Row">
    <xs:sequence>
        <xs:element name="Name" type="xs:string" minOccurs="1" maxOccurs="1" />
        <xs:element name="Value" type="xs:float" minOccurs="1" maxOccurs="1" />
    </xs:sequence>
</xs:complexType>

Pour faire référence à cet élément dans le reste du fichier XSD, il faut utiliser le préfixe:

<xs:element name="Rows" type="this:Row" minOccurs="0" maxOccurs="unbounded" />

Ce préfixe est utilisé au niveau du fichier XSD. On n’est pas obligé d’utiliser le même préfixe dans le fichier XML car il est redéfinit dans l’en-tête du fichier XML.

L’utilisation d’un préfixe n’est pas obligatoire

Par exemple, dans le cas précédent si on supprime le préfixe, le fichier XSD sera:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://yourdomain.com/namespace/"
    xmlns="http://yourdomain.com/namespace/">
    ...
</xs:schema>

elementFormDefault

On utilise cet attribut pour indiquer si les éléments du namespace doivent être qualifiés ou non en utlisant le préfixe du namespace dans le fichier XML. Les 2 valeurs possibles sont: "qualified" et "unqualified". Par exemple, si on considère les éléments suivants définis dans un fichier XSD:

<xs:complexType name="AuthorType">
  <xs:sequence>
     <xs:element name="name" type="xs:string"/>
     <xs:element name="phone" type="xs:string"/>
  </xs:sequence>
</xs:complexType>
<xs:element name="author" type="this:AuthorType"/>

elementFormDefault="qualified"

Les éléments définis doivent être préfixés. Donc dans le fichier XML, on devra préfixer tous les éléments de l’instance (par forcément avec le même préfixe que dans le fichier XSD puisqu’on redéfinit le préfixe):

<x:author xmlns:x="http://example.org/publishing">
    <x:name>Aaron Skonnard</name>
    <x:phone>(801)390-4552</phone>
</x:author>

elementFormDefault="unqualified"

Il n’est pas nécessaire de préfixer tous les éléments de l’instance:

<x:author xmlns:x="http://example.org/publishing">
    <name>Aaron Skonnard</name>
    <phone>(801)390-4552</phone>
</x:author>

Généralement, on impose la présence du préfixe en utilisant la valeur "qualified" dans le fichier XSD:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://yourdomain.com/namespace/"
    xmlns:this="http://yourdomain.com/namespace/"
    elementFormDefault="qualified">
    ...
</xs:schema>

Association du fichier XML avec le fichier XSD

Un fichier XML dont les éléments sont définis dans un fichier XSD est considéré comme un document instance. Dans le fichier XML, on utilise l’attribut xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" pour désigner cette instance.
On utilise ensuite l’attribut xsi pour indiquer le fichier XSD définissant les éléments du fichier XML.

Si on n’utilise pas de namespaces

Rien n’oblige à utiliser des namespaces. Dans ce cas, dans le fichier XML on utilise l’attribut xsi:noNamespaceSchemaLocation.

Par exemple, si on considère le fichier XSD:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema 
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    elementFormDefault="qualified">
    <xs:element name="Rows" type="Row" minOccurs="0" maxOccurs="unbounded" />
    ...
</xs:schema>

Le fichier XML pourrait être:

<?xml version="1.0" encoding="UTF-8" ?>
<Rows 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="schemaFile.xsd">
    ...
</Rows>

Si on utilise des namespaces

Dans le fichier XML, en plus de l’attribut xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" qui permet de préciser l’instance, il faut utiliser:
xsi:schemaLocation pour indiquer le fichier XSD
– redéfinir le ou les namespaces utilisés avec, par exemple, un attribut: xmlns:this="http://yourdomain.com/namespace/" (on est pas obligé d’utiliser le même préfixe que pour le fichier XSD).

Par exemple si on définit le fichier XSD de la façon suivante:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema 
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://yourdomain.com/namespace/"
    xmlns:this="http://yourdomain.com/namespace/"
    elementFormDefault="qualified">
   ...
</xs:schema>

Le fichier XML correspondant pourrait être:

<?xml version="1.0" encoding="UTF-8" ?>
<Rows 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://yourdomain.com/namespace/ schemaFile.xsd"
    xmlns:this="http://yourdomain.com/namespace/">
    ...
</Rows>

Le préfixe "this" n’est pas obligatoire et n’est pas forcément le même que celui du fichier XSD.

Les "dumps" mémoire en 5 min

Lorsqu’un bug se produit en production, il n’est pas toujours facile de reproduire le problème sur une plateforme de dévelopement pour le corriger ensuite. Sur certaines applications, il peut être récurrent d’échouer à trouver le scénario exact qui permet de révéler le bug. Si ce problème se présente régulièrement, une possibilité est de générer un "dump" mémoire en cas de crash de façon à pouvoir l’analyser par la suite et identifier de façon plus précise l’origine du bug.

1. Qu’est ce qu’un "dump" ?

Un "dump" mémoire d’un processus correspond à une copie du contenu de la mémoire virtuelle (pile, tas managé, pile d’appels des différents "threads" etc…). Un débogueur peut écrire le contenu de la mémoire virtuelle dans un fichier sur le disque de façon à pouvoir le lire plus tard. Avec les sources, on pourra ensuite lire le "dump" et voir une instance “gelée” du processus de façon à identifier plus précisemment la ligne de code qui a menée au crash.

A. Contenu d’un "dump"

Un "dump" peut contenir:
La pile en mémoire: contient les objets et variables créés par un processus.
Pile d’exécution (i.e. "callstack") de tous les "threads": on peut savoir précisemment les fonctions qui étaient exécutées au moment du "dump".
Blocs de l’environnement des "threads": contient des informations sur les "threads" en cours d’exécution de façon à en connaître l’état et le thread ID.
Code assembleur: dans le pire des cas, on peut avoir à lire le code assembleur. Cette solution est généralement trop fastidieuse et trop couteuse. Toutefois en rapprochant le "dump" des fichiers de symboles “PDB”, on peut avoir les piles d’appels par rapport au code source, ce qui est plus facile pour déboguer.
Information sur les modules: le processus charge souvent plusieurs assemblies. Le "dump" permet d’avoir des informations sur les dépendances qui ont été chargées notamment la version des assemblies.

B. Différents types de "dumps"

Il existe des types différents de "dumps" suivant les informations qu’il contient:
Full dump: les "full memory dumps" contiennent tout le contenu de la mémoire virtuelle. Ce type de "dump" est particulièrement utile lorsqu’on a aucune idée de l’origine du problème. L’inconvénient majeur de ce "dump" est qu’il faut du temps pour le collecter. Si le serveur à partir duquel on récupère le "dump" est saturé, la collecte pourrait encore ralentir l’exécution des processus.
Mini dump: ce type de "dump" concerne un processus spécifique et est configurable de façon à choisir les informations qu’il contiendra.

Le terme "mini dump" prête à confusion

"Mini dump" laisse penser que les "dumps" de ce type sont moins volumineux que les "full dumps". Un "mini dump" peut être plus volumineux et plus complet qu’un "full dump" car il donne la possibilité de choisir les éléments qui seront y stockés. Il est notamment de choisir des éléments plus volumineux que pour le “full dump”.

2. Capturer un "dump"

On peut capturer un "dump" de toute la mémoire ou d’un processus à un moment déterminé. Sachant qu’il est difficile de prévoir un crash, il ne sera pas aisé de trouver le moment où il faudra commencer la capture.

Ainsi les outils de capture de "dump" peuvent scruter un processus et effectue un "dump" quand certaines conditions sont remplies:
– si le processus provoque une activité trop élevée du processeur;
– si une fenêtre du processus reste bloquée pendant un certain temps;
– si le processus s’arrête etc…

Différents outils permettent de capturer des "dumps":

A. Gestionnaire de tâches (i.e. "task manager")

Le gestionnaire de tâches permet de capturer des "dumps" à la demande:
1. Ouvrir le gestionnaire de tâches: Ctrl + Majuscule + Echap.
2. Trouver le processus pour lequel on veut effectuer le "dump"
3. Clique droit puis sélectionner "Create dump file".
4. Le dump sera écrit dans un répertoire temporaire et le chemin sera indiqué dans une popup.

B. Visual Studio

A partir de Visual Studio 2010, on peut capturer un "dump" quand un processus est en cours de débug:
1. A partir du menu “Debug” en cliquant sur “Save Dump As…”.
2. Il est possible de sauvegarder le "dump" avec ou sans pile (“heap”).

Pour plus d’informations: Utiliser les fichiers de dump pour déboguer les pannes et les blocages d’application dans Visual Studio.

C. ProcDump

ProcDump appartient à la suite d’outils Windows Sysinternals.

Plus de détails sur ProcDump: https://technet.microsoft.com/en-us/sysinternals/dd996900.aspx.

Pour capturer un "dump" sans conditions:

procdump -ma [Name or PID]

Pour capturer un "dump" pour n’importe quelle exception (exception de plus bas niveau):

procdump -e 1 -ma [Name or PID]

Dans le cas d’une exception spécifique, dans l’exemple d’une exception de type System.NullReferenceException:

procdump -e 1 -f "System.NullReferenceException" -ma [Name or PID]

Dans le cas où le processus utilise plus de 500 Mo de mémoire:

procdump -m 500 -ma [Name or PID]

On peut déclencher la capture en fonction de la valeur d’un compteur de performances:
L’argument "-p \Process(Name_PID)\[counterName] [threshold]" permet d’indiquer un seuil pour une valeur spécifique du compteur de performance Windows (Windows Performance Counter).

Par exemple pour baser la valeur sur le nombre de "threads" du processus avec un seuil de déclenchement à 85 "threads", le processus ayant pour nom "w3mp" et pour PID "66666":

procdump -p "\Process(w3wp_66666)\Thread Count" 85 -ma 66666

Il est recommandé d’utiliser le nom et le PID pour désigner le processus pour lequel on veut effectuer la capture. Dans le cas où 2 processus ont le même nom, l’utilisation seule du nom pour désigner le processus peut mener à la capture d’un autre processus.

D. API MiniDumpWriteDump

MiniDumpWriteDump est une fonction de la DLL DbgHelp.dll qui fait partie des "Debugging Tools For Windows". Le grand intérêt de cette fonction est de pouvoir être appelée par programmation:

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.IO;

namespace MiniDumpUtility
{
    public static class MiniDump
    {
        public static class MINIDUMP_TYPE
        {
            public const int MiniDumpNormal = 0x00000000;
            public const int MiniDumpWithDataSegs = 0x00000001;
            public const int MiniDumpWithFullMemory = 0x00000002;
            public const int MiniDumpWithHandleData = 0x00000004;
            public const int MiniDumpFilterMemory = 0x00000008;
            public const int MiniDumpScanMemory = 0x00000010;
            public const int MiniDumpWithUnloadedModules = 0x00000020;
            public const int MiniDumpWithIndirectlyReferencedMemory = 0x00000040;
            public const int MiniDumpFilterModulePaths = 0x00000080;
            public const int MiniDumpWithProcessThreadData = 0x00000100;
            public const int MiniDumpWithPrivateReadWriteMemory = 0x00000200;
            public const int MiniDumpWithoutOptionalData = 0x00000400;
            public const int MiniDumpWithFullMemoryInfo = 0x00000800;
            public const int MiniDumpWithThreadInfo = 0x00001000;
            public const int MiniDumpWithCodeSegs = 0x00002000;
        }

        [DllImport("dbghelp.dll")]
        public static extern bool MiniDumpWriteDump(IntPtr hProcess,
            Int32 ProcessId,
            IntPtr hFile,
            int DumpType,
            IntPtr ExceptionParam,
            IntPtr UserStreamParam,
            IntPtr CallackParam);

        public static void CreateMiniDump()
        {
            using(FileStream fs = new FileStream("dump.dmp", FileMode.Create))
            {
                using(System.Diagnostics.Process process = 
                  System.Diagnostics.Process.GetCurrentProcess())
                {
                    MiniDumpWriteDump(process.Handle,
                        process.Id,
                        fs.SafeFileHandle.DangerousGetHandle(),
                        MINIDUMP_TYPE.MiniDumpNormal,
                        IntPtr.Zero,
                        IntPtr.Zero,
                        IntPtr.Zero);
                }
            }
        }
    }
}

On peut appeler MiniDump.CreateMiniDump() dans le cas où des exceptions surviennent. Toutefois il est préférable de lancer la capture du "dump" à partir d’un processus séparé car la création du "dump" peut elle-même causer un crash si la situation est très critique.

Le détail des valeurs de l’enum MINIDUMP_TYPE se trouve sur: MINIDUMP_TYPE enumeration.
Les options les plus intéressantes sont: MiniDumpWithFullMemory, MiniDumpWithFullMemoryInfo, MiniDumpWithUnloadedModules et MiniDumpWithThreadInfo.

Déclenchement de la création du "dump" au moment d’une exception

On peut gérer différent type d’exceptions au travers de clauses try...catch:

try
{
  ...
}
catch (NotImplementedException e)
{
  ...
}
catch (NullReferenceException e)
{
  ...
}

Toutes les exceptions ne sont pas forcément capturées par cette clause try...catch, elles seront remontées dans l’AppDomain courant (AppDomain.CurrentDomain) jusqu’elles soient interceptées par le CLR qui stoppera l’application. Pour capturer un "dump" au moment de ces exceptions, on peut s’abonner à certains évènements:

L’évènement AppDomain.CurrentDomain.FirstChanceException qui est déclenché en première position avant que le runtime ne cherche dans la pile d’appels, du code qui pourrait intercepter l’exception:

AppDomain.CurrentDomain.FirstChanceException += 
	(sender, eventArgs) => MiniDump.CreateMiniDump();

Plus de détails sur AppDomain.CurrentDomain.FirstChanceException sur MSDN.

– Les exceptions non gérées survenant dans l’AppDomain:

AppDomain.CurrentDomain.UnhandledException += 
	(sender, eventArgs) => MiniDump.CreateMiniDump();

Plus de détails sur MSDN.

3. Comment lire un dump ?

A. Avec Visual Studio

A partir de Visual Studio 2010 et à condition que le Windows Driver Kit (WDK) soit installé, il est possible d’ouvrir un fichier de "dump" en faisant:
1. "Open | Crash Dump"
2. Sélectionner le fichier du "dump"
3. Cliquer sur "Open".

Cette solution permet de voir les piles d’appels, le code assembleur qui était en cours d’exécution au moment de la capture et des informations sur le "dump".

Toutefois pour rapprocher la pile d’appels du code source, et pouvoir plus efficacement savoir les lignes du code source qui étaient en cours d’exécution, il faut indiquer le chemin des fichiers de symboles (fichiers ".pdb") liés aux assemblies à déboguer. Il faut impérativement que les versions des ".pdb" soient les mêmes que celles des assemblies. Il n’est pas nécessaire de rajouter les fichiers ".pdb" pour toutes les assemblies, seuls ceux des assemblies à déboguer sont nécessaires.

Pour indiquer le chemin des fichiers de symboles, il faut cliquer sur "Find symbol (.pdb) files" puis rajouter les chemins des répertoires contenant les fichiers ".pdb".

Pour plus d’informations: Specify Symbol (.pdb) and Source Files in the Visual Studio Debugger.

B. Avec WinDbg

WinDbg est un outil très puissant pour obtenir des informations à partir d’un fichier de "dump". Il est difficile à utiliser cependant on arrive à obtenir plus d’informations qu’avec Visual Studio notamment pour les DLL non managés. Parfois, il peut s’avérer plus utile que Visual Studio à condition de connaître quelques commandes.

WinDbg fait partie de la suite d’outils Windows Development Toolkit (SDK). Pour obtenir WinDbg, il suffit de télécharger le SDK: https://msdn.microsoft.com/fr-FR/windows/hardware/dn913721.aspx#windbg-symbols.

Après installation, WinDbg se trouve à partir du menu Windows dans:
Windows Kits => Debugging Tools for Windows (x86 ou x64).
Il faut choisir la version correspondant à l’architecture sur laquelle le processus a été exécutée.

Charger le "dump"

Pour charger le "dump", il faut cliquer sur:
1. Cliquer sur File
2. Puis "Open Crash Dump"

Ajouter les fichiers de symboles

Pour ajouter les fichiers de symboles de façon à voir la pile d’exécution, il faut:
1. Cliquer sur File
2. Puis sur "Symbol File Path"
3. Tous les fichiers ne doivent pas être nécessairement dans le même répertoire. Ils peuvent être dans des répertoires différents dont on indique le chemin séparé par le caractère ";".
Par exemple:

C:\Repertoire1;D:\Repertoire2

4. Il faut rajouter les fichiers de symboles des assemblies du framework:

C:\Windows\Microsoft.NET\Framework\v4.0.30319

5. Il faut ensuite rajouter le chemin permettant d’accéder aux fichiers des assemblies du système. Ces fichiers seront téléchargés automatiquement par WinDbg au besoin. Le chemin correspond au répertoire dans lequel les fichiers seront copiés et l’adresse du serveur à partir duquel les fichiers seront téléchargés:

srv*c:\symbols*http://msdl.microsoft.com/download/symbols
Répertoire C:\symbols

Il faut créer ce répertoire à la main. Si il n’existe pas, le téléchargement des fichiers va échouer. Même si on indique un autre répertoire, il faut que "C:\symbols" soit créé et qu’il soit accessible en écriture.

Pour résumer, dans le cas notre exemple, les chemins seront:

C:\Repertoire1;D:\Repertoire2;srv*c:\symbols*http://msdl.microsoft.com/download/symbols;C:\Windows\Microsoft.NET\Framework\v4.0.30319

Charger les fichiers de symboles

Généralement c’est durant cette étape qu’on peut voir s’il manque des fichiers de symboles, si leur version n’est pas correcte ou si une autre erreur liée au chargement, se produit.

Pour augmenter le niveau de log pour voir tous les feedbacks liés au chargement des "symbols":

!sym noisy

Pour revenir à un niveau de log normal:

!sym quiet

Pour charger ou recharger les fichiers de symboles:

.reload /i /f [nom de l'assembly]

Commandes principales

Quelques commandes utiles pour explorer le "dump".

S’il y a plusieurs "threads", on peut voir la pile d’exécution pour tous les "threads" en tapant:

∼*k

Pour sélectionner un "thread" particulier:

∼[numéro du thread]s

Pour voir la pile d’exécution du "thread" sélectionné:

kb

Pour effacer l’écran:

.cls

Pour effectuer une analyse de l’exception:

!analyze -v -f

Avoir des informations sur les assemblies connexes

Pour choisir les bons fichiers de symboles, il peut être utile de vérifier les informations sur les assemblies chargés dans le processus au moment du dump.

Ainsi pour avoir des infos générales:

!peb

Pour avoir les versions des assemblies:

lm -v

Pour avoir le détail de toutes les commandes Windbg:
windbg.info/doc/1-common-cmds.html

Utiliser Sosex

Sosex est une extension à WinDbg qui permet d’obtenir, en particulier, des piles d’exécution plus complètes. Il est possible de télécharger cette extension suivant l’architecture sur: http://www.stevestechspot.com.

Dans le fichier readme.txt, on peut voir les commandes spécifiques à Sosex.

Pour charger Sosex:

.load [chemin de la DLL]

Pour voir la pile d’exécution du thread sélectionné avec Sosex:

!mk

Pour voir les piles d’exécution pour tous les threads:

∼*e!mk

"ConcurrentDictionary" en 5 min

A partir du framework 4.0, la structure de données "ConcurrentDictionary" permet de stocker des objets rangés par clé tout en autorisant des accès provenant de "threads" multiples sans se préoccuper des problématiques de synchronisation.

Son utilisation est très similaire à celle du dictionaire mise à part qu’elle possède des méthodes pour guider l’implémentation et s’affranchir des problématiques d’accès concurrent.

Le "ConcurrentDictionary" permet d’échapper à la majorité des erreurs lors de l’implémentation de mécanismes de synchronisation, toutefois il est nécessaire d’observer quelques précautions pour conserver un niveau de performance comparable à celui d’une structure de données non "thread-safe".

Le ConcurrentDictionary<TKey, TValue> se trouve dans le namespace System.Collections.Concurrent: https://msdn.microsoft.com/fr-fr/library/dd287191%28v=vs.110%29.aspx.

A partir du framework 4.6

A partir du framework 4.6, ConcurrentDictionary<TKey, TValue> satisfait les interfaces IReadOnlyCollection<KeyValuePair<TKey, TValue>> et IReadOnlyDictionary<TKey, TValue>. Il peut être très intéressant d’utiliser ces interfaces dans des signatures de fonctions pour imposer l’utilisation du "ConcurrentDictionary" en lecture seule, sachant que les accès en lecture d’une "ConcurrentDictionary" sont bien plus performants que les accès en écriture et sont sans "lock".

Le "Dictionary<TKey, TValue>" autorise les accès concurrents

Dans certaines conditions, il n’est pas toujours nécessaire d’utiliser une structure entièrement "thread-safe".
Comme indiqué sur MSDN:

"A Dictionary<TKey, TValue> can support multiple readers concurrently, 
  as long as the collection is not modified."

Ainsi le Dictionary<TKey, TValue> classique autorise les accès multiples et concurents en lecture. Dans ce cas, il n’est pas nécessaire d’utiliser une structure plus complexe ou d’implémenter un mécanisme de synchronisation.

En revanche, les accès en écriture et les énumérations doivent être protégées contre les accès concurrents.
Le gros intérêt du dictionaire par rapport à une autre structure est la performance puisque l’accès en lecture d’un dictionaire est très rapide (pour plus de détails: Is It Faster to Preallocate Dictionary Sizes?).

Pour la lecture, il est préférable d’utiliser TryGetValue():

var simpleDictionary = new Dictionary<int, string>
{
  { 1, "first value" },
  { 2, "secund value" },
  { 3, "third value" }
};

string secundValue;
if (simpleDictionary.TryGetValue(2, out secundValue))
{
  ...
}

Plutôt que:

if (simpleDictionary.ContainsKey(2))
{
  string secundValue = simpleDictionary[2];
}

L’implémentation avec ContainsKey() peut mener à des erreurs si la valeur correspondant à la clé "2" est supprimée entre l’appel à ContainsKey() et simpleDictionary[2].

Pour protéger les accès en écriture et les énumérations, on peut utiliser un "lock" classique:

private readonly object dictionaryLock = new object();

private void AddToDictionary(int key, string newValue)
{
  lock (this.dictionaryLock)
  {
    this.simpleDictionary[key] = newValue;
  }
}

private void EnumerateDictionary()
{
  lock (this.dictionaryLock)
  {
    foreach (var kvp in this.simpleDictionary)
    {
      ...
    }
  }
}

Le "ConcurrentDictionary" ne protège pas contre des accès concurrents à une même valeur

Le "ConcurrentDictionary" fournit une solution pour accéder aux valeurs de façon concurrente. Il n’y a pas de précautions particulières à prendre pour effectuer une énumération ou une écriture dans la structure. En revanche, les objets correspondant aux valeurs du dictionaire ne sont pas protégés. Il faut donc prévoir des mécanismes de synchronisation si plusieurs "threads" sont susceptibles de modifier les propriétés des valeurs du dictionaire en même temps.

Par exemple, si on considère:

var values = new ConcurrentDictionary<int, List<string>>
{
  { 1, new List<string>{ "first", "secund", "third", "fourth", "fifth" }},
};

Si on exécute le code:

Task t1 = Task.Run(() => {
  for (int i = 0; i < 100; i++)
  {
    values[1].Add(i.ToString());
  }
});

Task t2 = Task.Run(() => {
  foreach (var value in values[1])
  {
    Console.WriteLine(value);
  }
});

On aura une erreur du type "Collection was modified; enumeration operation may not execute" car la liste est énumérée au moment où on y ajoute des éléments. Le "ConcurrentDictionary" n’a donc pas protégé la valeur des accès concurrents.

Utiliser des "struct"

Si les valeurs du "ConcurrentDictionary" doivent être utilisées de façon concurrente, une solution peut être d’utiliser des objets de type "struct". Sachant que les "struct" sont des types par valeur, chaque utilisation d’une "struct" sera une valeur copiée à partir de sa valeur d’origine. Les "threads" accédant à cette valeur en auront une copie spécifique et il ne sera plus nécessaire d’implémenter un mécanisme de synchronisation.

Par exemple, si on reprends l’exemple précédent:
On déclare la "struct":

public struct StoredList
{
  public List<string> Values = new List<string>();
  
  public Point(IEnumerable<string> newValues) 
  {
    this.Values.AddRange(newValues);
  }
}

Le dictionaire devient:

var firstValueList = new List<string>{ "first", "secund", "third", "fourth", "fifth" };
var values = new ConcurrentDictionary<int, StoredList>
{
  { 1, new StoredList(firstValueList) },
};

On peut ensuite utiliser la valeur en faisant:

Task t1 = Task.Run(() => {
  var storedList = values[1]; // On obtient une copie de la valeur stockée dans le dictionaire
  for (int i = 0; i < 100; i++)
  {
    storedList.Values.Add(i.ToString());
  }
  values[1] = storedList; // On recopie la valeur modifiée
});

Task t2 = Task.Run(() => {
  // La liste énumérée provient d'une copie de la valeur du dictionaire.
  foreach (var value in values[1].Values) 
  {
    Console.WriteLine(value);
  }
});

Lecture et écriture des valeurs d’un "ConcurrentDictionary"

On peut lire et écrire des valeurs dans un "ConcurrentDictionary" comme pour un dictionaire classique:

var values = new ConcurrentDictionary<int, string>();

Ecriture avec:

values[1] = "new value";

Lecture avec:

string newValue = values[1];

D’autres méthodes permettent une implémentation plus flexible:

GetOrAdd(TKey, Func<TKey, TValue>)

Permet de lire une valeur si la clé existe dans le ConcurrentDictionary ou d’ajouter une pair clé/valeur si elle n’existe pas.

La surcharge ConcurrentDictionary<TKey, TValue>.GetOrAdd(TKey, Func<TKey, TValue>) permet d’implémenter une logique dans le cas où la valeur doit être ajoutée en exécutant l’expression lambda Func<TKey, TValue>.

Par exemple:

public TValue GetOrAddValueWithSimpleLock<TKey, TValue>(
  ConcurrentDictionary<TKey, TValue> dictionary, TKey key)
{
  return dictionary.GetOrAdd(key, (k) => CreateValue<TKey, TValue>(k));
}

public TValue CreateValue<TKey, TValue>(TKey key)
{
  ...
}
Quelques remarques importantes:

1. Si plusieurs "threads" exécutent cette méthode avec la même clé et qu’elle n’existe pas encore dans le ConcurrentDictionary, l’expression lambda Func<TKey, TValue> peut être exécutée plusieurs fois mais tous les appels n’aboutiront pas à l’ajout de la pair clé/valeur.
Ainsi, seul le premier "thread" exécutant l’expression lambda ajoutera effectivement la pair clé/valeur, les "threads" suivant ne feront que récupérer la valeur. Toutefois, suivant la simultanéïté des exécutions l’expression lambda peut avoir été exécutée plusieurs fois.
Plus de détails sur MSDN.

2. L’ajout de valeurs dans le ConcurrentDictionary est plus lent par rapport l’utilisation d’un Dictionary<TKey, TValue> avec un "lock" simple. Les performances du ConcurrentDictionary<TKey, TValue> sont toutefois bonnes pour les accès en lecture.

Il faut tester pour comparer les différences entre "ConcurrentDictionary" et "Dictionary + Lock"

Il convient de tester les performances du ConcurrentDictionary<TKey, TValue> par rapport au Dictionary<TKey, TValue> + "lock" car la différence peut être déterminante.

En effet dans le cas où on utilise ConcurrentDictionary<TKey, TValue>.GetOrAdd(TKey, Func<TKey, TValue>) et si plusieurs "threads" exécutent cette méthode simultanément et que la clé n’existe pas déjà, l’expression lambda sera exécutée simultanément et un thread ne sera pas bloqué par rapport aux autres. Une seule exécution de l’expression lambda servira réellement à l’ajout de la pair clé/valeur, toutefois, le CPU aura été occupé à exécuter plusieurs fois l’expression lambda pour les autres "threads" pour rien.

Une implémentation judicieuse du "lock" permet d’éviter d’exécuter du code inutilement, par exemple:

public TValue GetOrAddValueWithSimpleLock<TKey, TValue>(
  ConcurrentDictionary<TKey, TValue> dictionary, TKey key)
{
  TValue result;
  lock(dictionary)
  {
    if (!dictionary.TryGetValue(key, out result))
    {
      result = CreateValue<TKey, TValue>(key);
      dictionary.Add(key, result);
    }
  }

  return result;
}

Dans le cas où plusieurs "threads" exécutent cette fonction pour une même clé, les "threads" suivant le premier "thread" seront bloqués lors de l’accès du premier "thread" à la section critique du "lock". Toutefois sachant que le premier "thread" aura rajouté la valeur dans le dictionaire, les threads suivant ne vont pas exécuter inutilement CreateValue(key).

AddOrUpdate(TKey, Func<TKey, TValue>)

Cette méthode permet de rajouter une paire clé/valeur si la clé n’existe pas ou de mettre à jour la valeur si la clé existe dans le "ConcurrentDictionary".

Il existe 2 surcharges:
TValue AddOrUpdate(TKey key, TValue addValue, Func<TKey, TValue, TValue> updateValueFactory): addValue est utilisé pour rajouter la valeur si la clé n’existe pas. L’expression lambda updateValueFactory permet de mettre à jour la valeur si la clé est présente.
TValue AddOrUpdate(TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory): addValueFactory est exécuté pour rajouter la valeur si la clé n’existe pas. updateValueFactory permet de mettre à jour la valeur si la clé est présente.

Par exemple, on peut utiliser la 2e surcharge de la façon suivante:

var simpleDictionary = new Dictionary<int, string>
{
  { 1, "first value" },
  { 2, "secund value" },
  { 3, "third value" }
};

simpleDictionary.AddOrUpdate(2, 
(key) => "new secund value",
(key, currentValue) => 
{
  if (currentValue.Equals("secund value"))
    return "secund value updated";
  else
    return currentValue;
});
Remarques importantes:

Les mêmes remarques concernant GetOrAdd(TKey, Func<TKey, TValue>) s’appliquent à AddOrUpdate (voir plus haut).

Performances

Les performances sont assez différentes suivant les membres et méthodes utilisés:

Membres utilisés Opérations Performant ? Remarques
TryGetValue() Lecture Oui Pas de "lock" pour les accès en lecture, seulement des "memory barriers" sont utilisées.
GetOrAdd(), AddOrUpdate() Ecriture Non Plusieurs objets de "lock" sont utilisés lors des accès en écriture.
Les performances sont différentes entre le framework 4.0 et 4.5.
Un "lock" est assigné à chaque objet suivant sa clé de hachage ("Hash code").
GetEnumerator() Enumeration Oui Pas de "lock" toutefois l’énumération ne correspond pas au contenu du "ConcurrentDictionary" à un moment donné.
Count (et non Count()) Nombre d’objets Non Cette méthode est particulièrement pas performante car elle nécessite l’acquisition de tous les "locks" à la fois.
Utiliser la méthode via LinQ en faisant dictionary.Skip(0).Count() n’utilise pas de "lock".
Keys, Values Obtenir les clés (respectivement les valeurs) Non Fait l’acquisition de tous les "locks" à la fois.Utiliser la méthode LinQ en faisant dictionary.Select(kvp => kvp.Key) (respectivement dictionary.Select(kvp => kvp.Value)) n’utilise pas de "lock".
ToArray() Obtenir un tableau de KeyValuePair Non Tous les "locks" sont acquis.
CopyTo() Copie les KeyValuePair du dictionaire dans un autre
Clear() Supprimer toutes les valeurs

Améliorations des performances à partir du framework 4.5

Lorsque le type de la valeur d’un "ConcurrentDictionary" est un type large (comme System.Guid par exemple), les écritures et les lectures en mémoire par le CLR ne sont pas atomiques. Ainsi, si l’écriture n’est pas complète et si on effectue une lecture en mémoire au même moment, la valeur lue pourrait être un "mélange" entre la nouvelle valeur et l’ancienne valeur.
Pour éviter ces problèmes, le "ConcurrentDictionary" en .NET 4.0, entoure toutes les valeurs dans un objet noeud. Lorsqu’on effectue une mise à jour de la valeur, un nouvel objet noeud est alloué par le "ConcurrentDictionary".
A partir du framework 4.5, le ConcurrentDictionary évite d’allouer un nouvel objet pour les types primitifs simples comme les Int32, byte etc… Pour ces types, les performances sont ainsi améliorées puisqu’il n’y a pas de réallocation en cas de mis à jour.

La 2e amélioration concerne les "locks". En .NET 4.0, le "ConcurrentDictionary" crée 4 fois le nombre de processeurs d’objets "lock". A partir du framework 4.5, ce nombre n’est pas fixe et augmente en fonction du nombre d’éléments dans le dictionaire. Ainsi plus il y a de "locks" et moins il y a de chances que ces "locks" soient acquis lorsqu’on essaie d’accèder à des valeurs de façons concurrentes.

TODO: parler des changements de contexte entre threads.

Pour résumer

1. Les écritures dans le "ConcurrentDictionary" sont lents par rapport à un "Dictionary + lock" mais ils se font en parallèle.
2. Les accès en lecture sont très rapides car non bloquants et sans "lock".
3. L’utilisation du "ConcurrentDictionary" permet d’éviter des erreurs d’implémentation dans les mécanismes de synchronisation entre "threads".
4. Les propriétés Count, Keys, Values et les méthodes ToArray(), CopyTo() et Clear() sont très peu performantes car elle nécessite l’acquisition de tous les "locks".
5. Les performances sont meilleures à partir du framework 4.5.
6. A partir du framework 4.6, "ConcurrentDictionary" satisfait les interfaces "IReadOnlyCollection" et "IReadOnlyDictionary".

Références:

Design pattern: Service Locator

Objectif:

Proposer une implémentation simple de l’inversion de contrôle

Justification

Lorsqu’un objet doit utiliser une compétence implémentée dans un autre objet, la première approche est d’instancier cet autre objet et de l’utiliser au moyen de ces membres publiques.

Par exemple, si on prends la classe suivante:

public class ConsumingObject
{
    private ConsumedObject consumedObject;

    public ConsumingClass()
    {
        this.consumedObject = new ConsumedObject();
    }
}

Cette instanciation aura plusieurs conséquences:
– Une dépendance de l’objet consommateur ConsumingObject vers l’objet consommé ConsumedObject,
– Eventuellement d’autres dépendances peuvent être nécessaires si l’instanciation de l’objet consommé nécessite d’autres objets,
– Implicitement, l’objet consommateur doit gérer la durée de vie de l’objet consommé.

Une approche différente serait de vouloir limiter la dépendance et ainsi réduire le couplage entre l’objet consommateur et l’objet consommé de façon à:
– permettre une meilleure maintenabilité,
– avoir une implémentation plus flexible en permettant d’adapter plus facilement de nouvelles implémentations,
– être plus extensible en permettant d’étendre plus facilement les fonctionnalités d’une classe.

Par exemple:
Si on prends l’exemple d’une application d’achat d’articles implémentée suivant le pattern "Model-Vue-Controleur" (MVC):
– La "Vue" ShowArticlesView permet d’afficher des articles et de voir le stock correspondant,
– Le "Modèle" Article détaille les caractéristiques d’un article,
– Le "Controleur" ArticleController permet d’intérroger la base de données par l’intermédiaire des "Repositories" ArticleRepository et StockRepository pour récupérer respectivement les détails des articles et le stock correspondant.

Voici un exemple de l’implémentation:

public class ShowArticlesView
{
    private ArticleController articleController;

	public ShowArticlesView()
    {
        this.articleController = new ArticleController();
    }

    public IEnumerable<Article> GetArticles()
    { ... }
}

public class ArticleController
{
    private ArticleRepository articleRepository;
    private StockRepository stockRepository;

    public ArticleController()
    {
        this.articleRepository = new ArticleRepository();
        this.stockRepository = new StockRepository();
    }

    public IEnumerable<Article> GetArticlesWithStock()
    { ... }
}

public class ArticleRepository : Repository<ArticleDetail>
{ }

public class StockRepository : Repository<ArticleStock>
{ }

public class Repository<T>
{
    public virtual T Create() { ... }
    public virtual T GetItem(string id) { ... }
    public virtual T UpdateItem(T updatedItem) { ... }
    public virtual bool DeleteItem(string id) { ... }
    public virtual IEnumerable<T> GetItems() { ... }
}

Dans cet exemple, ShowArticlesView est très dépendante de ArticleController qui lui-même est dépendant de StockRepository et ArticleRepository. Si on modifie la signature des constructeurs de ArticleController ou des classes "repository", par exemple en rajoutant un "logger" commun, il faudra modifier l’instanciation dans la ou les classes consommatrices.
Toutes ces dépendances rendent le couplage trop fort. Ce couplage ira en augmenter à mesure que l’application va devenir fonctionnellement plus riche.

Inversion de contrôle

Une possibilité pour réduire le couplage entre les objets est le pattern d’inversion de contrôle ou "Inversion of Control" (i.e. IoC). Ce pattern considère que l’architecture abstraite d’une application caractérise des comportements généraux qui vont former un framework. Ce framework doit rester abstrait et son comportement doit rester général.

Ainsi le sens classique de consommation des dépendances se fait de la classe consommatrice vers la classe consommée. Ainsi la classe consommatrice instancie et "contrôle" la vie des objets qu’elle consomme. "Inversion de contrôle" préconise de casser cette dépendance en laissant le framework instancier et contrôler les objets consommés pour l’objet consommateur. La dépendance sera alors réduite puisque l’objet consommateur ne gère plus l’existence de l’objet consommé.

D’autre part, sachant que c’est le framework dont le comportement est général qui instancie et contrôle les objets consommés pour l’objet consommateur, le flux de contrôle se fait du framework abstrait vers l’objet consommateur qui est spécialisé d’où l’inversion de contrôle.

Service locator

Une des implémentations de "l’inversion de contrôle" est le pattern "Service Locator". Le principe de "Service Locator" est de regrouper au sein d’un unique objet tous les services dont l’application peut avoir besoin. Cet objet unique s’appelle le "Service locator".

Les objets consommateurs vont ainsi appeler le "service locator" pour obtenir les objets qu’ils souhaitent consommer.

Plus précisemment, "Service locator" est une classe statique qui permet de récupérer directement les objets consommés sans se soucier de leur instanciation et de leur durée de vie. Une implémentation simple de cette classe est la suivante:

public class ServiceLocator 
{
    private readonly IDictionary<Type, Func<object>> registeredServices = 
        new Dictionary<Type, Func<object>>();
 
    public ServiceLocator()
    {
        this.registeredServices = new Dictionary<Type, Func<object>>();
    }
 
    public void RegisterService<T>(Func<T> instanciateService)
    {
        this.registeredServices.Add(typeof(T)) = () => instanciateService();
    }

    public T GetRegisteredService<T>()
    {
        return (T)this.registeredServices[typeof(T)];
    }
}

Dans notre exemple, on peut enregistrer les services en faisant:

ServiceLocator serviceLocator = new ServiceLocator();
serviceLocator.RegisterService<ArticleRepository>(() => new ArticleRepository());
serviceLocator.RegisterService<StockRepository>(() => new StockRepository());
serviceLocator.RegisterService<ArticleController>(() => new ArticleController());

On peut consommer les services en faisant:

StockRepository stockRepository = serviceLocator.GetRegisteredService<StockRepository>();

Utilisation d’un "service locator" statique ou sous forme de singleton

On peut transformer ServiceLocator en classe statique ou en "singleton" pour faciliter les appels mais rendra plus difficile les tests.

Par exemple, en tant que singleton, l’implémentation de ServiceLocator sera:

public class ServiceLocator 
{
    private static readonly Lazy<ServiceLocator> instance;
    private readonly IDictionary<Type, Func<object>> registeredServices = 
        new Dictionary<Type, Func<object>>();
 
    private ServiceLocator()
    {
        this.registeredServices = new Dictionary<Type, Func<object>>();
    }
 
    public static ServiceLocator Instance
    {
        get
        {
            return this.instance.Value;
        }
    }

    public void RegisterService<T>(Func<T> instanciateService)
    {
        this.registeredServices.Add(typeof(T)] = () => instanciateService();
    }

    public T GetRegisteredService<T>()
    {
        return (T)this.registeredServices[typeof(T)];
    }
}

L’ajout de service devient plus direct:

ServiceLocator.Instance.RegisterService<ArticleRepository>(() => new ArticleRepository());

De même, pour récupérer un service enregistré:

ServiceLocator.Instance.GetRegisteredService<ArticleRepository>();

En reprenant l’exemple précédent et en utilisant la version "singleton", les objets consommateurs n’assurent plus l’instanciation des objets consommés:

public class ShowArticlesView
{
    private ArticleController articleController;

    public ShowArticlesView()
    {
        this.articleController = ServiceLocator.Instance.GetRegisteredService<ArticleController>();
    }

    public IEnumerable<Article> GetArticles()
    { ... }
}

public class ArticleController
{
    private ArticleRepository articleRepository;
    private StockRepository stockRepository;

    public ArticleController()
    {
        this.articleRepository = ServiceLocator.Instance.GetRegisteredService<ArticleRepository>();
        this.stockRepository = ServiceLocator.Instance.GetRegisteredService<StockRepository>();
    }

    public IEnumerable<Article> GetArticlesWithStock()
    { ... }
}

...

Comme on peut le voir dans l’exemple:
– Les objets consommateurs n’assurent plus l’instanciation des objets consommés,
– L’objet ServiceLocator qui fait office de framework, permet de contrôler les objets consommés par l’objet consommateur,
ServiceLocator assure la durée de vie des objets consommés.

Utilisation d’interfaces

On peut encore découpler davantages les objets en utilisant non pas leur type directement mais des interfaces. On peut adapter ServiceLocator pour qu’il référence le service par interface et non par le type des services.
L’intérêt d’utiliser des interfaces est de ne pas avoir de dépendances entre les objets à la compilation. Il est donc plus facile à l’exécution de choisir quels sont les objets qui vont être utilisés pour une interface donnée.

En utilisant des interfaces, l’implémentation de ServiceLocator change pour l’enregistrement des services:

public class ServiceLocator 
{
    private static readonly Lazy<ServiceLocator> instance;
    private readonly IDictionary<Type, Func<object>> registeredServices = 
        new Dictionary<Type, Func<object>>();
 
    private ServiceLocator()
    {
        this.registeredServices = new Dictionary<Type, Func<object>>();
    }
 
    public static ServiceLocator Instance
    {
        get
        {
            return this.instance.Value;
        }
    }

    public void RegisterService<TInterface, TObject>(Func<TObject> instanciateService)
       where TObject : class
    {
        this.registeredServices.Add(typeof(TInterface)] = () => instanciateService();
    }

    public T GetRegisteredService<T>()
    {
        return (T)this.registeredServices[typeof(T)];
    }
}

L’implémentation des classes change puisqu’elles doivent satisfaire des interfaces.
Par exemple pour ArticleController:

public interface IArticleController
{
    IEnumerable<Article> GetArticlesWithStock();
}

public class ArticleController : IArticleController
{
    ...    
}

On peut enregistrer les services en faisant:

ServiceLocator.Instance.RegisterService<IArticleController, ArticleController>(() => new ArticleRepository());

Pour récupérer un service enregistré:

ServiceLocator.Instance.GetRegisteredService<IArticleController>();
Ne pas utiliser cette implémentation de "Service Locator"

Cette inplémentation de "Service Locator" ne devrait pas être utilisée car:
– L’implémentation n’est pas thread-safe,
– A mesure que la complexité de l’application augmentera, ServiceLocator se transformera en une classe "fourre-tout" où tous les objets consommés seront instanciés.
– L’exemple utilisé est très simple mais dans la "vraie-vie", les liens entre les objets sont plus complexes et les dépendances sont plus nombreuses. Ce type d’implémentation s’avérera très peu robuste pour gérer l’ordre d’instanciation des objets consommés.
– Le pattern "Service Locator" a déjà été implémenté dans plusieurs frameworks d’injection de dépendances comme Unity. Il sera plus efficace d’utiliser ces frameworks plutôt que de réimplémenter ce pattern.

Implémentation de "Service Locator" avec Unity

Unity est un framework d’injection de dépendances. Une implémentation existe pour "Service Locator" même si le framework n’en propose pas une à la base.

En prenant l’exemple précédent, on peut utiliser Unity de la façon suivante:

UnityServiceLocator locator = new UnityServiceLocator(ConfigureUnityContainer());
ServiceLocator.SetLocatorProvider(() => locator);
var articleController = ServiceLocator.Current.GetInstance<IArticleController>();
var articleRepository = ServiceLocator.Current.GetInstance<IArticleRepository>();
var stockRepository = ServiceLocator.Current.GetInstance<IStockRepository>();

Avec:

private static IUnityContainer ConfigureUnityContainer()
{
    UnityContainer container = new UnityContainer();
    container.RegisterType<IArticleRepository, ArticleRepository>(
        new ContainerControlledLifetimeManager());
    container.RegisterType<IStockRepository, StockRepository>(
        new ContainerControlledLifetimeManager());
    container.RegisterType<IArticleController, ArticleController>(
        new ContainerControlledLifetimeManager());
    return container;
}

new ContainerControlledLifetimeManager() permet d’indiquer que la durée de vie du service enregistré est liée à celle du container. Il y aura donc une instance par container.

UnityServiceLocator s’implémente de la façon suivante:

using System;
using System.Collections.Generic;
using Microsoft.Practices.ServiceLocation;

namespace Microsoft.Practices.Unity.ServiceLocatorAdapter
{
    public class UnityServiceLocator : ServiceLocatorImplBase
    {
        private IUnityContainer container;

        public UnityServiceLocator(IUnityContainer container)
        {
            this.container = container;
        }

        /// <summary>
        /// When implemented by inheriting classes, this method will do the actual work of resolving
        /// the requested service instance.
        /// </summary>
        /// <param name="serviceType">Type of instance requested.</param>
        /// <param name="key">Name of registered service you want. May be null.</param>
        /// <returns>
        /// The requested service instance.
        /// </returns>
        protected override object DoGetInstance(Type serviceType, string key)
        {
            return container.Resolve(serviceType, key);
        }

        /// <summary>
        /// When implemented by inheriting classes, this method will do the actual work of
        /// resolving all the requested service instances.
        /// </summary>
        /// <param name="serviceType">Type of service requested.</param>
        /// <returns>
        /// Sequence of service instance objects.
        /// </returns>
        protected override IEnumerable<object> DoGetAllInstances(Type serviceType)
        {
            return container.ResolveAll(serviceType);
        }
    }
}
Détails de l’instanciation du "Service Locator"

Il faut faire attention à l’instanciation de:

UnityServiceLocator locator = new UnityServiceLocator(ConfigureUnityContainer());
ServiceLocator.SetLocatorProvider(() => locator);

D’autres implémentations ressemblantes ne sont pas équivalentes.
Par exemple:

ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(ConfigureUnityContainer()));

ou

UnityContainer container = new UnityContainer();
container.RegisterType<IFoo, Foo>(new ContainerControlledLifetimeManager());
ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(container));

La différence avec l’implémentation proposée plus haut est qu’on instancie un nouveau "Service Locator" en exécutant le délégué SetLocatorProvider() à chaque exécution de ServiceLocator.Current. La conséquence est que les instances des services récupérées par ServiceLocator.Current.GetInstance<...>() seront différentes à chaque exécution de cette ligne.

Cette implémentation est proposée par Chris Tavares: CommonServiceLocator.
Cette implémentation utilise ServiceLocatorImplBase et la classe statique ServiceLocator qui font partie de l’assembly Microsoft.Practices.ServiceLocation: disponible avec le package nuget "CommonServiceLocation".

Inconvénients de "Service Locator"

"Service Locator" ne résouds pas vraiment le problème des dépendances entre objets car il fait croire que les dépendances entre les objets ont été diminuées. Ce n’est pas tout-à-fait vrai car toutes les dépendances ont été regroupées dans le "Service Locator" qui possède désormais un lien avec tous les autres services.

"Service Locator" risque de diverger vers une classe "fourre-tout"

Etant donné que "Service Locator" possède des références vers tous les services, rien n’empêche de l’utiliser pour les mettre en relation. A terme, l’objet ServiceLocator risque de devenir un sac de noeud qui peut vite devenir inextricable. Il faut donc empêcher l’utilisation de références spécialisées dans le "Service Locator":
– Une façon de le garantir est d’implémenter le "Service Locator" dans un projet séparé où il n’existera aucune référence vers un projet contenant des implémentations spécialisées.
– Une autre méthode est d’utiliser un framework d’injection de dépendances.

"Service Locator" casse l’encapsulation

Le gros inconvénient de "Service Locator" est que, vu de l’extérieur, il est difficile de savoir quels sont les objets consommés par l’objet consommateur.
Par exemple, dans l’exemple précédent, si on souhaite utiliser ArticleController sans avoir enregistré ArticleRepository:

public class ArticleController
{
    private ArticleRepository articleRepository;
    private StockRepository stockRepository;

    public ArticleController()
    {
        this.articleRepository = ServiceLocator.Instance
            .GetRegisteredService<ArticleRepository>();
        this.stockRepository = ServiceLocator.Instance
            .GetRegisteredService<StockRepository>();
    }
}

La ligne this.articleRepository = ServiceLocator.Instance.GetRegisteredService(); va provoquer une exception car ArticleRepository n’est pas enregistrée.
Si quelqu’un utilise la classe ArticleController sans en connaître l’implémentation, il sera impossible de prévoir qu’il est nécessaire d’avoir enregistrer ArticleRepository. Le problème apparaîtra à l’exécution si toutefois il a été testé.

La solution à ce problème est l’injection de dépendances par le constructeur qui permet de montrer clairement les dépendances de l’objet consommateur.

Les dépendances sont plus difficiles à maintenir

Plus généralement, sachant que les objets consommés n’apparaissent que dans le corps des fonctions de la classe consommatrice, ils n’apparaitront plus dans la signature de ces fonctions. En cas de modification de l’implémentation, il est plus difficile de prévoir quelles sont les dépendances qui ont changées.
Cette difficulté rends la maintenance des dépendances plus délicates puisqu’elle nécessite d’aller systématiquement vérifier l’implémentation des classes et d’être attentif aux modifications de dépendances.

Références:

Versions des composants .NET

1. Synthèse des versions des composants du Framework .NET

Version framework .NET Date de sortie CLR .NET Standard(1) Visual Studio Compilateur C# MSBuild
(ToolsVersion)
Version des assemblies .NET Compatibilité Windows
4.7.1 octobre 2017 4.0 2.0(2) VS 2017 (15.3) 7.1 15.0 Cette règle n’est plus applicable(8) 7 SP1, 8.1,
10 Anniversary Update,
2008 R2 SP1, 2012,
2012 R2, 2016
4.7 avril 2017 VS 2017 (15.0)(3) 7.0
4.6.2 août 2016 VS 2015 (14.0)(3) 6.0 14.0 4.0.30319.X
(par ex. 4.0.30319.42000)
4.6.X.X
7 SP1, 8, 8.1, 10,
2008 SP2, 2008 R2 SP1, 2012, 2012 R2
4.6.1 novembre 2015
4.6 juillet 2015 1.3 4.0.30319.X
(par ex. 4.0.30319.18400)
4.5.X.X
Vista SP2, 7 SP1, 8, 8.1, 10,
2008 SP2, 2008 R2 SP1, 2012, 2012 R2
4.5.2 mai 2014 1.2 VS 2013 (12.0)(4) 5.0 12.0(7) Vista, 7, 8, 8.1,
2008 SP2, 2008 R2 SP1, 2012, 2012 R2
4.5.1 novembre 2013
4.5 août 2012 1.1 VS 2012 (11.0)(4) 4.0 4.0.30319.X(6)
(par ex. 4.0.30319.17329)
4.5.X.X
Vista, 7, 8,
2008 SP2, 2008 R2 SP1, 2012
4.0 janvier 2010 VS 2010 (10.0)(5) 4.0 4.0.30319.X
(par ex. 4.0.30319.1/4.0.30319.269)
Vista, 7,
2003, 2008 SP2, 2008 R2 SP1
3.5 novembre 2007 2.0 VS 2008 (9.0) 3.0 3.5 3.5.X.X
(par ex. 3.5.30729.5420)
Vista, 7, 8, 8.1, 10,
2003, 2008 SP2, 2012, 2012 R2, 2008 R2 SP1
3.0 novembre 2006 Visual Studio 2005 (8.0) 2.0 2.0 3.0.X.X
(par ex 3.0.6920.5011)
Vista,
2003, 2008 SP2, 2008 R2 SP1
2.0 janvier 2006 2.0.X.X
(par ex. 2.0.50727.8645)
2003, 2008 SP2, 2008 RS SP1
1.1 avril 2003 1.1 VS 2003 (7.1) 1.2 2003
1.0 janvier 2002 1.0 VS .NET (7.0) 1.0

(1): Version maximum du .NET Standard implémentée par le framework (par exemple: le framework 4.6.1 respecte au maximum le .NET Standard 2.0) (cf. .NET Standard Versions).
(2): La version minimum du framework implémentant le .NET Standard 1.4 est 4.6.1 donc tous les frameworks supérieurs à 4.6.1 respectent le .NET Standard 1.4 et supérieur (cf. .NET Standard Versions).
(3): Visual Studio 2015 et 2017 permettent de compiler des projets du framework 4.7.
(4): Visual Studio 2012 et 2013 permettent de compiler des projets du framework 4.6.2 maximum.
(5): Visual Studio 2010 permet de compiler des projets du Framework 4.0 maximum.
(6): Les frameworks 4.5 et suivants ont remplacés des assemblies du framework 4.0 contrairement aux versions précédant la 4.0 qui ajoutent des assemblies (voir plus bas).
(7): A partir de Visual Studio 2013, MSBuild n’est plus livré avec le framework mais directement avec Visual. Le passage de la version de MSBuild 4.0 à 12.0 est dû à l’alignement avec la version de Visual Studio 2013.
(8): Microsoft conseille de ne plus se baser sur les versions d’assemblies pour déterminer la version du framework (cf. How to: Determine Which .NET Framework Versions Are Installed). Cette règle n’est donc plus applicable.

Remplacement du Framework 4.0 par la version 4.5

Jusqu’à la version 4.0, toutes les versions du framework correspondait à des ajouts d’assemblies. Les assemblies du framework 2.0 sont rangées dans un répertoire particulier, et il en est de même pour le framework 3.0 et 3.5. Il suffisait donc de regarder la version des assemblies pour savoir de quel framework elles dépendaient.

Depuis la version 4.5, Microsoft a remplacé certaines assemblies du Framework 4.0 par des assemblies du framework 4.5 (de même pour les frameworks suivants). Ces nouvelles assemblies sont rangées dans le même répertoire que ceux du 4.0 et surtout leur version ne change que pour le numéro de révision:

Par exemple, l’assembly "clr.dll" dans C:\Windows\Microsoft.NET\Framework\v4.0.30319 aura pour numéro de version:

Version framework .NET Version clr.dll
4.0 4.0.30319.0 à 4.0.30319.17000
4.5 4.0.30319.17001 à 4.0.3019.18400
4.5.1 4.0.30319.18401 à 4.0.30319.33999
4.5.2 4.0.30319.34000 à 4.0.30319.41999
4.6 A partir de 4.0.30319.42000
Ne pas utiliser les versions d’assemblies pour déterminer le framework installé

Il est conseillé de ne plus se baser sur les versions d’assemblies pour déterminer la version du framework. Il faut utiliser la méthode avec la base de registres (cf. How to: Determine Which .NET Framework Versions Are Installed).

Problèmes de compatibilité

Sachant que les assemblies du Framework 4.0 sont remplacées par celles du Framework 4.5 et suivants, il peut se produire une incompatibilité. 2 cas de figure peuvent se produire:

1er cas: compatibilité ascendante
Par exemple, si une application est compilée en ciblant le Framework 4.0 et qu’elle est testée sur une machine comprenant seulement le Framework 4.0. Les assemblies .NET utilisées seront celles du Framework 4.0.
En revanche, si on exécute cette application sur une autre machine où le Framework 4.5 est installée, les assemblies .NET utilisées seront celles du Framework 4.5 (certaines assemblies du Framework 4.0 ayant été remplacées par l’installation du Framework 4.5). Il peut se produire un problème de compatiblité car l’application n’a pas été testée pour les assemblies appartenant au Framework 4.5.
Ce cas de figure est plus rare puisque les assemblies sont théoriquement plus stables lorsque la version augemente.

2e cas: compatibilité descendante
Si on compile une application en ciblant le Framework 4.0 sur une machine où le Framework 4.5 est installée, cette application sera testée en utilisant les assemblies du Framework 4.5.
Si on exécute cette même application sur une autre machine où seul le Framework 4.0 est installé, il peut se produire un problème de compatibilité car l’application n’a pas été testée pour les assemblies appartenant au Framework 4.0.
La solution à ce problème est de forcer à exécuter l’application si le Framework 4.5 en l’indiquant dans le fichier de configuration.

Une solution plus générale serait de tester une application exactement dans la même version de framework que les machines sur lesquelles elle sera déployée.

"Breaking-changes" connus:
Application Compatibility in the .NET Framework 4.5.

Comment indiquer qu’une application nécessite le Framework 4.5 ?

Pour une application WinForms, WPF ou Console, il faut indiquer dans le app.Config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration>

Pour une application ASP.NET:

<configuration>
    <system.web>
        <compilation debug="true" strict="false" explicit="true" targetFramework="4.5" />
    </system.web>
</configuration>

Un message sera affiché à l’exécution de l’application si la version nécessaire du framework n’est pas installée.

Comment déterminer la version du framework installé ?

Suite au remplacement du 4.0 par les versions suivantes, on ne peut plus regarder les répertoires d’installation du framework pour savoir si le framework est installée. De nombreuses assemblies se trouveront toujours dans le répertoire C:\Windows\Microsoft.NET\Framework\v4.0.30319 alors qu’elles appartiennent à des frameworks suivants le 4.0.

La méthode consiste à regarder en base de registres en tapant "regedit.exe":

  • Pour les frameworks de 1 à 4: sous la clé HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP se trouve des noeuds avec des numéros correspondant aux versions des frameworks installés.
  • Pour les frameworks supérieurs ou égaux à 4.5: à la clé HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full, se trouvent la valeur "Release". La valeur du DWORD "Release" permet de déterminer le framework:
    Version du Framework .NET Valeur du DWORD Release
    4.7.1 Sur Windows 10: 461308
    Sur tous les autres OS: 461310
    4.7 Sur Windows 10: 460798
    Sur tous les autres OS: 460805
    4.6.2 Sur Windows 10: 394802
    Sur tous les autres OS: 394806
    4.6.1 Sur Windows 10: 394254
    Sur tous les autres OS: 394271
    4.6 Sur Windows 10: 393295
    Sur tous les autres OS: 393297
    4.5.2 379893
    4.5.1
    installé sur Windows 8, Windows 7 SP1 ou Windows Vista SP2
    378758
    4.5.1
    installé sur Windows 8.1 ou Windows Server 2012 R2
    378675
    4.5 378389

Plus d’informations sur: How to: Determine Which .NET Framework Versions Are Installed.

Chemin des assemblies du Framework .NET

Les assemblies du framework se trouvent dans:

C:\Windows\Microsoft.NET\Framework\vX.X

avec "X.X" étant le numéro du framework de 1.0 à 3.5 et 4.0.30319 pour les versions supérieurs à 4.0. Ces assemblies ne doivent pas être référencées dans un projet Visual.

Les assemblies à référencer dans un projet Visual se trouvent dans:

C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework

Emplacement de MSBuild

Avant Visual Studio 2013, MSBuild était livré avec le Framework .NET et se trouvait dans le répertoire du framework:

  • .NET v2.0: C:\Windows\Microsoft.NET\Framework\v2.0.50727\MSBuild.exe
  • .NET v3.5: C:\Windows\Microsoft.NET\Framework\v3.5\MSBuild.exe
  • .NET v4.0: C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe

A partir de Visual Studio 2013, MSBuild est livré avec Visual et se trouve dans le répertoire "Program Files":

  • Sur une machine 32-bit: C:\Program Files\MSBuild\12.0\bin
  • Sur une machine 64-bit: C:\Program Files (x86)\MSBuild\12.0\bin

2. Synthèse des fonctionnalités par version

Framework

2.0:

  • WinForms,
  • ASP.NET,
  • ADO.NET.

3.0:

  • WPF,
  • WCF,
  • WF,
  • Card Space.

3.5:

  • LinQ,
  • Entity Framework.

4.0:

  • Parallel LinQ,
  • Task Parallel Library.

4.5

  • Modern UI Runtime,
  • Modèle asynchrone basé sur des tâches.

4.5.1

  • Compactage de la pile des objets de grande taille ("large object heap") à la demande,
  • Amélioration de la compilation à la volée ("JIT") sur des machines multicore.

Compilateur C#

Se reporter à la Synthèse des fonctionnalités du langage C# par version.

3. Dates de fin de support

Framework .NET

Version Framework .NET Support standard Support étendu
.NET 3.5 et 3.5 SP1 12/07/2011 12/07/2011
.NET 4.0, 4.5 et 4.5.1 12/01/2016 12/01/2016
.NET 4.5.2(1) 09/01/2018 10/01/2023
.NET 4.6(2) 13/10/2020 14/10/2025
.NET 4.6.1(2)
.NET 4.6.2(2)
.NET 4.7(3) 11/01/2022 11/01/2027
.NET 4.7.1(3)

(1): même cycle de vie que Windows 8.1.
(2): même cycle de vie que Windows 2012.
(3): même cycle de vie que Windows Server 2016.

Visual Studio

Version Visual Studio Support standard Support étendu
VS 2005 12/04/2011 12/04/2016
VS 2008 09/04/2013 10/04/2018
VS 2010 14/07/2015 10/07/2020
VS 2012 09/01/2018 10/01/2023
VS 2013 09/04/2019 09/04/2024
VS 2015 13/10/2020 14/10/2025
VS 2017 12/04/2022 13/04/2027

Windows

Version Windows Support standard Support étendu
Windows Server 2003 14/07/2015
Windows Server 2003 SP2 13/01/2015 14/01/2020
Windows Vista SP2 10/04/2012 11/04/2017
Windows Server 2008 R2 13/01/2015 14/01/2020
Windows 7 SP1 13/01/2015 14/01/2020
Windows 8.1 9/01/2018 10/01/2023
Windows Server 2012 09/10/2018 10/10/2023
Windows 10 13/10/2020 14/10/2025
Windows Server 2016 11/01/2022 11/01/2027

Pour plus d’informations:
Politique de support Microsoft

Fichier de configuration en .NET en 10 min

Fichier de configuration simple

Ajouter un fichier à un projet avec Visual Studio

Clique droit sur le projet 
    => Add 
    => New item 
    => Application configuration file.

Le fichier rajouté sera appelé "App.config". Lorsqu’il sera copié dans le répertoire de l’exécutable, il sera nommé: "[nom de l’exécutable].exe.config".

Le contenu du fichier sera:

<?xml version="1.0" encoding="utf-8" ?>  
<configuration>  
   <appSettings>   
   </appSettings>  
</configuration> 

On peut aussi directement créer un fichier XML avec le nom "[nom de l’exécutable].exe.config" et configurer sa copie dans le dossier de l’exécutable:

Clique droit sur le fichier 
    => Properties 
    => Sélectionnez le propriété "Copy to ouput directory" 
    sur "Copy if newer".

Ajouter des paramètres dans une section prédéfinie

On peut directement ajouter des paramètres dans la section "AppSettings":

<configuration>  
   <appSettings>  
       <add key="key1" value="value1"/>  
       <add key="key2" value="value2"/>  
   </appSettings>  
</configuration> 

Les paramètres sont accessibles dans le code avec la classe System.Configuration.ConfigurationManager (dans l’assembly System.Configuration.dll):
string value1 = ConfigurationManager.AppSettings["key1"];
ou
string value2 = ConfigurationManager.AppSettings[1];

ConnectionStrings

On peut utiliser la section ConnectionStringsSection de la même façon que AppSettingsSection avec le nœud XML <connectionStrings>.

Section de configuration

ConfigurationSection

Sections de configuration pré-définies

Quelques sections prédéfinies peuvent être utilisées:

  • ProtectedConfigurationSection: propose une section de configuration cryptée.
  • IgnoreSection: section ignorée par le parser. Attention, cette section ne doit toutefois par contenir d’erreurs de syntaxe.

Section de configuration personnalisée

On peut personnaliser une section de configuration en précisant directement le type de la section personnalisée. Il faut toutefois que la section personnalisée dérive de ConfigurationSection:

public class CustomSection : ConfigurationSection 
{ 
    [ConfigurationProperty("property1", IsRequired = true)] 
    public string Property1 
    { 
        get { return (string)this["property1"]; } 
        set { this["property1"] = value; } 
    } 
 

    [ConfigurationProperty("property2", DefaultValue = "false", 
       IsRequired = false)] 
    public string Property2 
    { 
        get { return (string)this["property2"]; } 
        set { this["property2"] = value; } 
    } 
}

On définit la section personnalisée dans le fichier de configuration avec:

<configuration> 
    <configSections> 
        <section name="customSection" 
            type="ApplicationNamespace.CustomSection" /> 
    </configSections> 
    <CustomSection property1="value1" property2="false"/> 
</configuration> 

On peut lire les valeurs en utilisant le ConfigurationManager avec:

CustomSection section = (CustomSection)ConfigurationManager.GetSection(
    "customSection"); 

Attributs utilisés dans la section de configuration personnalisée
Comme on peut le voir dans l’exemple plus haut, on utilise certains attributs pour définir les propriétés des éléments de configuration: [ConfigurationProperty("property2", DefaultValue = "false", IsRequired = false)]

  • ConfigurationProperty: attribut pour indiquer une propriété de configuration de la section de configuration
  • DefaultValue: valeur par défaut
  • IsRequired: indique si la valeur est requise.
  • Description: permet de rajouter une description qui peut être exploitée au moment de l’utilisation de l’objet ConfigurationSection.

Définition des propriétés avec ConfigurationPropertyCollection
On peut définir des propriétés dans la section de configuration en les ajoutant à une liste de propriété de type ConfigurationPropertyCollection. Cette méthode permet aussi de préciser des éléments pour configurer la propriété: valeur par défaut, indiquer si la valeur est indispensable, validateur, convertisseur etc… Les propriétés sont de type ConfigurationProperty.

Par exemple:

public class TypeSafeExampleSection: ConfigurationSection 
{ 
    private static ConfigurationPropertyCollection _properties; 
    private static ConfigurationProperty _intProperty; 
 
    static TypeSafeExampleSection() 
    { 
 
        _intProperty = new ConfigurationProperty( 
            "myInt", 
            typeof(int), 
            "Infinite", 
            new InfiniteIntConverter(), 
            new IntegerValidator(-10, 10), 
            ConfigurationPropertyOptions.IsRequired 
        ); 
 
        _properties = new ConfigurationPropertyCollection(); 
        _properties.Add(_intProperty); 
    } 
 
    [ConfigurationProperty("myInt", DefaultValue="Infinite", IsRequired=true)] 
    [IntegerValidator(-10, 10)] 
    [TypeConverter(typeof(InfiniteIntConverter)] 
    public int MyInt 
    { 
        get { return (int)base[_intProperty]; } 
    } 
}

Les deux notations sont équivalentes: par attribut ou en ajoutant la propriété instanciée avec ConfigurationProperty à la collection de type ConfigurationPropertyCollection.

Validator
On définit les validateurs sous forme d’un autre attribut, par exemple:

[ConfigurationProperty("background", DefaultValue = "FFFFFF", 
    IsRequired = true)] 
[StringValidator(InvalidCharacters = "~!@#$%^&*()[]{}/;'\"|\\GHIJKLMNOPQRSTUVWXYZ", 
    MinLength = 6, MaxLength = 6)]  
public String Background { … } 

On peut utiliser d’autres validateurs:

  • CallbackValidator (CallbackValidatorAttribute): pour définir une callback particulière pour valider la valeur d’une propriété.
  • IntegerValidator IntegerValidatorAttribute), LongValidator (LongValidatorAttribute): s’applique à des entiers (sur 32 bits) ou des long (64 bits). Il permet d’indiquer des valeurs exclues (ExcludeRange), une valeur minimale (MinValue) et une valeur maximale (MaxValue).
  • TimeSpanValidator (TimeSpanValidatorAttribute): s’applique à un objet de type TimeSpan.
  • RegexStringValidator (RegexStringValidatorAttribute): pour valider une chaîne de caractères en utilisant une Regex.
  • StringValidator (StringValidatorAttribute): on peut préciser des caractères invalides (InvalidCharacters), une longueur de chaîne minimale (MinLength) ou maximale (MaxLength).
  • SubclassTypeValidator (SubclassTypeValidatorAttribute): pour effectuer une validation sur le type de la propriété, par exemple: SubclassTypeValidator(typeof(MyBaseType)).

Validator personnalisé:
On peut définir un validator en dérivant de ConfigurationValidatorBase et en surchargeant 2 méthodes:

  • CanValidate(Type type) pour indiquer si le validator est capable de valider un type donné.
  • Validate(object value): valide la valeur donnée ou lancer une ArgumentException si la validation échoue.

Par exemple, si on définit un validator de chaîne qui utilise la validation par regex:

public class RegexValidator: ConfigurationValidatorBase  
{  
    private RegexStringValidator _regexValidator;  
 
    public RegexValidator(string regex)   
    {  
        this._regexValidator = new RegexStringValidator(regex); 
    }  
 
    public override bool CanValidate(Type type)  
    {  
        return (type == typeof(string));  
    }  
 
    public override void Validate(object value)  
    {  
        this._regexValidator.Validate(value);  
    }  
} 

Cette définition suffit si on affecte un validator à une propriété avec le constructeur ConfigurationProperty. Pour utiliser la notation utilisant un attribut, il faut définir l’attribut en dérivant de ConfigurationValidatorAttribute.

Par exemple:

public class RegexValidatorAttribute: ConfigurationValidatorAttribute 
{ 
    private string _regex; 
 
    public RegexValidatorAttribute(string regex) 
    { 
        this._regex = regex; 
    } 
 
    public string Regex 
    { 
        get { return this._regex; } 
    } 
 
    public override ConfigurationValidatorBase ValidatorInstance 
    { 
        return new RegexStringWrapperValidator(this._regex); 
    } 
} 

Converter
Il est possible d’appliquer une conversion entre les valeurs lues et l’objet utilisé dans la section de configuration.

Quelques "converters" prédéfinis mais il en existe d’autres:

  • CommaDelimiterStringCollectionConverter: convertit une chaîne de caractères contenant des valeurs séparées par des virgules. La conversion se fait vers une collection de type CommaDelimiterStringCollection.
  • GenericEnumConverter: convertit une chaîne vers un type d’énumérateur.
  • InfiniteIntConverter: convertit entre une chaine et un entier comprenant la valeur infinie.
  • InfiniteTimeSpanConverter: convertit une chaine vers un TimeSpan comprenant la valeur infinie.
  • TimeSpanMinutesConverter: convertit une chaine contenant une valeur en minutes vers un TimeSpan.
  • TimeSpanMinutesOrInfiniteConverter: convertit une chaine contenant une valeur en minutes ou infini vers un TimeSpan.
  • TimeSpanSecondsConverter: convertit une chaine contenant une valeur en secondes vers un TimeSpan.
  • TimeSpanSecondsOrInfiniteConverter: convertit une chaine contenant une valeur en secondes ou infini vers un TimeSpan.
  • TypeNameConverter: convertit le nom d’un type sous forme de chaine de caractères et le type.
  • WhiteSpaceTrimStringConverter: conversion vers une chaine dont le caractère de début et la fin est supprimé s’il s’agit d’un espace (Trim).

Converter personnalisé:
On peut définir un converter personnalisé en le faisant dériver de TypeConverter:

public class CustomTypeConverter : TypeConverter 
{ 
    public override bool CanConvertFrom(ITypeDescriptorContext context, 
        Type sourceType) 
    { 
        return sourceType == typeof(string); 
    } 
 
    public override object ConvertFrom(ITypeDescriptorContext context, 
        CultureInfo culture, object value) 
    { 

        // Conversion de la valeur écrite dans le fichier de configuration 
        // vers le type de la section de configuration 
        return new CustomTypeToConvert((string)value); 
    } 
 
    public override bool CanConvertTo(ITypeDescriptorContext context, 
        Type destinationType) 
    { 
        return destinationType == typeof(string); 
    } 
 
    public override object ConvertTo(ITypeDescriptorContext context, 
        CultureInfo culture, object value, Type destinationType) 
    { 

        // Conversion de type de la section vers une chaine écrite 
        // dans le fichier de configuration 
        var val = (CustomTypeToConvert)value; 
        return val.ToString();  
    } 
} 

ConfigurationElement

Les ConfigurationElement sont des éléments personnalisables à utiliser dans les sections de configurations:

<configuration> 
  <configSections> 
    <section name="customSection" 
        type="Application.Configuration.CustomSection, Application.Configuration" /> 
  </configSections> 
 
  <customSection property1="A sample string value." property2="true"> 
    <nestedElement nestedString="1" nestedDateTime="20/11/2015"/> 
  </customSection> 
</configuration>

Le ConfigurationElement se définit de cette façon:

namespace Application.Configuration 
{ 
    public class NestedElement: ConfigurationElement 
    { 
        private static ConfigurationProperty _nestedInteger; 
        private static ConfigurationProperty _nestedDateTime; 
 
        private static ConfigurationPropertyCollection _properties; 
 
        static NestedElement() 
        { 
            _nestedInteger = new ConfigurationProperty( 
                "nestedString", 
                typeof(int), 
                0, 
                ConfigurationPropertyOptions.IsRequired 
            ); 
 
            _nestedDateTime = new ConfigurationProperty( 
                "nestedDateTime", 
                typeof(DateTime), 
                null, 
                ConfigurationPropertyOptions.IsRequired 
            ); 
 
            _properties = new ConfigurationPropertyCollection(); 
             
            _properties.Add(_nestedInteger); 
            _properties.Add(_nestedDateTime); 
        } 
          
        [ConfigurationProperty("nestedString")] 
        public int NestedInteger 
        { 
            get { return (int)base[_nestedInteger]; } 
        } 
 
        [ConfigurationProperty("nestedDateTime", IsRequired=true)] 
        public DateTime NestedDateTime 
        { 
            get { return (DateTime)base[_nestedDateTime]; } 
        } 
 
        protected override ConfigurationPropertyCollection Properties 
        { 
            get { return _properties; } 
        } 
    } 
} 

Le ConfigurationElement s’utilise dans la section de configuration de cette façon:

public class CustomSection: ConfigurationSection 
{ 
    private static ConfigurationProperty _nestedElement; 
    private static ConfigurationPropertyCollection _properties; 
 
    static CustomSection() 
    { 
        _nestedElement = new ConfigurationProperty( 
            "nestedElement", 
            typeof(NestedElement), 
            null, 
            ConfigurationPropertyOptions.IsRequired 
        ); 
 
        _properties = new ConfigurationPropertyCollection(); 
        _properties.Add(_nestedElement); 
    } 
 
    [ConfigurationProperty("nestedElement")] 
    public NestedElement Nested 
    { 
        get { return (NestedElement)base[_nestedElement]; } 
    } 
} 

ConfigurationElementCollection

Permet de définir une liste d’éléments:

<configuration> 
  <configSections> 
    <section name="customSection" 
        type="Application.Configuration.CustomSection, Application.Configuration" /> 
  </configSections> 
  <customSection> 
    <item property1="key1" property2="value1"/> 
    <item property1="key2" property2="value2"/> 
  </customSection> 
</configuration> 

La liste d’éléments se définit de la façon suivante:

namespace Application.Configuration 
{ 
    public class Element : ConfigurationElement 
    { 
        private static readonly ConfigurationPropertyCollection _properties; 
        private static readonly ConfigurationProperty _property1; 
        private static readonly ConfigurationProperty _property2; 
 
        static Element() 
        { 
            _property1 = new ConfigurationProperty("property1", typeof(string), 
                null, ConfigurationPropertyOptions.IsKey); 
            _property2 = new ConfigurationProperty("property2", typeof(string), 
                null, ConfigurationPropertyOptions.IsRequired); 
             
            _properties = new ConfigurationPropertyCollection(); 
            _properties.Add(_property1); 
            _properties.Add(_property2); 
        } 
 
        public string Property1 
        { 
            get { return (string)this["property1"]; } 
            set { this["property1"] = value; } 
        } 
 
        public string Property2 
        { 
            get { return (string)this["property2"]; } 
            set { this["property2"] = value; } 
        } 
 
        protected override ConfigurationPropertyCollection Properties 
        { 
            get { return _properties; } 
        } 
    } 
 
    public class CustomElementCollection : ConfigurationElementCollection 
    { 
        public override ConfigurationElementCollectionType CollectionType 
        { 
            get { return ConfigurationElementCollectionType.BasicMap; } 
        } 
        protected override string ElementName 
        { 
            get { return "item"; } 
        } 
 
        protected override ConfigurationPropertyCollection Properties 
        { 
            get { return new ConfigurationPropertyCollection(); } 
        } 
 
        public Element this[int index] 
        { 
            get { return (Element)BaseGet(index); } 
            set 
            { 
                if (BaseGet(index) != null) 
                { 
                    BaseRemoveAt(index); 
                } 
                base.BaseAdd(index, value); 
            } 
        } 
 
        public new Element this[string elementName] 
        { 
            get { return (Element)BaseGet(elementName); } 
        } 
 
        public void Add(Element item) 
        { 
            base.BaseAdd(item); 
        } 
 
        public void Remove(Element item) 
        { 
            BaseRemove(item); 
        } 
 
        public void RemoveAt(int index) 
        { 
            BaseRemoveAt(index); 
        } 
 
        public void Clear() 
        { 
            BaseClear(); 
        } 
 
        protected override ConfigurationElement CreateNewElement() 
        { 
            return new Element(); 
        } 
 
        protected override object GetElementKey(ConfigurationElement element) 
        { 
            if (element != null) 
                return ((Element)element).Property1; 
            else 
                return null; 
        } 
    } 

} 

La section de configuration dans laquelle on utilise la liste se définit de la façon suivante:

namespace Application.Configuration 
{ 
    public class CustomSection : ConfigurationSection 
    { 
        private static readonly ConfigurationPropertyCollection _properties; 
        private static readonly ConfigurationProperty _elements; 
 
        static CustomSection() 
        { 
            _elements = new ConfigurationProperty( 
                "",  
                typeof(CustomElementCollection),  
                null,  
                ConfigurationPropertyOptions.IsRequired 
                    | ConfigurationPropertyOptions.IsDefaultCollection 
            ); 
 
            _properties = new ConfigurationPropertyCollection(); 
            _properties.Add(_elements); 
        } 
 
        public CustomElementCollection Elements 
        { 
            get { return (CustomElementCollection)base[_elements]; } 
        } 
 
        public new Element this[string elementName] 
        { 
            get { return Elements[elementName]; } 
        } 
 
        protected override ConfigurationPropertyCollection Properties 
        { 
            get { return _properties; } 
        } 
    } 

} 

On peut atteindre la section en exécutant:

CustomSection section = (CustomSection)ConfigurationManager.GetSection(
    "customSection"); 
Element item1 = section["key1"]; 
Element item2 = section["key2"]; 

ConfigurationElementCollectionType
Ce paramètre permet de gérer le comportement de la collection d’éléments de configuration pour les "cascades de collection".

Dans une application ASP.NET, plusieurs fichiers web.config peuvent être utilisés. Les applications "enfant" vont hériter des paramètres définis dans le web.config des applications "parentes".

ConfigurationElementCollectionType permet d’indiquer comment l’héritage des éléments de configuration dans la collection seront hérités entre une application "parente" et une application "enfant":

  • AddRemoveClearMap: on peut utiliser les 3 directives "add", "remove" et "clear" dans les collections de ce type. "add" va ajouter un élément; "remove" supprime un élément si il a été ajouté dans une application "parente" et "clear" va supprimer tous les éléments hérités. Les éléments dans cette collection sont hérités dans les applications "enfant" en ajoutant les éléments des applications "parentes" en première position.

    On peut l’utiliser comme la section AppSettingsSection:

    <appSettings> 
      <add property1="key1" property2="value1" /> 
      <add property1="key2" /> 
      <clear /> 
    </appSettings>
    
  • AddRemoveClearMapAlternate: même utilisation que AddRemoveClearMap sauf que les éléments sont hérités dans les applications "enfant" en ajoutant les éléments des applications "parentes" en dernière position.
  • BasicMap: ce type est plus restrictif que AddRemoveClearMap puisque les éléments sont hérités des applications "parentes" sans pouvoir les modifier dans les applications "enfant". Les éléments dans cette collection sont hérités dans les applications "enfant" en ajoutant les éléments des applications "parentes" en première position.
  • BasicMapAlternate: même utilisation que BasicMap sauf que les éléments sont hérités dans les applications "enfant" en ajoutant les éléments des applications "parentes" en dernière position.

SectionGroup

On peut personnaliser la configuration en rajoutant des groupes de sections de configuration dans le nœud "configSections":

<configuration> 
  <configSections> 
    <sectionGroup name="customSectionGroup"> 
      <section name="firstSection" 
          type="System.Configuration.NameValueSectionHandler" /> 
    </sectionGroup> 
  </configSections> 
  <customSectionGroup> 
    <firstSection> 
      <add key="key1" value="value1"/>  
      <add key="key2" value="value2"/>  
    </firstSection>     
  </customSectionGroup> 
</configuration>

Le nom "customSectionGroup" du "sectionGroup" fait référence au nœud du même plus bas. Le type System.Configuration.NameValueSectionHandler fait référence à un type de "section handler" prédéfini.

Pour récupérer les valeurs au niveau du code:

NameValueCollection section = (NameValueCollection)ConfigurationManager.GetSection(
    "customSectionGroup/firstSection"); 

Section handler

En plus de NameValueSectionHandler utilisé plus haut, il existe d’autres "section handler" prédéfinis. On a aussi la possibilité d’implémenter un "section handler" personnalisé.

Types de "section handler" prédéfini

NameValueSectionHandler

Permet d’obtenir les valeurs sous forme de clé/valeur. Dans le code le type de collection contenant les valeurs sera NameValueCollection.

DictionarySectionHandler

De même que NameValueSectionHandler, les valeurs seront sous forme de clé/valeur mais le type de la collection dans le code sera HashTable.

SingleTagSectionHandler

Permet de définir une section dans laquelle on pourra indiquer des attributs avec n’importe quelle nom de clé:

<configuration> 
   <configSections> 
      <section name="sampleSection" 
          type="System.Configuration.SingleTagSectionHandler" /> 
   </configSections> 
   <sampleSection setting1="Value1" setting2="value two"  
                  setting3="third value" /> 
</configuration> 

De même les valeurs peuvent être récupérées dans le code sous forme de HashTable:

Hashtable section = (Hashtable)ConfigurationManager.GetSection(
    "sampleSection"); 
string value1 = section["setting1"]; 
string value2 = section["setting2"]; 
string value3 = section["customSetting"]; 

IgnoreSectionHandler

Permet de ne pas prendre en compte une section qui ne peut pas être gérée par System.Configuration. IgnoreSectionHandler n’empêche pas les exceptions dues à parsing incorrect du fichier de configuration.

Pour accéder aux paramètres de cette section, il faut "parser" directement le fichier XML.

Modification de la configuration par programmation

A l’exécution, on peut modifier et enregistrer la configuration par programmation. Par exemple:

Configuration config = ConfigurationManager.OpenExeConfiguration(
    ConfigurationUserLevel.None); 
config.AppSettings.Settings.Remove("key1"); 
config.AppSettings.Settings.Add("key1", "value1"); 
config.Save(ConfigurationSaveMode.Modified); 
ConfigurationManager.RefreshSection("appSettings"); 

ConfigurationSaveMode permet d’indiquer quelles sont les propriétés qui seront écrites:

  • Full: toutes les propriétés sont enregistrées, même si elles n’ont pas été modifiées.
  • Minimal: seules les propriétés modifiées ayant une valeur différente de la valeur précédente seront enregistrées.
  • Modified: les propriétés modifiées seront enregistrées y compris celles ayant la même valeur que précédemment.

Utilisation de fichiers externes

Sections de configuration

Il est possible de définir des sections de configuration dans des fichiers externes au fichier de configuration (i.e. le fichier nommé [nom de l’assembly].exe.config). Il suffit d’utiliser l’attribut "configSource":

Dans le fichier de configuration principal:

<configuration> 
  <configSections> 
    <section name="customSection" 
        type="Application.Configuration.CustomSection, Application.Configuration" /> 
  </configSections> 
  <customSection configSource="config/customValues.config" /> 
</configuration> 

Dans le fichier externe dans le répertoire "config":

<customSection> 
    <item property1="key1" property2="value1"/> 
    <item property1="key2" property2="value2"/> 
</customSection> 
ATTENTION:

Le fichier externe doit obligatoirement être dans le même répertoire ou dans un sous-répertoire du fichier de configuration principal pour que l’attribut "configSource" fonctionne.

Section prédéfinie "AppSettings"

Avec la section AppSettings, il est possible de définir un fichier externe en utilisant l’attribut "configSource" comme précédemment:

<configuration>  
   <connectionStrings configSource="connections.config" />  
   <appSettings configSource="appSettings.config" />  
</configuration> 

On peut aussi utiliser l’attribut "file" pour spécifier un fichier externe contenant des valeurs. L’intérêt de "file" par rapport à "configSource" est que les valeurs dans le fichier externe surcharge les valeurs définies dans le fichier principal.

Par exemple:
Dans le fichier principal:

<configuration>  
   <appSettings file="appSettings.config">  
       <add key="key1" value="valueFromMainFile"/>  
       <add key="key3" value="value3"/>  
   </appSettings>  
</configuration>

Dans “appSettings.config”:

<appSettings>  
     <add key="key1" value="valueFromExternalFile"/>  
     <add key="key2" value="value2"/> 
</appSettings> 

Dans ce cas, les valeurs dans le fichier externe surchargent celles définies dans le fichier principal. Ainsi, ConfigurationManager.AppSettings contiendra:

key="key1"/value="valueFromExternalFile" 
key="key2"/value="value2" 
key="key3"/value="value3"
Remarques:
  • Si le fichier "appSettings.config" n’existe pas, il n’y aura pas de message d’erreur et l’attribut "file" sera ignoré.
  • Il n’est pas possible de préciser un autre fichier externe à partir du fichier externe "appSettings.config" (i.e. on ne peut pas utiliser l’attribut "file" dans le fichier externe).
  • Le fichier externe doit contenir seulement un nœud "appSettings".

Design pattern: Façade

Objectif:

Simplifie l’interface d’une ou plusieurs classes

Justification

Problème

Lorsqu’une interface doit être consommée par une classe cliente, il est courant de vouloir simplifier cette interface:
– pour cacher la complexité de l’implémentation interne et présenter une interface simple à utiliser,
– simplifier l’appel à beaucoup d’objets internes en ne proposant qu’une interface unique,
– limiter les dépendances des classes clientes en évitant d’exposer trop d’objets internes. Ces objets internes étant invisibles de l’extérieur, on est sûr qu’ils sont utilisés uniquement en interne. On est alors plus libre de les modifier pour des éventuelles évolutions futures sans casser les dépendances des classes clientes extérieures.

Par exemple
On réalise un système d’émission de billets qui utilise d’autres systèmes de billeterie hétérogènes:
– un système de réservation de billets d’avion accessible au moyen d’une API propriétaire,
– un système de réservation de billets de train interrogeable avec un web service sécurisé,

Les demandes d’émissions peuvent être lancées au moyen d’une interface graphique ou d’un web service. Pour lancer les émissions, on ne communique que les références de dossier. C’est au système de récupérer toutes les données nécessaires pour effectuer les émissions.

Sachant que le système peut être interrogée au moins de 2 façons: interface graphique et web service, on voudrait avoir une interface unique pour lancer l’émission:

public interface ITicketIssuingSystem
{
    TicketIssue IssueTicket(string ticketReference);
    bool IsTicketAlreadyIssued(string ticketReference);
}

public class TicketIssue
{
    public bool HasBeenIssued { get; set; }
    public string OccuredError { get; set; }
}

L’interface de l’objet interne permettant de s’interfacer avec le système de réservation de billets d’avion est:

public interface IAirlineTicketBookingSystem
{
    ...
    ITicketData GetTicket(string ticketReference);
}

L’interface de l’objet interne permettant de s’interfacer avec le système de réservation de billets de train est:

public interface ITrainTicketBookingSystem
{
    ...
    ITicketData GetTicket(string ticketReference);
}

Pour interroger la base de données, on utilise la classe "TicketRepository" qui effectue la requête sur la table "IssuedTicket":

internal class IssuedTicketRepository
{
    ...
    public bool AddNewIssuedTicket(string ticketReference)
    {...}

    public bool ContainsTicket(string ticketReference)
    {
        using (var context = new TicketDBEntities())
        {
            return context.IssuedTickets.Any(t => t.Reference.Equals(ticketReference));
        };
    }
}

Pour effectuer chaque émission, le système doit:
– interroger les deux autres systèmes de réservation pour récupérer les données relatives au billet,
– interroger une base de données pour vérifier que l’émission du billet n’a pas déjà été demandée, de façon à ne pas émettre 2 fois le même billet,
– logger des informations relatives à la requête,
– envoyer des mails aux passagers pour indiquer l’émission du ou des billets.

Solution

"Façade" permet d’apporter une solution:
– en proposant une interface unique aux classes clientes,
– en évitant d’exposer les autres objets internes aux classes clientes et
– en utilisant les objets internes pour effectuer l’émission et renvoyer les résultats.

La "Façade" sera alors l’unique objet appelé par l’interface graphique et par le web service et c’est elle qui va interroger tous les autres objets pour effectuer l’émission:

public class TicketIssueFacade : ITicketIssuingSystem
{
    private IAirlineTicketBookingSystem _airlineSystem;
    private ITrainTicketBookingSystem _trainSystem;
    private IssuedTicketRepository _issuedTicketRepository;
    private ILog _logger;

    public TicketIssueFacade(IAirlineTicketBookingSystem airlineSystem, 
        ITrainTicketBookingSystem trainSystem, IssuedTicketRepository _issuedTicketRepository,
        ILog logger)
    {
        this._airlineSystem = airlineSystem;
        this._trainSystem = trainSystem;
        this._issuedTicketRepository = issuedTicketRepository;
        this._logger = logger;
    }

    public TicketIssue IssueTicket(string ticketReference)
    {
        if (this.IsTicketAlreadyIssued(ticketReference))
            return new TicketIssue
            {
                HasBeenIssued = false,
                OccuredError = "Ticket has been alreadu issued."
            };

        var ticket = this._airlineSystem.GetTicket(ticketReference);
        if (ticket == null)
        {
            ticket = this._trainSystem.GetTicket(ticketReference);
        }

        if (ticket == null)
            return new TicketIssue
            {
                HasBeenIssued = false,
                OccuredError = "Ticket data not found."
            };

        bool ticketIssued = false;
        string occuredError = string.Empty;
        if (this._issuedTicketRepository.AddNewIssuedTicket(ticketReference))
        {
            ticketIssued = true;
            this._logger.InfoFormat("Ticket {0} has been issued.", ticketReference);
        }
        else
        {
            occuredError = "Ticket not issued for an unknown reason."
            this._logger.InfoFormat("Ticket {0} not issued.", ticketReference);
        }

        return new TicketIssue
        {
            HasBeenIssued = ticketIssued,
            OccuredError = occuredError,
        };
    }

    public bool IsTicketAlreadyIssued(string ticketReference)
    {
        return this._issuedTicketRepository.ContainsTicket(ticketReference);
    }
}

TicketIssueFacade devient alors la seule classe accessible par des classes clientes permettant d’émettre des billets.

Pour aller plus loin…

Diagramme théorique

Limites

Le plus gros inconvénient à "Façade" est qu’une classe façade peut rapidement devenir une classe "fourre-tout" où on aura tendance à placer tout le code. La raison principale est que c’est la classe qui met en relation d’autres classes sous-jacentes. Le risque est d’avoir une façade contenant beaucoup de code métier. Elle va donc perdre son objectif de simplifier des interfaces internes au profit d’une classe mettant tous les objets internes en relation. Cette tendance sera renforcée si la façade est consommée par d’autres objets internes.
"Façade" ne peut donc suffire seul à organiser l’implémentation, il faut y ajouter une rigueur et garder en tête que la façade sert à simplifier une implémentation ou des interfaces internes et doit être consommée par des objets externes.

Différences entre "Adapter" et "Façade"

"Adapter" et "Façade" sont très semblables, ils visent tous deux à adapter une complexité à une classe cliente.
Toutefois, "Adapter" s’utilise dans un contexte plus précis de l’adaptation ou de la conversion d’une ou plusieurs classes pour un besoin particulier.
"Façade" s’utilisera davantage pour cacher la complexité d’une fonctionnalité plus générale c’est-à-dire simplifier une implémentation ou plus généralement une interface, en particulier lorsque plusieurs classes internes doivent être appelées.

"Façade" simplifie les interfaces d’une ou plusieurs classes alors que "Adapter" convertit des interfaces pré-existantes.

Design pattern: Adapter

Objectif:

Convertir l’interface d’une ou plusieurs classes pour qu’elle soit adaptée à un ou plusieurs clients.

Justifications

Problèmes

Le besoin de présenter différemment un objet à une autre classe qui le consomme peut se justifier par plusieurs raisons:
– On veut présenter un objet plus adapté aux besoins de la classe cliente, de façon à volontairement éviter de présenter trop de fonctions, trop de membres ou des signatures trop complexes. On cible alors plus précisemment les besoins de la classe cliente et on évite de maintenir des fonctions ou membres non consommés.
– Les besoins des classes clientes peuvent nécessiter l’utilisation de plusieurs objets sous-jacents. Pour éviter des appels à tous ces objets, on peut vouloir aggréger les appels dans un seul objet.
– On souhaite limiter l’exposition d’objets internes.
– Présenter un objet plus adapté peut aussi signifier qu’une conversion de données est nécessaire entre le ou les objets consommés et la classe cliente. Cette conversion peut être unique ou spécifique à chaque classe cliente.

Par exemple:
On développe une API d’application lourde. Différents éléments sur cette application lourde permettent d’ajouter un bouton: le menu MenuHandler, la barre de raccourci ShortcutBar et d’une barre d’accès rapide EasyAccessToolbar.
Dans une première implémentation, ces éléments dérivent de l’objet ButtonContainer qui satisfait l’interface IButtonContainer. Seule cette interface est publique et est accessible dans l’API:

public interface IButtonContainer
{
   void AddButton(string buttonName, string caption, Action callback);
   void RemoveButton(string buttonName);
}

internal class ButtonContainer : IButtonContainer
{
   public virtual void  AddButton(string buttonName, string caption, Action callback)
   { ... }

   public virtual void RemoveButton(string buttonName)
   { ... }
}

internal class MenuHandler : ButtonContainer 
{
   public override void  AddButton(string buttonName, string caption, Action callback)
   { ... }

   public override void RemoveButton(string buttonName)
   { ... }
}

internal class ShortcutBar : ButtonContainer 
{
}

internal class EasyAccessToolbar : ButtonContainer 
{
}

Le client de l’API peut accéder aux objets de cette façon:

public interface IMainWindow
{
    IButtonContainer GetButtonContainer(ButtonContainerType containerType);
}

public class MainWindow : IMainWindow
{
    private MenuHandler _menuHandler;
    private ShortcutBar _shortcutBar;
    private EasyAccessToolbar _easyAccessToolbar;

    public MainWindow
    {
        this._menuHandler = new MenuHandler();
        this._shortcutBar = new ShortcutBar();
        this._easyAccessToolbar = new EasyAccessToolbar();
    }

    public IButtonContainer GetButtonContainer(ButtonContainerType containerType)
    {
        switch (containerType)
        {
            case ButtonContainerType.Menu:
                return this._menuHandler;
                break;
            case ButtonContainerType.Shortcut:
                return this._shortcutBar;
                break;
            case ButtonContainerType.EasyAccessToolbar:
                return this._easyAccessToolbar;
                break;
            default:
                throw new NotSupportedException();
        }
    }
}

public enum ButtonContainerType
{
    Menu,
    Shortcut,
    EasyAccessToolbar,
}

Deux demandes d’évolution imposent quelques changements:
– devoir afficher des menus déroulants dans le composant EasyAccessToolbar. On ne peut donc plus dériver de ButtonContainer.
– devoir ajouter un "ribbon" qui contient aussi des boutons. Ce ribbon provient d’un autre éditeur et impossible aussi de le faire dériver de ButtonContainer ou de le faire satisfaire l’interface IButtonContainer.

Enfin on doit pouvoir assurer la compatiblité ascendante (compatiblité par rapport aux anciennes versions) et on ne peut pas casser IMainWindow et IButtonContainer.

Solution

Le pattern "Adapter" permet de résoudre ce problème en permettant:
– D’adapter tous les composants pour qu’ils soient visibles de l’extérieur sous forme de IButtonContainer et ainsi encapsuler la complexité du polymorphisme dans une interface unique.
– Ne pas modifier d’interfaces et ainsi assurer la compatiblité ascendante.
– Organiser l’architecture sans trop casser l’existant.

On introduit donc un intermédiaire qui sera un adaptateur entre les objets contenant des boutons (MenuHandler, ShortcutBar, EasyAccessToolbar et Ribbon) et une classe cliente.
Les "adapters" satisferont IButtonContainer et appeleront directement les objets sous-jacents.

On peut proposer l’implémentation suivante en définissant un "adapter" abstrait:

internal abstract class ButtonContainerAdapter : IButtonContainer
{
    public abstract void AddButton(string buttonName, string caption, Action callback);
    public abstract void RemoveButton(string buttonName);
}

On implémente les différents "adapters":

internal class EasyAccessToolbarAdapter : ButtonContainerAdapter
{
   private EasyAccessToolbar _easyAccessToolbar;

   public EasyAccessToolbarAdapter(EasyAccessToolbar easyAccessToolbar)
   {
       this._easyAccessToolbar = easyAccessToolbar;
   }

   public override void AddButton(string buttonName, string caption, Action callback)
   {
       this._easyAccessToolbar.AddButton(buttonName, caption, callback);
   }

   public override void RemoveButton(string buttonName)
   {
       this._easyAccessToolbar.RemoveButton(buttonName);
   }
}

internal class RibbonAdapter : ButtonContainerAdapter
{
   private Ribbon _ribbon;

   public RibbonAdapter(Ribbon ribbon)
   {
       this._ribbon = ribbon;
   }

   public override void AddButton(string buttonName, string caption, Action callback)
   { ... }

   public override void RemoveButton(string buttonName)
   { ... }
}

Enfin on modifie MainWindow:

public class MainWindow : IMainWindow
{
    private MenuHandler _menuHandler;
    private ShortcutBar _shortcutBar;
    private EasyAccessToolbar _easyAccessToolbar;
    private Ribbon _ribbon;

    public MainWindow
    {
        this._menuHandler = new MenuHandler();
        this._shortcutBar = new ShortcutBar();
        this._easyAccessToolbar = new EasyAccessToolbar();
        this._ribbon = new Ribbon();
    }

    public IButtonContainer GetButtonContainer(ButtonContainerType containerType)
    {
        switch (containerType)
        {
            case ButtonContainerType.Menu:
                return this._menuHandler;
                break;
            case ButtonContainerType.Shortcut:
                return this._shortcutBar;
                break;
            case ButtonContainerType.EasyAccessToolbar:
                return new EasyAccessToolbarAdapter(this._easyAccessToolbar);
                break;
            case ButtonContainerType.Ribbon:
                return new RibbonAdapter(this._ribbon);
                break;
            default:
                throw new NotSupportedException();
        }
    }
}

public enum ButtonContainerType
{
    Menu,
    Shortcut,
    EasyAccessToolbar,
}

Pour aller plus loin…

Diagramme théorique

La variante présentée ci-dessus se base sur l’exemple présenté plus haut:
– Un "adapter" par classe adaptée,
– Les "adapters" dérivent d’un "adapter" abstrait.

Seulement avec une interface et sans classe abstraite:

Autres variantes

Comme tous les patterns, il n’y a pas de définition définitive ou absolue d’"Adapter", d’autres variantes sont possibles suivant le contexte:
– Au lieu d’utiliser une classe abstraite "Adapter", on peut utiliser seulement une interface. Et donc tous les "Adapters" doivent simplement satisfaire l’interface.
– Il peut y avoir un seul "adapter" pour plusieurs objets adaptés:

– Il peut aussi y avoir plusieurs "adapters" pour un seul objet adapté suivant la classe cliente qui le consomme.

Différences entre "Adapter" et "Façade"

"Adapter" et "Façade" sont très semblables, ils visent tous deux à adapter une complexité à une classe cliente.
Toutefois, "Adapter" s’utilise dans un contexte plus précis de l’adaptation ou de la conversion d’une ou plusieurs classes pour un besoin particulier.
"Façade" s’utilisera davantage pour cacher la complexité d’une fonctionnalité plus générale c’est-à-dire simplifier une implémentation ou plus généralement une interface, en particulier lorsque plusieurs classes internes doivent être appelées.

En définitive, "Façade" simplifie les interfaces d’une ou plusieurs classes alors que "Adapter" convertit des interfaces pré-existantes.