2017-04-25 1 views
1

Donc, il y a un bug dans un code hérité que je maintiens. Cela provoque une légère corruption des données, donc c'est plutôt sérieux. J'ai trouvé la cause première, et ai fait un exemple d'application fiable qui reproduit le bogue. Je voudrais le réparer avec le moins d'impact possible sur les applications existantes, mais je me bats.Utilisation de DI pour ajouter Interceptor aux sessions NHibernate dans le code existant

Le problème réside dans la couche d'accès aux données. Plus précisément, dans la façon dont un intercepteur est injecté dans une nouvelle session de Nhibernate. L'intercepteur est utilisé pour définir une propriété d'entité spécifique lors de la sauvegarde ou du vidage. La propriété, LoggedInPersonID, est présente sur presque toutes nos entités. Toutes les entités sont générées à partir des modèles CodeSmith à l'aide du schéma de la base de données, de sorte que la propriété LoggedInPersonID correspond à une colonne qui se trouve sur presque toutes les tables de la base de données. Avec quelques autres colonnes et déclencheurs, il est utilisé pour suivre l'utilisateur qui a créé et modifié un enregistrement dans la base de données. Toute transaction qui insère ou met à jour des données doit fournir une valeur LoggedInPersonID, sinon la transaction échouera.

Chaque fois qu'un client requiert une nouvelle session, un appel est effectué à OpenSession dans la SessionFactory (pas SessionFactory de Nhibernate, mais un wrapper). Le code ci-dessous montre les parties pertinentes de la classe wrapper SessionFactory:

public class SessionFactory 
{ 
    private ISessionFactory sessionFactory; 

    private SessionFactory() 
    { 
     Init(); 
    } 

    public static SessionFactory Instance 
    { 
     get 
     { 
      return Nested.SessionFactory; 
     } 
    } 

    private static readonly object _lock = new object(); 

    public ISession OpenSession() 
    { 
     lock (_lock) 
     { 
      var beforeInitEventArgs = new SessionFactoryOpenSessionEventArgs(null); 

      if (BeforeInit != null) 
      { 
       BeforeInit(this, beforeInitEventArgs); 
      } 

      ISession session; 

      if (beforeInitEventArgs.Interceptor != null 
       && beforeInitEventArgs.Interceptor is IInterceptor) 
      { 
       session = sessionFactory.OpenSession(beforeInitEventArgs.Interceptor); 
      } 
      else 
      { 
       session = sessionFactory.OpenSession(); 
      } 

      return session; 
     } 
    } 

    private void Init() 
    { 
     try 
     { 
      var configuration = new Configuration().Configure(); 
      OnSessionFactoryConfiguring(configuration); 
      sessionFactory = configuration.BuildSessionFactory(); 
     } 
     catch (Exception ex) 
     { 
      Console.Error.WriteLine(ex.Message); 
      while (ex.InnerException != null) 
      { 
       Console.Error.WriteLine(ex.Message); 
       ex = ex.InnerException; 
      } 
      throw; 
     } 
    } 

    private void OnSessionFactoryConfiguring(Configuration configuration) 
    { 
     if(SessionFactoryConfiguring != null) 
     { 
      SessionFactoryConfiguring(this, new SessionFactoryConfiguringEventArgs(configuration)); 
     } 
    } 

    public static event EventHandler<SessionFactoryOpenSessionEventArgs> BeforeInit; 
    public static event EventHandler<SessionFactoryOpenSessionEventArgs> AfterInit; 
    public static event EventHandler<SessionFactoryConfiguringEventArgs> SessionFactoryConfiguring; 

    public class SessionFactoryConfiguringEventArgs : EventArgs 
    { 
     public Configuration Configuration { get; private set; } 

     public SessionFactoryConfiguringEventArgs(Configuration configuration) 
     { 
      Configuration = configuration; 
     } 
    } 

    public class SessionFactoryOpenSessionEventArgs : EventArgs 
    { 

     private NHibernate.ISession session; 

     public SessionFactoryOpenSessionEventArgs(NHibernate.ISession session) 
     { 
      this.session = session; 
     } 

     public NHibernate.ISession Session 
     { 
      get 
      { 
       return this.session; 
      } 
     } 

     public NHibernate.IInterceptor Interceptor 
     { 
      get; 
      set; 
     } 
    } 

    /// <summary> 
    /// Assists with ensuring thread-safe, lazy singleton 
    /// </summary> 
    private class Nested 
    { 
     internal static readonly SessionFactory SessionFactory; 

     static Nested() 
     { 
      try 
      { 
       SessionFactory = new SessionFactory(); 
      } 
      catch (Exception ex) 
      { 
       Console.Error.WriteLine(ex); 
       throw; 
      } 
     } 
    } 
} 

L'intercepteur est injecté à travers le BeforeInit événement. Ci-dessous la mise en œuvre des intercepteurs:

public class LoggedInPersonIDInterceptor : NHibernate.EmptyInterceptor 
{ 
    private int? loggedInPersonID 
    { 
     get 
     { 
      return this.loggedInPersonIDProvider(); 
     } 
    } 

    private Func<int?> loggedInPersonIDProvider; 

    public LoggedInPersonIDInterceptor(Func<int?> loggedInPersonIDProvider) 
    { 
     SetProvider(loggedInPersonIDProvider); 
    } 

    public void SetProvider(Func<int?> provider) 
    { 
     loggedInPersonIDProvider = provider; 
    } 

    public override bool OnFlushDirty(object entity, object id, object[] currentState, object[] previousState, 
             string[] propertyNames, NHibernate.Type.IType[] types) 
    { 
     return SetLoggedInPersonID(currentState, propertyNames); 
    } 

    public override bool OnSave(object entity, object id, object[] currentState, 
          string[] propertyNames, NHibernate.Type.IType[] types) 
    { 
     return SetLoggedInPersonID(currentState, propertyNames); 
    } 

    protected bool SetLoggedInPersonID(object[] currentState, string[] propertyNames) 
    { 
     int max = propertyNames.Length; 

     var lipid = loggedInPersonID; 

     for (int i = 0; i < max; i++) 
     { 
      if (propertyNames[i].ToLower() == "loggedinpersonid" && currentState[i] == null && lipid.HasValue) 
      { 
       currentState[i] = lipid; 

       return true; 
      } 
     } 

     return false; 
    } 
} 

Voici une classe d'aide utilisée par les applications pour enregistrer un gestionnaire d'événements BeforeInit :

public static class LoggedInPersonIDInterceptorUtil 
    { 
     public static LoggedInPersonIDInterceptor Setup(Func<int?> loggedInPersonIDProvider) 
     { 
      var loggedInPersonIdInterceptor = new LoggedInPersonIDInterceptor(loggedInPersonIDProvider); 

      ShipRepDAL.ShipRepDAO.SessionFactory.BeforeInit += (s, args) => 
      {  
       args.Interceptor = loggedInPersonIdInterceptor; 
      }; 

      return loggedInPersonIdInterceptor; 
     } 
    } 
} 

Le bug est particulièrement important dans nos services Web (WCF SOAP) . Les liaisons de point de terminaison des services Web sont toutes basicHttpBinding. Une nouvelle session Nhibernate est créée pour chaque requête client. La méthode LoggedInPersonIDInterceptorUtil.Setup est appelée après l'authentification d'un client, avec l'ID du client authentifié capturé dans la fermeture. Ensuite, il y a une course pour atteindre le code qui déclenche un appel à sessionFactory.openSession avant une autre demande client enregistre un gestionnaire d'événements à l'BeforeInit événement avec une fermeture différente - parce que, il est le dernier gestionnaire dans l'invocation du BeforeInit événement liste qui "gagne", renvoyant potentiellement le mauvais intercepteur. Le bogue se produit généralement lorsque deux clients effectuent des requêtes presque simultanément, mais également lorsque deux clients appellent des méthodes de service Web différentes avec des temps d'exécution différents (l'un prend plus de temps de l'authentification à OpenSession qu'un autre).

En plus de la corruption de données, il existe également une fuite de mémoire car les gestionnaires d'événements ne sont pas désenregistrés? C'est peut-être la raison pour laquelle notre processus de service Web est recyclé au moins une fois par jour?

Il ressemble vraiment la BeforeInit (et afterinit) les événements doivent aller. I pourrait modifier la signature de la méthode OpenSession, et ajouter un paramètre IInterceptor. Mais cela casserait beaucoup de code, et je ne veux pas passer dans un intercepteur chaque fois qu'une session est récupérée - je voudrais que cela soit transparent. Puisque l'intercepteur est un problème transversal dans toutes les applications utilisant le DAL, l'injection de dépendance serait-elle une solution viable? Unity est utilisé dans d'autres domaines de nos applications.

Tout petit coup de pouce dans la bonne direction serait grandement apprécié :)

Répondre

2

Au lieu de fournir l'intercepteur à chaque appel ISessionFactory.OpenSession, je voudrais utiliser une seule instance d'intercepteurs globalement configuré (Configuration.SetInterceptor()).

Cette instance récupèrerait les données à utiliser dans un contexte adéquat permettant d'isoler ces données par requête/utilisateur/tout ce qui convient à l'application. (System.ServiceModel.OperationContext, System.Web.HttpContext, ..., en fonction du type d'application.)

Les données de contexte dans votre cas où seraient fixés LoggedInPersonIDInterceptorUtil.Setup actuellement appelé. Si vous devez utiliser la même implémentation d'intercepteur pour des applications nécessitant des contextes différents, vous devrez choisir le contexte à utiliser en fonction de certains paramètres de configuration que vous ajouterez (ou l'injecter en tant que dépendance dans votre intercepteur).

+0

Cela semble être une approche prometteuse! Merci pour l'idée, je vais la tester demain et la marquer comme la réponse si ça marche :) – matsho