web-dev-qa-db-fra.com

Peut-on obliger Unity à ne pas lancer SynchronizationLockException tout le temps?

Le conteneur d'injection de dépendance Unity a ce qui semble être un problème largement connu dans lequel SynchronizedLifetimeManager oblige souvent la méthode Monitor.Exit à envoyer une exception SynchronizationLockException qui est ensuite interceptée et ignorée. C'est un problème pour moi parce que j'aime bien déboguer avec Visual Studio configuré pour interrompre toute exception levée. Par conséquent, chaque fois que mon application démarre, j'interromps cette exception plusieurs fois sans raison.

Comment puis-je empêcher cette exception d'être levée?

Chaque fois que ce problème est mentionné ailleurs sur le Web, le conseil consiste généralement à modifier les paramètres du débogueur pour l’ignorer. Cela revient à aller chez le médecin et à dire: «Docteur, docteur, mon bras me fait mal au bras quand je le lève», à quoi on lui dit: «Eh bien, arrête de le lever. Je cherche une solution qui empêche l’exception d’être levée.

L'exception se produit dans la méthode SetValue car elle suppose que GetValue aura été appelé en premier, où Monitor.Enter est appelé. Toutefois, les classes LifetimeStrategy et UnityDefaultBehaviorExtension appellent régulièrement SetValue sans appeler GetValue.

Je préférerais ne pas avoir à modifier le code source et à maintenir ma propre version de Unity. J'espère donc une solution permettant d'ajouter une combinaison d'extensions, de stratégies ou de stratégies au conteneur qui garantira que, si le Le gestionnaire de durée de vie est un SynchronizedLifetimeManager, GetValue est toujours appelé avant toute autre chose.

64
Rory MacLeod

Je suis certain que le code peut être appelé de différentes manières par SynchronizedLifetimeManager ou par un descendant tel que ContainerControlledLifetimeManager, mais deux scénarios en particulier me causaient des problèmes.

La première était de ma faute: j'utilisais l'injection de constructeur pour fournir une référence au conteneur. Dans ce constructeur, j'ajoutais également la nouvelle instance de la classe au conteneur pour une utilisation ultérieure. Cette approche en arrière a eu pour effet de changer le gestionnaire de durée de vie de Transient à ContainerControlled, de sorte que l'objet Unity appelé GetValue sur n'était pas le même objet sur lequel il s'appelait SetValue. La leçon à retenir est ne faites rien pendant la construction qui pourrait changer le gestionnaire de durée de vie d'un objet.

Le deuxième scénario était que chaque fois que RegisterInstance est appelé, UnityDefaultBehaviorExtension appelle SetValue sans appeler GetValue auparavant. Heureusement, Unity est suffisamment extensible pour pouvoir contourner le problème avec suffisamment de sang-mêlé.

Commencez avec une nouvelle extension de comportement comme celle-ci:

/// <summary>
/// Replaces <see cref="UnityDefaultBehaviorExtension"/> to eliminate 
/// <see cref="SynchronizationLockException"/> exceptions that would otherwise occur
/// when using <c>RegisterInstance</c>.
/// </summary>
public class UnitySafeBehaviorExtension : UnityDefaultBehaviorExtension
{
    /// <summary>
    /// Adds this extension's behavior to the container.
    /// </summary>
    protected override void Initialize()
    {
        Context.RegisteringInstance += PreRegisteringInstance;

        base.Initialize();
    }

    /// <summary>
    /// Handles the <see cref="ExtensionContext.RegisteringInstance"/> event by
    /// ensuring that, if the lifetime manager is a 
    /// <see cref="SynchronizedLifetimeManager"/> that its 
    /// <see cref="SynchronizedLifetimeManager.GetValue"/> method has been called.
    /// </summary>
    /// <param name="sender">The object responsible for raising the event.</param>
    /// <param name="e">A <see cref="RegisterInstanceEventArgs"/> containing the
    /// event's data.</param>
    private void PreRegisteringInstance(object sender, RegisterInstanceEventArgs e)
    {
        if (e.LifetimeManager is SynchronizedLifetimeManager)
        {
            e.LifetimeManager.GetValue();
        }
    }
}

Ensuite, vous avez besoin d'un moyen de remplacer le comportement par défaut. Unity n'a pas de méthode pour supprimer une extension spécifique, vous devez donc tout supprimer et réinstaller les autres extensions:

public static IUnityContainer InstallCoreExtensions(this IUnityContainer container)
{
    container.RemoveAllExtensions();
    container.AddExtension(new UnityClearBuildPlanStrategies());
    container.AddExtension(new UnitySafeBehaviorExtension());

#pragma warning disable 612,618 // Marked as obsolete, but Unity still uses it internally.
    container.AddExtension(new InjectedMembers());
#pragma warning restore 612,618

    container.AddExtension(new UnityDefaultStrategiesExtension());

    return container;
}

Notez que UnityClearBuildPlanStrategies? RemoveAllExtensions efface toutes les listes internes de stratégies et de stratégies du conteneur, à l'exception d'une seule. J'ai donc dû utiliser une autre extension pour éviter d'insérer des doublons lorsque j'ai restauré les extensions par défaut:

/// <summary>
/// Implements a <see cref="UnityContainerExtension"/> that clears the list of 
/// build plan strategies held by the container.
/// </summary>
public class UnityClearBuildPlanStrategies : UnityContainerExtension
{
    protected override void Initialize()
    {
        Context.BuildPlanStrategies.Clear();
    }
}

Maintenant, vous pouvez utiliser RegisterInstance en toute sécurité sans craindre d’être conduit au bord de la folie. Juste pour être sûr, voici quelques tests:

[TestClass]
public class UnitySafeBehaviorExtensionTests : ITest
{
    private IUnityContainer Container;
    private List<Exception> FirstChanceExceptions;

    [TestInitialize]
    public void TestInitialize()
    {
        Container = new UnityContainer();
        FirstChanceExceptions = new List<Exception>();
        AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionRaised;
    }

    [TestCleanup]
    public void TestCleanup()
    {
        AppDomain.CurrentDomain.FirstChanceException -= FirstChanceExceptionRaised;
    }

    private void FirstChanceExceptionRaised(object sender, FirstChanceExceptionEventArgs e)
    {
        FirstChanceExceptions.Add(e.Exception);
    }

    /// <summary>
    /// Tests that the default behavior of <c>UnityContainer</c> leads to a <c>SynchronizationLockException</c>
    /// being throw on <c>RegisterInstance</c>.
    /// </summary>
    [TestMethod]
    public void UnityDefaultBehaviorRaisesExceptionOnRegisterInstance()
    {
        Container.RegisterInstance<ITest>(this);

        Assert.AreEqual(1, FirstChanceExceptions.Count);
        Assert.IsInstanceOfType(FirstChanceExceptions[0], typeof(SynchronizationLockException));
    }

    /// <summary>
    /// Tests that <c>UnitySafeBehaviorExtension</c> protects against <c>SynchronizationLockException</c>s being
    /// thrown during calls to <c>RegisterInstance</c>.
    /// </summary>
    [TestMethod]
    public void SafeBehaviorPreventsExceptionOnRegisterInstance()
    {
        Container.RemoveAllExtensions();
        Container.AddExtension(new UnitySafeBehaviorExtension());
        Container.AddExtension(new InjectedMembers());
        Container.AddExtension(new UnityDefaultStrategiesExtension());

        Container.RegisterInstance<ITest>(this);

        Assert.AreEqual(0, FirstChanceExceptions.Count);
    }
}

public interface ITest { }
38
Rory MacLeod

Corrigé dans la dernière version de Unity (2.1.505.2). Obtenez-le via NuGet.

12
Grigori Melnik

La réponse à votre question est malheureusement non. J'ai suivi cette question avec l'équipe de développement ici au sein du groupe de modèles et pratiques de Microsoft (j'étais jusqu'à récemment le responsable du développement) et nous avions ce bogue à considérer pour EntLib 5.0. Nous avons fait des recherches et avons conclu que cela était dû à des interactions inattendues entre notre code et le débogueur. Nous avons envisagé une solution, mais celle-ci s'est avérée plus complexe que le code existant. À la fin, cela a été priorisé en dessous d’autres choses et n’a pas fait la barre des 5.

Désolé je n'ai pas de meilleure réponse pour vous. Si cela peut vous consoler, je le trouve également irritant.

10
Ade Miller

J'utilise cette solution courte:

/// <summary>
/// KVV 20110502
/// Fix for bug in Unity throwing a synchronizedlockexception at each register
/// </summary>
class LifeTimeManager : ContainerControlledLifetimeManager
{
    protected override void SynchronizedSetValue(object newValue)
    {
        base.SynchronizedGetValue();
        base.SynchronizedSetValue(newValue);
    }
}

et l'utiliser comme ça:

private UnityContainer _container;
...
_container.RegisterInstance(instance, new LifeTimeManager());

le problème est que la classe de base de ContainerControlledLifetimeManager s'attend à ce que SynchronizedSetValue fasse un monitor.Enter () via la base.GetValue, mais la classe ContainerControlledLifetimeManager ne le fait pas (apparemment, ses développeurs n'avaient pas l'option "break at exception"?) .

cordialement, Koen

7
Koen VV

La solution de Rory est géniale - merci. Résolution d'un problème qui m'ennuie tous les jours! 

    public static void ReplaceBehaviourExtensionsWithSafeExtension(IUnityContainer container)
    {
        var extensionsField = container.GetType().GetField("extensions", BindingFlags.Instance | BindingFlags.NonPublic);
        var extensionsList = (List<UnityContainerExtension>)extensionsField.GetValue(container);
        var existingExtensions = extensionsList.ToArray();
        container.RemoveAllExtensions();
        container.AddExtension(new UnitySafeBehaviorExtension());
        foreach (var extension in existingExtensions)
        {
            if (!(extension is UnityDefaultBehaviorExtension))
            {
                container.AddExtension(extension);
            }
        }
    }
4
Zubin Appoo

Attention à une erreur dans la réponse de Zubin Appoo: il y a UnityClearBuildPlanStrategies il manque dans son code. 

Le bon extrait de code est:

FieldInfo extensionsField = container.GetType().GetField("extensions", BindingFlags.Instance | BindingFlags.NonPublic);
List<UnityContainerExtension> extensionsList = (List<UnityContainerExtension>)extensionsField.GetValue(container);
UnityContainerExtension[] existingExtensions = extensionsList.ToArray();
container.RemoveAllExtensions();
container.AddExtension(new UnityClearBuildPlanStrategiesExtension());
container.AddExtension(new UnitySafeBehaviorExtension());

foreach (UnityContainerExtension extension in existingExtensions)
{
   if (!(extension is UnityDefaultBehaviorExtension))
   {
       container.AddExtension(extension);
   }
}
1
user556903

Unity 2.1 - Mise à jour d'août 2012 corrige le bogue

  1. Résolution d'un problème de sécurité des threads: http://unity.codeplex.com/discussions/328841

  2. Amélioration de l'expérience de débogage sur System.Threading.SynchronizationLockException: https://entlib.uservoice.com/forums/89245-general/suggestions/2377307-fix-the-system-threading-synchronizationlockexcep

  3. Amélioration de l'expérience de débogage grâce à une meilleure messagerie d'erreur lorsqu'un type ne peut pas être chargé: http://unity.codeplex.com/workitem/9223

  4. Prise en charge d'un scénario d'exécution de BuildUp () sur une instance existante d'une classe dépourvue de constructeur public: http://unity.codeplex.com/workitem/9460

Pour rendre l'expérience de mise à jour aussi simple que possible pour les utilisateurs et éviter le recours aux redirections de liaison avec Assembly, nous avons choisi d'incrémenter uniquement la version du fichier Assembly, et non la version .NET Assembly.

0
huoxudong125