使用DI在旧代码中将拦截器添加到NHibernate会话中 [英] Using DI to add Interceptor to NHibernate Sessions in legacy code

查看:56
本文介绍了使用DI在旧代码中将拦截器添加到NHibernate会话中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

因此,我正在维护的一些旧代码中存在错误。它会造成一些轻微的数据损坏,因此相当严重。我找到了根本原因,并制作了一个示例应用程序,该程序可靠地重现了该错误。我想在不影响现有应用程序的情况下对其进行修复,但我很挣扎。

So, there's a bug in some legacy code I'm maintaining. It causes some mild data corruption, so it's rather serious. I've found the root cause, and have made a sample application that reliable reproduces the bug. I would like to fix it with as little impact on existing applications as possible, but I'm struggling.

该错误位于数据访问层。更具体地说,是如何将拦截器注入到新的Nhibernate会话中。在保存或刷新时,拦截器用于设置特定的实体属性。在几乎所有实体上都可以找到属性LoggedInPersonID。所有实体都是使用数据库模式从CodeSmith模板生成的,因此LoggedInPersonID属性对应于在数据库中几乎所有表上都可以找到的列。它与其他几个列和触发器一起用于跟踪哪个用户在数据库中创建和修改了一条记录。任何插入或更新数据的事务都需要提供LoggedInPersonID值,否则事务将失败。

The bug lies in the data access layer. More specifically, in how an interceptor is injected into a new Nhibernate Session. The interceptor is used to set a specific entity property when saving or flushing. The property, LoggedInPersonID, is found on nearly all our entities. All entities are generated from CodeSmith templates using the database schema, so the LoggedInPersonID property corresponds to a column that is found on nearly all tables in the database. Together with a couple of other columns and triggers, it is used to keep track of which user created and modified a record in the database. Any transaction that inserts or updates data need to supply a LoggedInPersonID value, or else the transaction will fail.

每当客户端需要新会话时,都会调用OpenSession在SessionFactory中(不是Nhibernate的SessionFactory,而是包装器)。下面的代码显示了SessionFactory包装器类的相关部分:

Whenever a client requires a new session, a call is made to OpenSession in the SessionFactory (not Nhibernate's SessionFactory, but a wrapper). The code below shows the relevant parts of the SessionFactory wrapper class:

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;
            }
        }
    }
}

拦截器是通过 BeforeInit 事件注入的。下面是拦截器的实现:

The interceptor is injected through the BeforeInit event. Below is the interceptor implementation:

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;
    }
}

以下是应用程序用来注册 BeforeInit 事件处理程序:

Below is a helper class used by applications to register a BeforeInit event handler:

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;
        }
    }
}

该错误在我们的Web服务(WCF SOAP)。 Web服务端点绑定都是 basicHttpBinding 。将为每个客户端请求创建一个新的Nhibernate会话。对客户端进行身份验证后,将调用LoggedInPersonIDInterceptorUtil.Setup方法,并在闭包中捕获经过身份验证的客户端的ID。然后,在另一个客户端请求使用另一个闭包将事件处理程序注册到 BeforeInit 事件的事件处理程序之前,争夺代码触发对 SessionFactory.OpenSession 的调用-因为,它是 BeforeInit 事件调用列表中的最后一个获胜处理程序,有可能返回错误的拦截器。该错误通常发生在两个客户端几乎同时发出请求时,也包括两个客户端客户端以不同的执行时间调用不同的Web服务方法(从身份验证到OpenSession花费的时间比另一种更长。)

The bug is especially prominent in our web services (WCF SOAP). The web services endpoint bindings are all basicHttpBinding. A new Nhibernate session is created for each client request. The LoggedInPersonIDInterceptorUtil.Setup method is called after a client is authenticated, with the authenticated client's ID captured in the closure. Then there's a race to reach code that triggers a call to SessionFactory.OpenSession before another client request registers an event handler to the BeforeInit event with a different closure - because, it's the last handler in the BeforeInit event's invocation list that "wins", potentially returning the wrong interceptor. The bug usually happens when two clients are making requests nearly simultaneously, but also when two clients are calling different web service methods with different execution times (one taking longer from authentication to OpenSession than another).

除了数据损坏外,还存在内存泄漏不取消注册事件处理程序?

In addition to the data corruption, there's also a memory leak as the event handlers aren't de-registered? It might be the reason why our web service process is recycled at least once a day?

它真的看起来像是 BeforeInit (和 AfterInit )事件需要进行。我可以更改OpenSession方法的签名,并添加IInterceptor参数。但这会破坏很多的代码,并且我不想在检索会话时传递拦截器-我希望这是透明的。由于拦截器是所有使用DAL的应用程序中的一个横切关注点,因此依赖注入是否是可行的解决方案?

It really looks like the BeforeInit (and AfterInit) events need to go. I could alter the signature of the OpenSession method, and add an IInterceptor parameter. But this would break a lot of code, and I don't want to pass in an interceptor whenever a session is retrieved - I would like this to be transparent. Since the interceptor is a cross cutting concern in all applications using the DAL, would dependency injection be a viable solution? Unity is used in some other areas of our applications.

任何朝着正确方向轻推的人将不胜感激:)

Any nudge in the right direction would be greatly appreciated :)

推荐答案

而不是在每个 ISessionFactory.OpenSession 调用中都提供拦截器,而是使用全局配置的单个拦截器实例( Configuration.SetInterceptor())。

Instead of supplying the interceptor at each ISessionFactory.OpenSession call, I would use a single interceptor instance globally configured (Configuration.SetInterceptor()).

此实例将从足够的上下文中检索要使用的数据,从而隔离该数据每个请求/用户/适合任何应用程序的条件。
System.ServiceModel.OperationContext System.Web.HttpContext ,...,取决于

This instance would retrieve the data to use from an adequate context allowing to isolate this data per request/user/whatever suits the application. (System.ServiceModel.OperationContext, System.Web.HttpContext, ..., depending on the application kind.)

您案例中的上下文数据将在当前调用 LoggedInPersonIDInterceptorUtil.Setup 的位置进行设置。

The context data in your case would be set where LoggedInPersonIDInterceptorUtil.Setup is currently called.

如果需要为需要不同上下文的应用程序使用相同的拦截器实现,则需要根据要添加(或注入)的某些配置参数选择要使用的上下文作为拦截器中的依赖项。

If you need to use the same interceptor implementation for applications requiring different contextes, then you will need to choose the context to use according to some configuration parameter you would add (or inject it as a dependency in your interceptor).

这篇关于使用DI在旧代码中将拦截器添加到NHibernate会话中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆