使用 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.OperationContext
、System.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 获取当前用户。
希望对大家有所帮助
所以,我正在维护的一些遗留代码中存在错误。它会导致一些轻微的数据损坏,因此相当严重。我找到了根本原因,并制作了一个可以可靠地重现错误的示例应用程序。我想尽可能减少对现有应用程序的影响来修复它,但我正在努力。
问题出在数据访问层。更具体地说,是如何将拦截器注入到新的 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.OperationContext
、System.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 获取当前用户。
希望对大家有所帮助