使用 DI 将拦截器添加到遗留代码中的 NHibernate 会话

Using DI to add Interceptor to NHibernate Sessions in legacy code

所以,我正在维护的一些遗留代码中存在错误。它会导致一些轻微的数据损坏,因此相当严重。我找到了根本原因,并制作了一个可以可靠地重现错误的示例应用程序。我想尽可能减少对现有应用程序的影响来修复它,但我正在努力。

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

每当客户端需要新会话时,都会调用 SessionFactory 中的 OpenSession(不是 Nhibernate 的 SessionFactory,而是包装器)。下面的代码显示了 SessionFactory 包装器的相关部分 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事件注入的。下面是拦截器的实现:

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 事件处理程序的助手 class:

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

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

除了数据损坏之外,还存在内存泄漏,因为事件处理程序未注销?这可能是我们的网络服务进程每天至少被回收一次的原因?

看起来 BeforeInit(和 AfterInit)事件确实需要结束。我 可以 更改 OpenSession 方法的签名,并添加一个 IInterceptor 参数。但这会破坏 很多 代码,而且我不想在每次检索会话时都传入拦截器 - 我希望这是透明的。由于拦截器是所有使用 DAL 的应用程序的横切关注点,依赖注入是否是一个可行的解决方案? Unity 用于我们应用程序的其他一些领域。

任何正确方向的推动都将不胜感激:)

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

此实例将从适当的上下文中检索要使用的数据,允许根据 request/user/whatever 适合应用程序隔离此数据。 (System.ServiceModel.OperationContextSystem.Web.HttpContext、...,取决于应用类型。)

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

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

依赖注入示例:

DependencyInjectionInterceptor.cs:

using NHibernate;
using System;
using Microsoft.Extensions.DependencyInjection;

namespace MyAmazingApplication
{
    public class DependencyInjectionInterceptor : EmptyInterceptor
    {
        private readonly IServiceProvider _serviceProvider;

        public DependencyInjectionInterceptor(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public T GetService<T>() => _serviceProvider.GetService<T>();

        public T GetRequiredService<T>() => _serviceProvider.GetRequiredService<T>();
    }
}

Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    ...
    var cfg = new Configuration();
    ... // your config setup
    cfg.SetListeners(NHibernate.Event.ListenerType.PreInsert, new[] { new AuditEventListener() });
    cfg.SetListeners(NHibernate.Event.ListenerType.PreUpdate, new[] { new AuditEventListener() });

    services.AddSingleton(cfg);
    services.AddSingleton(s => s.GetRequiredService<Configuration>().BuildSessionFactory());
    services.AddScoped(s => s.GetRequiredService<ISessionFactory>().WithOptions().Interceptor(new DependencyInjectionInterceptor(s)).OpenSession());

    ... // you other services setup
}

AuditEventListener.cs:

public class AuditEventListener : IPreUpdateEventListener, IPreInsertEventListener
{
    public bool OnPreUpdate(PreUpdateEvent e)
        {
            var user = ((DependencyInjectionInterceptor)e.Session.Interceptor).GetService<ICurrentUser>();

            if (e.Entity is IEntity)
                UpdateAuditTrail(user, e.State, e.Persister.PropertyNames, (IEntity)e.Entity, false);

            return false;
        }
}

所以你使用拦截器来获取你的作用域或任何其他服务: var myService = ((DependencyInjectionInterceptor)e.Session.Interceptor).GetService<IService>();

ICurrentUser 特别是一个范围服务,它使用 HttpContext 获取当前用户。

希望对大家有所帮助