Quelques patterns pour attendre la fin de l’exécution d’une tâche*

*: Le terme “Tâche” est utilisé içi au sens large et non au sens de la Task Parallel Library (TPL).

Lorsque plusieurs threads sont utilisés et qu’il est nécessaire de synchroniser certains de ces threads, on peut être amener à implémenter des mécanismes d’attente. Ces mécanismes permettent ainsi:

  • D’éviter un accès concurrent à une section critique,
  • D’attendre le résultat d’un autre thread (rendre un algorithme synchrone),
  • De ralentir un tempo d’horloge trop rapide pour, par exemple, implémenter des algorithmes de “conflation”.

Plusieurs solutions sont possibles en .NET. Toutefois il peut être utile que ces solutions permettent d’implémenter certaines fonctionnalités comme:

  • Un “timeout”: au délà d’un certain temps d’attente, le thread reprendra son exécution.
  • L’annulation de l’attente: dans le cas où on souhaite terminer prématurément l’exécution d’un thread, il faut pouvoir annuler l’attente.

Mécanisme de signalement

ManualResetEvent

La méthode la plus directe est l’utilisation de l’objet ManualResetEvent ou des autres objets du même type comme AutoResetEvent ou ManualResetEventSlim.

Ces objets permettent de placer directement des threads en attente et de signaler la fin de l’attente.

Les principales différences entre ces objets sont:

  • AutoResetEvent: c’est l’objet le plus simple. Il est possible de mettre un thread en attente en exécutant WaitOne(). Ce thread sera débloqué si un autre thread exécute Set(). Cet objet n’autorise le déblocage que d’un seul thread à la fois. Si plusieurs threads attendent en exécutant Wait
  • ManualResetEvent: par rapport à AutoResetEvent, cet objet nécessite d’exécuter Reset() après avoir exécuté Set() pour qu’un thread puisse attendre à nouveau en exécutant WaitOne(). Si Reset() n’est pas exécuté, les appels suivants à WaitOne() ne seront pas bloquants. Contrairement à AutoResetEvent, si plusieurs threads sont en attente en exécutant WaitOne(), un seul appel à Set() débloque tous les threads.
  • ManualResetEventSlim: le comportement de cet objet est identique à ManualResetEvent à la différence qu’il ne permet pas de synchroniser des threads dans des processus différents. Toutefois il est plus performant que ManualResetEvent et AutoResetEvent.

On peut résumer brievement les performances et l’utilisation entre processus de ces objets dans le tableau suivant:

Objet Utilisation dans des processus différents Ordre de grandeur du temps d’un appel à cet objet *
AutoResetEvent Oui 1000 ns
ManualResetEvent Oui 1000 ns
ManualResetEventSlim Non 40 ns (25 fois plus rapide)

*: Les temps sont indiqués principalement pour montrer les différences en terme d’ordre de grandeur entre les différents objets.

En utilisant ManualResetEvent, on peut implémenter un mécanisme d’attente de la façon suivante:

public class ManualResetEventUsage  
{  
  private readonly ManualResetEventSlim waitingEvent = new ManualResetEventSlim(false); 
 
  public ManualResetEventUsage() 
  { } 
 
  public bool Wait(TimeSpan timeout) 
  { 
    Console.WriteLine("Waiting..."); 
    bool signaled = this.waitingEvent.Wait(timeout); 
    Console.WriteLine("Unlocked"); 
 
    return signaled; 
  } 
 
  public void Signal() 
  {  
    Console.WriteLine("Signaling..."); 
    this.waitingEvent.Set(); 
    Console.WriteLine("Signaled."); 
  } 
} 
 
class Test 
{  
  static void Main(string[] args) 
  {  
    var resetEventUsage = new ManualResetEventUsage(); 
 
    Task t = Task.Factory.StartNew(() => { resetEventUsage.Wait(TimeSpan.FromMilliseconds(1000)); }); 
    Thread.Sleep(200); 
 
    resetEventUsage.Signal(); 
    t.Wait(); 
  } 
}

AutoResetEvent, ManualResetEvent et ManualResetEvent permettent de préciser un timeout quand un appel est fait à WaitOne().

Plus de détails à propos de ces objets sur MSDN: ManualResetEvent, AutoResetEvent et ManualResetEventSlim.

Monitor

Contrairement au "lock" classique, Monitor permet d’indiquer un timeout ce qui permet d’implémenter facilement un mécanisme d’attente.

En exécutant Monitor.Wait(locker), on peut placer un thread en attente. L’objet “locker” est libéré pendant l’attente. Pour débloquer un thread en attente, il faut exécuter Monitor.Pulse(locker). Dans le cas où plusieurs threads sont en attente, on peut les libérer en exécutant Monitor.PulseAll(locker).

L’implémentation d’un mécanisme d’attente avec Monitor n’est pas trivial. On peut distinguer 2 types de files d’attente:

  • La file d’attente des threads prêts à s’exécuter: ce sont des threads qui sont en attente de l’acquisition d’un "lock".
  • La file d’attente des threads attendants: ce sont les threads ayant appelés Monitor.Wait() et qui sont dans l’attente d’une exécution de Monitor.Pulse().

Ces deux files d’attentes peuvent mener à des comportements inattendus. Lorsque Monitor.Pulse() est exécuté, le premier thread dans la file d’attente des threads attendants est placé dans la file d’attente des threads prêts. Toutefois d’autres threads peuvent être déjà présents dans cette file d’attente et faire l’acquisition du "lock" avant que le thread libéré ne puisse en faire l’acquisition. Le thread libéré n’aura donc pas accès au "lock" et ne s’exécutera pas.

La solution consiste à utiliser une boucle "while" pour permettre de replacer le thread libéré dans la file d’attente des threads attendants s’il n’a pas réussi à faire l’acquisition du "lock".

On peut implémenter le mécanisme d’attente de la façon suivante:

public class MonitorUsage  
{  
  private readonly object locker = new object(); 
  private bool block = true; 
 
  public MonitorUsage() 
  { } 
 
  public bool Wait(TimeSpan timeout) 
  { 
    Console.WriteLine("Waiting..."); 
    bool timeoutExpired = false; 
    lock (this.locker) 
    { 
      while (this.block && !timeoutExpired) 
      {  
        timeoutExpired = !Monitor.Wait(this.locker, timeout); 
      } 
         
      this.block = true; 
    } 
    Console.WriteLine("Unlocked"); 
 
    return timeoutExpired; 
  } 
 
  public void Signal() 
  {  
    Console.WriteLine("Signaling..."); 
    lock (this.locker) 
    { 
      this.block = false; 
      Monitor.Pulse(this.locker); 
    } 
    Console.WriteLine("Signaled."); 
  } 
} 
 
class Test 
{  
  static void Main(string[] args) 
  {  
    var monitorUsage = new MonitorUsage(); 
 
    Task t = Task.Factory
      .StartNew(() => { monitorUsage.Wait(TimeSpan.FromMilliseconds(1000)); }); 
    Thread.Sleep(200); 
 
    monitorUsage.Signal(); 
    t.Wait(); 
  } 
}
Attention à l’utilisation de Monitor

Quand on utilise Monitor.Wait() et Monitor.Pulse(), le pattern suivant ne suffit pas:

// thread A
lock (key) Monitor.Wait(key);

// thread B
lock (key) Monitor.Pulse(key);

Il faut plutôt utiliser un pattern semblable à celui utilisé dans l’exemple précédent car il permet de résoudre 2 problèmes:

  • La “race condition” qui peut se produire si on exécute Signal() avant Wait(). Dans ce cas, block sera à “false” et le boucle while ne s’exécutera pas ainsi que l’appel à Monitor.Wait().
  • Le problème du thread libéré qui ne peut pas faire l’acquisition du "lock". Si le thread libéré est ajouté à la file d’attente des threads prêts et qu’un autre thread fait l’acquisition du lock alors block sera à “true”. Ainsi le thread libéré ne sortira pas de la boucle "while" et exécutera à nouveau Monitor.Wait() et donc sera replacé dans la file d’attente des threads attendants.

Plus de détails sur Monitor sur MSDN.

Avec la Task Parallel Library (TPL)

Il existe de multiples implémentations de mécanismes d’attente avec des "Task".

Mécanisme de signalement avec une Task

La solution la plus directe consiste à utiliser Task.Wait() et TaskCompletionSource. Task.Wait() est bloquant et permet de renseigner un timeout. Le gros intérêt de cette méthode est de bénéficier de la gestion de l’annulation et des exceptions avec TPL.

Eviter d’utiliser ce pattern

Cet exemple permet de faire lien avec le mécanisme de signalement mais il n’est pas très adapté quand on utilise des Task.

On peut implémenter un mécanisme d’attente de cette façon:

public class TaskUsage  
{  
  private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>(); 
  private readonly Task canceledTask; 
  private readonly Task exceptionOccuredTask; 
 
  public TaskUsage() 
  {  
      this.canceledTask = this.taskCompletionSource.Task.ContinueWith( 
          tsk => Console.WriteLine("Canceled"), 
          TaskContinuationOptions.OnlyOnCanceled); 
      this.exceptionOccuredTask = this.taskCompletionSource.Task.ContinueWith( 
          tsk => Console.WriteLine("Exception occured: " + tsk.Exception.ToString()), 
          TaskContinuationOptions.OnlyOnFaulted); 
  } 
 
  public bool Wait(TimeSpan timeout) 
  { 
    Console.WriteLine("Waiting..."); 
    bool executedWithinTimeout = this.taskCompletionSource.Task.Wait(timeout) 
    Console.WriteLine("Unlocked"); 
 
    return executedWithinTimeout; 
  } 
 
  public void Signal() 
  {  
    Console.WriteLine("Signaling..."); 
    this.taskCompletionSource.SetResult(true); 
    Console.WriteLine("Signaled."); 
  } 
} 
 
class Test 
{  
  static void Main(string[] args) 
  {  
    var taskUsage = new TaskUsage(); 
 
    Task t = Task.Factory
      .StartNew(() => { taskUsage.Wait(TimeSpan.FromMilliseconds(1000)); }); 
    Thread.Sleep(200); 
 
    monitorUsage.Signal(); 
    t.Wait(); 
  } 
}

Continuation

Le mécanisme de signalement comme précédemment n’est pas vraiment adapté quand on utilise des “tasks” au sens TPL. Le pattern le plus direct pour attendre la fin de l’exécution d’une tâche est la “continuation”.

Une “continuation” permet de déclencher une tâche lorsqu’une autre tâche est terminée. Si cette autre tâche provoque une exception ou ne se termine pas, la “continuation” peut ne pas s’exécuter suivant les options indiquées à la définition de la “continuation”.

L’intérêt de cette méthode est la simplicité de son implémentation notamment en ce qui concerne la gestion des exceptions et des annulations.

Une “continuation” simple qui s’exécute en récupérant le résultat de la première tâche et qui ne s’exécute que si la première tâche se termine s’implémente de la façon suivante:

Task<bool> t = Task<bool>.Factory.StartNew(() =>  
  { 
    Console.WriteLine("Beginning first task execution..."); 
    // Making other processes 
    // ...
    Console.WriteLine("First ended"); 
  }); 
t.ContinueWith(tsk =>  
  { 
    Console.WriteLine("Result from first task: ", tsk.Result); 
  }, TaskContinuationOptions.OnlyOnRanToCompletion);  
t.Wait();

Il est possible d’implémenter des mécanismes de “continuation” quand la première tâche provoque une exception ou si elle est annulée: Utilisation des “Task” en 5 min.

Continuation + TaskCompletionSource

Dans le cas où la première tâche à exécuter ne contient pas de code particulier, on peut utiliser l’objet TaskCompletionSource et définir une continuation sur la task de la TaskCompletionSource. Pour déclencher la “continuation”, il suffit d’affecter un résultat à la task de la TaskCompletionSource.

L’intérêt de cette méthode est qu’il est possible de définir la “continuation” bien avant son exécution même dans le cas où on a pas d’implémentation particulière pour la première tâche. Le déclenchement de la “continuation” est rapide.

Ce type de mécanisme peut s’implémenter de la façon suivante:

public class TaskCompletionSourceUsage  
{  
  private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>(); 
 
  public TaskCompletionSourceUsage(Action<Task> actionToExecute) 
  {  
    this.taskCompletionSource.Task.ContinueWith(actionToExecute); 
  } 
 
  public void Signal() 
  {  
    Console.WriteLine("Signaling..."); 
    this.taskCompletionSource.SetResult(true); 
    Console.WriteLine("Signaled."); 
  } 
} 
 
class Test 
{  
  static void Main(string[] args) 
  {  
    var taskCompletionSourceUsage = new TaskCompletionSourceUsage(task =>  
      {  
        Console.WriteLine("Executing task..."); 
        // Making some processes 
        // ... 
        Console.WriteLine("Task ended"); 
      }); 
 
    // Making other processes 
    // ... 

    // Trigger the execution of the task 
    taskCompletionSourceUsage.Signal(); 
  } 
}

Task retardée (ajout d’un “timeout”)

Il est possible d’ajouter un espèce de “timeout” à l’implémentation précédente. Si la “continuation” n’a pas été exécuté dans l’intervalle du “timeout”, on peut déclencher un autre type de traitement.

Task retardée avec un “Timer” (Framework 4.0)

L’intérêt d’utiliser un Timer est de permettre de rendre cette technique compatible avec le Framework .NET 4.0.

Ce type de mécanisme peut s’implémenter de la façon suivante:

using System;
using System.Threading;

public class TimerUsage  
{  
  private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>(); 
  private bool resultAlreadySet = false; 
  private readonly object resultSync = new object(); 
  private readonly Timer timer; 
 
  public TimerUsage(int timeout, Action<bool> actionToExecute) 
  {  
    this.taskCompletionSource.Task.ContinueWith(completionSourceTask =>  
      {  
        this.ExecuteTask(completionSourceTask, actionToExecute); 
      }, TaskContinuationOptions.OnlyOnRanToCompletion); 
    this.timer = new Timer(state => this.SetTaskCompletionSourceResult(false)); 
    this.timer.Change(timeout, -1); 
  } 
 
  public void Signal() 
  {  
    Console.WriteLine("Signaling..."); 
    this.SetTaskCompletionSourceResult(true); 
    Console.WriteLine("Signaled."); 
  } 
 
  private void ExecuteTask(Task completionSourceTask, Action<bool> actionToExecute) 
  {  
    actionToExecute(completionSourceTask.Result); 
  } 
 
  private void SetTaskCompletionSourceResult(bool result) 
  {  
    if (!this.resultAlreadySet) 
    { 
      lock (this.resultSync) 
      {  
        if (!this.resultAlreadySet) 
        { 
          this.taskCompletionSource.SetResult(result); 
          this.resultAlreadySet = true; 
        } 
      } 
    } 
  } 
} 
 
class Test 
{  
  static void Main(string[] args) 
  {  
    var timerUsage = new TimerUsage(1000, result =>  
      {  
        if (result) 
        {  
          Console.WriteLine("Task executed within timeout"); 
        } 
        else 
        {  
          Console.WriteLine("Timeout");
        } 
      }); 
 
    // Making other processes 
    // ... 
 
    // Trigger the execution of the task 
    timerUsage.Signal(); 
  } 
}

SetTaskCompletionSourceResult permet d’éviter d’affecter le résultat 2 fois à la TaskCompletionSource.

Task retardée en utilisant Task.Delay() (Framework > 4.0)

Il suffit de remplacer le Timer avec une instruction Task.Delay(). A partir de l’exemple précédent la classe TimerUsage devient:

public class TaskDelayUsage  
{  
  private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>(); 
  private bool resultAlreadySet = false; 
  private readonly object resultSync = new object(); 
  private readonly Task delayedTask; 
 
  public TaskDelayUsage(int timeout, Action<bool> actionToExecute) 
  {  
    this.taskCompletionSource.Task.ContinueWith(completionSourceTask =>  
      {  
        this.ExecuteTask(completionSourceTask, actionToExecute); 
      }, TaskContinuationOptions.OnlyOnRanToCompletion); 
 
    this.delayedTask = Task.Delay(timeout) 
      .ContinueWith(task => this.SetTaskCompletionSourceResult(false)) 
      .Start(); 
  } 
 
  public void Signal() 
  {  
    Console.WriteLine("Signaling..."); 
    this.SetTaskCompletionSourceResult(true); 
    Console.WriteLine("Signaled."); 
  } 
 
  private void ExecuteTask(Task completionSourceTask, Action<bool> actionToExecute) 
  {  
    actionToExecute(completionSourceTask.Result); 
  } 
 
  private void SetTaskCompletionSourceResult(bool result) 
  {  
    if (!this.resultAlreadySet) 
    { 
      lock (this.resultSync) 
      {  
        if (!this.resultAlreadySet) 
        { 
          this.taskCompletionSource.SetResult(result); 
          this.resultAlreadySet = true; 
        } 
      } 
    } 
  } 
}

Liste de patterns non exhaustive

La liste des patterns présentés précédemment est loin d’être exhaustive. Le but est juste de référencer quelques méthodes de façon à donner des idées d’implémentation.
Toutefois, on peut dire que dans la majorité des cas, il est préférable d’utiliser TPL et de privilégier des mécanismes de “continuation”. Les mécanismes de signalement sont à utiliser avec plus de réserve ou pour les tests unitaires.

Plateforme cible en .NET en 5 min

A partir des options du “Configuration Manager” dans Visual, on peut compiler ses projets suivant plusieurs plateformes: “AnyCPU”, “x86”, “x64” et “Itanium”. A chacune de ses valeurs correspond une plateforme cible sur laquelle les assemblies pourront être chargées. Le mauvais choix de plateforme cible peut mener à des exceptions de type BadImageFormatException qu’il n’est pas forcément facile à prévoir.

Ainsi une assembly générée avec pour plateforme cible:

  • “AnyCPU” sera exécutée par le CLR 32 ou 64 bits.
  • “x86” sera exécutée exclusivement par le CLR 32 bits.
  • “x64” sera exécutée exclusivement par le CLR 64 bits.
  • “Itanium” sera exécutée sur une architecture Itanium.

Tout se complique en sachant qu’une machine avec une architecture 64 bits peut exécuter du code compilé pour les plateformes “AnyCPU”, “x86” et “x64”.

Le choix de la plateforme entraîne d’autres implications qu’il est important d’avoir en tête pour éviter des erreurs ou exceptions à l’exécution.

1. Différences de fonctionnement des exécutables

Quel que soit la plateforme cible choisie, s’il n’y a que de code managé, le code IL généré est pratiquement le même. Dans une assembly “AnyCPU”, il n’y a qu’une seule version du code IL (et non une version 32 bits et une version 64 bits). D’ailleurs quelque soit la plateforme, la taille des assemblies est pratiquement la même.

L’indication de la plateforme permet de rajouter des indications dans l’entête de l’assembly. A l’exécution en fonction de ces indications, le compilateur JIT choisira comment transformer le code IL en code machine:

  • Un code 64 bits ne peut être exécuté que sur une architecture 64 bits,
  • Un code 32 bits peut être exécuté sur une architecture 32 bits mais aussi une architecture 64 bits avec WoW64 (Windows 32-bit on Windows 64-bit qui est sous-sytème de Windows capable d’exécuter du code 32 bits).
  • Un code “AnyCPU” peut être exécuté sur une architecture 32 bits et 64 bits, toutefois il sera exécuté par un CLR 32 bits ou un CLR 64 bits (voir plus bas pour plus de détails).

Ainsi suivant la plateforme cible choisie, les assemblies résultants peuvent avoir ou non un comportement différent:

  • Les types primitifs: les types primitifs (integer ou long) sont codés sur 32 bits quel que soit l’architecture, en revanche les pointeurs sont codés sur 32 ou 64 bits suivant les architectures.
  • IntPtr: pour stocker des adresses qui s’adaptent à l’architecture, on peut utiliser le type IntPtr. En 32 bits, IntPtr.Size = 4; en 64 bits, IntPtr.Size = 8.
  • Assemblies système: suivant l’architecture, des assemblies systèmes différentes sont chargées puis utilisées. Par exemple, en 32 bits, C:\Windows\Microsoft.NET\Framework peut être utilisé alors qu’en 64 bits, on utilise plutôt C:\Windows\Microsoft.NET\Framework64.
  • Lecture de la base de registres: si on exécute
    RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\CustomNode");

    Une application 32 bits exécutée sur une machine 32 bits lira la clé dans: HKEY_LOCAL_MACHINE\SOFTWARE\CustomNode. De même une application 64 bits exécutée sur une machine 64 bits lira la clé dans HKEY_LOCAL_MACHINE\SOFTWARE\CustomNode. En revanche, une application 32 bits exécutée sur une machine 64 bits lira la clé dans: HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\CustomNode.

D’autres différences existent, on peut trouver une liste plus exhaustive de ces différences sur les pages suivantes:

Tests

Si on livre des assemblies séparées en 32 ou 64 bits, il est donc important de tester séparement dans toutes les architectures livrées.

Limite de mémoire occupée par le processus

Par défaut, un processus 32 bits ne peut occuper plus de 2 GB en mémoire. Toutefois en ajoutant
IMAGE_FILE_LARGE_ADDRESS_AWARE dans l’entête de l’assembly (voir plus bas), on peut augmenter cette limite à:

  • 3 GB sur un Windows 32 bits
  • 4 GB si le processus 32 bits est exécuté sur un Windows 64 bits.

Par défaut, pour un processus 64 bits, IMAGE_FILE_LARGE_ADDRESS_AWARE est ajouté dans l’entête de l’assembly. La limite est donc de:

  • 8 TB sur une architecture 64 bits classique,
  • 7 TB sur une architecture Itanium,
  • 128 TB si le processus s’exécute sur Windows 8.1 ou Windows Server 2012 R2,
  • 2 GB si IMAGE_FILE_LARGE_ADDRESS_AWARE n’est pas rajouté dans l’entête.

Plus de renseignements sont disponibles sur MSDN.

Comment ajouter IMAGE_FILE_LARGE_ADDRESS_AWARE dans l’entête d’une assembly ?

Pour lever la limite de 2 GB qu’un processus 32 bits peut occuper en mémoire, il faut ajouter IMAGE_FILE_LARGE_ADDRESS_AWARE dans l’entête de l’assembly.
Pour effectuer cette modification, on peut s’aider de l’outil editbin.exe qui est livré avec Visual Studio C++. Il est accessible avec la ligne de commandes Visual Studio. Toutefois on peut y accéder dans le répertoire:

C:\Program Files\Microsoft Visual Studio [version de VS par exemple 10.0]\VC\bin 

Pour ajouter IMAGE_FILE_LARGE_ADDRESS_AWARE dans l’entête d’une assembly, il suffit d’exécuter la commande:

editbin /LARGEADDRESSAWARE [chemin de l'assembly]

Pour supprimer IMAGE_FILE_LARGE_ADDRESS_AWARE de l’entête:

editbin /LARGEADDRESSAWARE:no [chemin de l'assembly]

On peut vérifier que l’entête a été modifié en utilisant dumpbin (voir plus bas pour plus de détails). Il faut exécuter la commande:

dumpbin /headers [chemin de l'assembly]

Dans le résultat, il faut ensuite vérifier la présence d’une ligne: Application can handle large (>2GB) addresses, par exemple:

FILE HEADER VALUES
             14C machine (x86)
               3 number of sections
               0 file pointer to symbol table
               0 number of symbols
              E0 size of optional header
              22 characteristics
                   Executable
                   Application can handle large (>2GB) addresses

Il est possible d’exécuter la commande editbin.exe dans un évènement “post-build” du projet Visual Studio pour que l’entête soit systématiquement modifié à chaque compilation de l’assembly.

Chargement des assemblies

Une incompatiblité dans le choix de la plateforme cible peut mener à des exceptions de type BadImageFormatException lorsque le CLR tente de charger des assemblies incompatibles.

On peut résumer les différents cas possibles dans le tableau suivant:

Architecture du système Windows Architecture du processus Plateformes cible compatibles
Windows 32 bits 32 bits x86, AnyCPU
Windows 64 bits 32 bits x86, AnyCPU
64 bits x64, AnyCPU

Ainsi, on peut remarquer qu’il est possible d’exécuter un processus 32 bits sur une architecture 64 bits. De plus la plateforme cible “AnyCPU” semble être compatible avec toutes les architectures. Ceci ne veut pas dire que le choix de cette plateforme cible ne ménera jamais à une exception lors du chargement. En effet, sur une architecture 64 bits, à la notion de plateforme cible “AnyCPU”, il faut ajouter la préférence de l’architecture du processus.

Préférence de l’architecture pour la plateforme “AnyCPU”

Avant Visual Studio 2010, “AnyCPU” était la plateforme par défaut pour un projet et l’architecture de préférence du processus était 64 bits. Ainsi si on lance un exécutable compilé suivant la plateforme “AnyCPU” sur une architecture 64 bits, il s’exécute dans un processus 64 bits et s’il doit charger une assembly “x86”, une BadImageFormatException se produit car le CLR 64 bits ne pourra pas charger cette assembly. Seules les assemblies compilées suivant les plateformes “AnyCPU” et “x64” peuvent être chargées.

Pour Visual Studio 2010, la plateforme cible par défaut est “x86”. Il n’y a pas de confusion possible puisqu’une assembly compilé suivant cette plateforme, sera forcément exécutée dans un processus 32 bits. Seules les assemblies compilées suivant les plateformes “AnyCPU” et “x86” peuvent être chargées.

Enfin à partir du framework 4.5 et de Visual Studio 2012, il est possible d’indiquer l’architecture de préférence quand on choisit la plateforme cible “AnyCPU”:

  • “AnyCPU” correspond à l’architecture de préférence 64 bits.
  • “Any CPU 32-bit prefered” correspond à l’architecture de préférence 32 bits.

La valeur par défaut est “Any CPU 32-bit prefered”, ce qui signifie que:

  • Sur une architecture 32 bits, l’exécutable s’exécute dans un processus 32 bits et que les assemblies compatibles ont pour plateforme cible “AnyCPU” et “x86”.
  • De même, sur une architecture 64 bits, l’exécutable s’exécute dans un processus 32 bits et que les assemblies compatibles ont pour plateforme cible “AnyCPU” et “x86”.

2. Outils pour vérifier l’entête d’une assembly

Certains outils permettent de connaître la plateforme cible d’une assembly.

Task Manager

Le gestionnaire de tâches permet de voir si un processus exécuté est 32 ou 64 bits. Il suffit de lancer le gestionnaire de tâches suivant l’une des méthodes suivantes:

  • Appuyer simultanément sur les touches Ctrl + Maj + Echap,
  • Clique droit sur la barre de tâches puis cliquer sur “Start Task Manager”,
  • Appuyer sur Windows + E puis taper “taskmgr”.

Dans l’onglet “Processes”, on peut voir la liste des processus:

  • Si le nom du processus est suffixé avec “*32” alors il s’agit d’un processus 32 bits (Exemple: “firefox.exe *32”)
  • Si le nom du processus n’est pas suffixé avec “*32” alors il s’agit d’un processus 64 bits.

Process Explorer

ProcessExplorer est un outils assez puissant qui permet d’indiquer des informations supplémentaires par rapport au Task Manager.

On peut télécharger ProcessExplorer sur: https://technet.microsoft.com/en-us/sysinternals/processexplorer.aspx.

Pour voir la liste des processus, il faut:

  1. Executer ProcessExplorer avec les droits administrateur,
  2. Faire un clique droit sur l’entête des colonnes pour afficher la colonne “Image Type”,
  3. On peut voir directement quels sont les processus 32 et 64 bits

Corflags.exe

Corflags.exe est avec le SDK Windows et est accessible à partir de la ligne de commande Visual Studio.
Le chemin dépend de la version du système, par exemple sur Windows 7 32 bits:

C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\CorFlags.exe

Sur une machine Windows 8.1 64 bits avec le framework 4.5.1:

C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools\CorFlags.exe

En utilisant CorFlags.exe en tapant la ligne suivante, on peut avoir des détails sur l’entête de l’assembly:

CorFlags.exe [chemin de l'assembly]

Framework 4.0

A la version 4.0 du framework, les valeurs qui présentent un intéret sont “PE” et “32BIT”:

Plateforme cible PE 32BIT
AnyCPU PE32 0
x86 PE32 1
x64 PE32+ 0
Itanium PE32+ 0

On peut remarquer que les plateformes cible “x64” et “AnyCPU” donnent le même résultat.

Architecture de préférence par défaut des processus pour la plateforme “AnyCPU”

Pour la version 4.0 du framework, l’architecture de préférence des processus compilé avec la plateforme cible “AnyCPU” est par défaut 64 bits. Ainsi sur une machine 64 bits, l’assembly “AnyCPU” sera exécutée de préférence dans un processus 64 bits.

A partir du framework 4.5

A partir de la version 4.5 du framework et de Visual Studio 2012, dans les options de compilation:

  1. Clique droit sur le projet correspondant à l’assembly
  2. Dans l’onglet “Build”

Le paramètre “Prefer 32-bit” permet d’indiquer l’architecture de préférence des processus dans le cas de la plateforme cible “AnyCPU”. Ainsi sur une machine 64 bits, une assembly avec la plateforme cible “AnyCPU” sera exécutée dans un processus:

  • 32 bits si la case est cochée.
  • 64 bits si la case est décochée.

CorFlags.exe a aussi évolué avec les paramètres “32BITREQ” et “32BITPREF” permettant la préférence de l’architecture:

Plateforme cible Case “Prefer 32-bit” cochée PE 32BITREQ 32BITPREF
x86 N/A PE32 1 0
x64 N/A PE32+ 0 0
AnyCPU Non PE32 0 0
AnyCPU Oui PE32 0 1
Paramètre CLR Header

Attention à ce paramètre qui n’indique pas vraiment la version du framework:

  • Pour les frameworks 1.0 et 1.1, CLR Header = 2.0
  • Pour les frameworks ayant une version supérieure ou égale à 2.0, CLR Header = 2.5.

Plus d’informations à propos de CorFlags.exe sur MSDN.

Dumpbin.exe

Dumpbin est livré avec Visual Studio C++, le chemin de l’exécutable est:

C:\Program Files\Microsoft Visual Studio [version VS]\VC\bin

A partir de la ligne de commandes de Visual Studio, en tapant la ligne suivante, on peut avoir quelques détails sur l’entête de l’assembly:

dumpbin [chemin de l'assembly] /CLRHEADER /HEADERS

Parmi les informations affichées, quelques unes permettent de déterminer l’architecture pour laquelle l’assembly a été compilée.

Par exemple:

FILE HEADER VALUES
             14C machine (x86)
               3 number of sections
        4DFA7751 time date stamp Thu Jun 16 23:36:17 2011
               0 file pointer to symbol table
               0 number of symbols
              E0 size of optional header
            2102 characteristics
                   Executable
                   32 bit word machine
                   DLL

OPTIONAL HEADER VALUES
             10B magic # (PE32)
            8.00 linker version
             800 size of code
[...]

  clr Header:

              48 cb
            2.05 runtime version
            2058 [     5B4] RVA [size] of MetaData Directory
               3 flags
                   IL Only
                   32-Bit Required
               0 entry point token
[...]

La paramètre “machine” permet d’indiquer directement la plateforme cible.

Plus d’informations à propos de Dumpbin sur MSDN.

ILSpy

ILSpy permet d’indiquer directement l’architecture cible d’une assembly ainsi que la version du “runtime”. En cliquant sur le nom de l’assembly, ILSpy affiche des informations contenues dans l’entête de l’assembly.

Plus d’informations à propos d’ILSpy sur: ilspy.net.

3. Résoudre des problèmes de chargement d’assemblies

Dans le cas de BadImageFormatException, parmi toutes les dépendances d’un exécutable, il est parfois difficile d’identifier l’assembly dont l’architecture cible est incompatible avec celle de l’exécutable. Certains outils permettent d’avoir plus d’informations sur les dépendances d’une assembly et de visualiser le détail de chargement des assemblies pour un exécutable donné:
Quelques outils pour résoudre les problèmes de chargement d’assemblies.

Références

Détecter l’architecture d’un exécutable:

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".