【问题标题】:NInject, nHibernate, and auditing in ASP.NET MVCASP.NET MVC 中的 NInject、nHibernate 和审计
【发布时间】:2011-08-17 09:31:39
【问题描述】:

我正在开发一个继承的应用程序,它使用 NInject 和 nHibernate 作为 ASP.NET MVC (C#) 应用程序的一部分。目前,我正在研究修改审计的问题。每个实体都有 ChangedOn/ChangedBy 和 CreatedOn/CreatedBy 字段,它们映射到数据库列。但是,这些要么填写错误的用户名,要么根本没有用户名。我认为这是因为它的配置方式错误,但是我对nHibernate和NInject的了解还不够,无法解决问题,所以希望有人能提供帮助。下面是一些代码 sn-ps,希望能在应用程序中提供足够的洞察力。

创建会话工厂和会话:

public class NHibernateModule : NinjectModule
{
    public override void Load()
    {
        Bind<ISessionFactory>().ToProvider(new SessionFactoryProvider()).InSingletonScope();

        Bind<ISession>().ToProvider(new SessionProvider()).InRequestScope();
        Bind<INHibernateUnitOfWork>().To<NHibernateUnitOfWork>().InRequestScope();
        Bind<User>().ToProvider(new UserProvider()).InRequestScope();
        Bind<IStamper>().ToProvider(new StamperProvider()).InRequestScope();
    }
}

public class SessionProvider : Provider<ISession>
{
    protected override ISession CreateInstance(IContext context)
    {
        // Create session
        var sessionFactory = context.Kernel.Get<ISessionFactory>();            
        var session = sessionFactory.OpenSession();            
        session.FlushMode = FlushMode.Commit;

        return session;
    }
}

public class SessionFactoryProvider : Provider<ISessionFactory>
{
    protected override ISessionFactory CreateInstance(IContext context)
    {
        var connectionString = ConfigurationManager.ConnectionStrings["DefaultConnectionString"].ToString();
        var stamper = context.Kernel.Get<IStamper>();

        return NHibernateHelper.CreateSessionFactory(connectionString, stamper);
    }
}

public class StamperProvider : Provider<IStamper>
{
    protected override IStamper CreateInstance(IContext context)
    {
        System.Security.Principal.IPrincipal user = HttpContext.Current.User;
        System.Security.Principal.IIdentity identity = user == null ? null : user.Identity;
        string name = identity == null ? "Unknown" : identity.Name;

        return new Stamper(name);
    }
}

public class UserProvider : Provider<User>
{
    protected override UserCreateInstance(IContext context)
    {
        var userRepos = context.Kernel.Get<IUserRepository>();

        System.Security.Principal.IPrincipal user = HttpContext.Current.User;
        System.Security.Principal.IIdentity identity = user == null ? null : user.Identity;
        string name = identity == null ? "" : identity.Name;

        var user = userRepos.GetByName(name);
        return user;
    }
}

配置会话工厂:

public static ISessionFactory CreateSessionFactory(string connectionString, IStamper stamper)
    {
        // Info: http://wiki.fluentnhibernate.org/Fluent_configuration
        return Fluently.Configure()
                .Database(MsSqlConfiguration.MsSql2008
                    .ConnectionString(connectionString))
                .Mappings(m => 
                    {
                        m.FluentMappings
                            .Conventions.Add(PrimaryKey.Name.Is(x => "Id"))
                            .AddFromAssemblyOf<NHibernateHelper>();

                        m.HbmMappings.AddFromAssemblyOf<NHibernateHelper>();
                    })
                // Register 
                .ExposeConfiguration(c => {
                    c.EventListeners.PreInsertEventListeners = 
                        new IPreInsertEventListener[] { new EventListener(stamper) };
                    c.EventListeners.PreUpdateEventListeners =
                        new IPreUpdateEventListener[] { new EventListener(stamper) };
                })
                .BuildSessionFactory();
     }

来自事件监听器的片段:

public bool OnPreInsert(PreInsertEvent e)
{
    _stamper.Insert(e.Entity as IStampedEntity, e.State, e.Persister);
    return false;
}

如您所见,会话工厂位于单例范围内。因此 eventlistener 和 stamper 也在这个范围内被实例化(我认为)。这意味着当用户尚未登录时,压模中的用户名设置为空字符串或“未知”。 我试图通过修改 Stamper 来弥补这个问题。它检查用户名是空还是空。如果这是真的,它会尝试找到活动用户,并用该用户的名称填充用户名属性:

    private string GetUserName()
    {
        if (string.IsNullOrWhiteSpace(_userName))
        {
            var user = ServiceLocator.Resolve<User>();

            if (user != null)
            {
                _userName = user.UserName;
            }
        }

        return _userName;
    }

但这会导致一个完全不同的用户名,该用户名也登录到应用程序,登录到数据库中。我猜这是因为它解决了错误的活动用户,即最后一个登录的用户,而不是启动事务的用户。

【问题讨论】:

  • UserProvider 和 StamperProvider 中的代码是什么样的? SessionFactory 被配置为单例,因为创建 SessionFactory 相对昂贵,而您实际上只需要一个。 ISession 是实际与数据库对话的 NHibernate 会话,它是为每个 RequestScope 创建的,这意味着您会为每个 Web 请求获得一个新会话。我想知道您的 UserProvider 如何获取当前用户以及 StamperProvider 如何将其附加到实体。
  • Nathan,如果您在带有会话和会话工厂的代码块中向下滚动一点,您也可以看到这两个提供程序。我认为这很重要,但是代码块有点大,所以 SO 的样式表用滚动条隐藏了溢出。

标签: asp.net-mvc nhibernate ninject auditing


【解决方案1】:

有问题的部分在这里:

Bind<ISessionFactory>().
    .ToProvider(new SessionFactoryProvider())
    .InSingletonScope();

Bind<IStamper>()
    .ToProvider(new StamperProvider())
    .InRequestScope();

后来:

public class SessionFactoryProvider : Provider<ISessionFactory>
{
    protected override ISessionFactory CreateInstance(IContext context)
    {
        // Unimportant lines omitted
        var stamper = context.Kernel.Get<IStamper>();
        return NHibernateHelper.CreateSessionFactory(connectionString, stamper);
    }
}

public class StamperProvider : Provider<IStamper>
{
    protected override IStamper CreateInstance(IContext context)
    {
        // Unimportant lines omitted
        string name = /* whatever */
        return new Stamper(name);
    }
}

让我们分析一下代码是怎么回事:

  • ISessionFactory 绑定为单实例。在整个过程的生命周期中只会有一个。这是相当典型的。

  • ISessionFactorySessionFactoryProvider 初始化,它立即出去获取IStamper 的实例,并将其作为常量 参数传递以初始化会话工厂。

  • IStamper 依次由 StamperProvider 初始化,该 Stamper 类将 constant name 设置为 current用户主体/身份。

这样做的最终结果是只要进程处于活动状态,每个“标记”都会被分配给第一个登录的用户的名称。这甚至可能是 anonymous 用户,这解释了为什么您会看到这么多空白条目。

写这个的人只猜对了一半。 IStamper 绑定到请求范围,但它被 提供 到一个 singleton,这意味着只会创建一个 IStamper。你很幸运 Stamper 没有任何资源或任何终结器,否则你可能最终会遇到很多 ObjectDisposedException 和其他奇怪的错误。

对此有三种可能的解决方案:

  1. (推荐)- 重写 Stamper 类以在每次调用时查找当前用户,而不是使用静态用户信息进行初始化。之后,Stamper 类将不再接受任何构造函数参数。您可以绑定IStamper InSingletonScope 而不是InRequestScope

  2. 使用GetStamper 方法创建一个抽象IStamperFactory,并通过包装IKernel 实例来实现它的具体StamperFactory。将这些绑定在一起InSingletonScope。有你的混凝土工厂return kernel.Get&lt;IStamper&gt;()。修改会话工厂以接受并持有IStamperFactory 而不是 IStamper每次需要戳,使用工厂获取一个新的IStamper实例。

  3. ISessionFactory 更改为InRequestScope不推荐,因为如果您不使用 DB 生成的身份,它会影响性能并可能会弄乱 ID 生成器,但它解决您的审计问题。

【讨论】:

【解决方案2】:

Aaronaught,你的分析正是我所怀疑的。但是,我发现有第四种解决方案更容易和更直接的恕我直言。 我修改了 sessionprovider,使得对 OpenSession 的调用将 IInterceptor 的实例作为参数。事实证明,事件侦听器实际上不应该用于审计 (a bit of a rant, but other than that he is right, according to Fabio as well)。

AuditInterceptor 实现 OnFlushDirty(用于审计现有实体)和 OnSave(用于审计新创建的实体)。 SessionProvider 如下所示:

public class SessionProvider : Provider<ISession>
{
    protected override ISession CreateInstance(IContext context)
    {
        // Create session
        System.Security.Principal.IPrincipal user = HttpContext.Current.User;
        System.Security.Principal.IIdentity identity = user == null ? null : user.Identity;
        string name = identity == null ? "" : identity.Name;

        var sessionFactory = context.Kernel.Get<ISessionFactory>();
        var session = sessionFactory.OpenSession(new AuditInterceptor(name));            
        session.FlushMode = FlushMode.Commit;

        return session;
    }
}

【讨论】:

  • 这是一个非常糟糕的做法。依赖项不应该知道拦截器或任何其他 DI/AOP 管道的存在。如果您想采用这种方法,请使用 Ninject.Extensions.Interception 库(最好是 Castle 实现)并向 ISession 绑定添加一个拦截器,这可能是拦截器实际连接的位置。
猜你喜欢
  • 1970-01-01
  • 2011-04-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-15
  • 2011-05-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多