NInject, nHibernate и аудит в ASP.NET MVC

Я работаю над унаследованным приложением, которое использует NInject и nHibernate как часть приложения ASP.NET MVC (C #). В данный момент у меня проблема с аудитом модификаций. Каждая сущность имеет поля ChangedOn / ChangedBy и CreatedOn / CreatedBy, которые сопоставляются со столбцами базы данных. Однако они либо заполняются неправильным именем пользователя, либо вообще не имеют имени пользователя. Я думаю, это потому, что он был настроен неправильно, но я недостаточно знаю о nHibernate и NInject, чтобы решить эту проблему, поэтому я надеюсь, что кто-то может помочь. Ниже приведены некоторые фрагменты кода, которые, надеюсь, предоставят достаточное представление о приложении.

Создание фабрики сеансов и сеанса:

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. Он проверяет, является ли имя пользователя пустым или пустым. Если это правда, он пытается найти активного пользователя и заполнить свойство username именем этого пользователя:

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

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

        return _userName;
    }

Но это приводит к тому, что совершенно другое имя пользователя, которое также входит в приложение, регистрируется в базе данных. Я предполагаю, что это связано с тем, что он разрешает неправильного активного пользователя, являющегося последним пользователем, вошедшим в систему, а не пользователем, который начал транзакцию.


person Pieter    schedule 17.08.2011    source источник
comment
Как выглядит код в UserProvider и StamperProvider? SessionFactory настроен как singleton, потому что создание SessionFactory относительно дорого и вам действительно нужен только один. ISession - это сеанс NHibernate, который фактически общается с базой данных и создается для каждого RequestScope, что означает, что вы получаете новый для каждого веб-запроса. Мне интересно, как ваш UserProvider получает текущего пользователя и что делает StamperProvider, чтобы прикрепить его к сущности.   -  person Nathan Ratcliff    schedule 18.08.2011
comment
Натан, если вы немного прокрутите блок кода с сессией и фабрикой сессий, вы также увидите этих двух провайдеров. Я подумал, что это будет важно, но блок кода немного велик, поэтому таблица стилей SO скрывает переполнение с помощью полосы прокрутки.   -  person Pieter    schedule 18.08.2011


Ответы (2)


Обидные части находятся здесь:

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 привязан как единичный. На протяжении всего процесса будет только один. Это довольно типично.

  • ISessionFactory инициализируется параметром SessionFactoryProvider, который немедленно получает экземпляр IStamper и передает его в качестве аргумента константы для инициализации фабрики сеанса.

  • IStamper, в свою очередь, инициализируется StamperProvider, который инициализирует класс Stamper с константой name, установленной на текущего участника / идентификатор пользователя.

Конечным результатом этого является то, что пока процесс жив, каждой «отметке» будет присвоено имя того пользователя, который первым вошел в систему. Это может быть даже анонимный пользователь, что объясняет, почему вы видите так много пустых записей.

Кто бы это ни написал, он понял только половину уравнения. IStamper привязан к области запроса, но он передается в singleton, что означает, что когда-либо будет создан только один IStamper. Вам повезло, что Stamper не содержит ресурсов или финализаторов, иначе вы, вероятно, получите много ObjectDisposedException и других странных ошибок.

Для этого есть три возможных решения:

  1. (Рекомендуется) - Перепишите класс Stamper для поиска текущего пользователя при каждом вызове вместо инициализации статической информацией о пользователе. После этого класс Stamper больше не будет принимать аргументы конструктора. Вы можете привязать IStamper InSingletonScope вместо InRequestScope.

  2. Создайте абстрактный IStamperFactory с GetStamper методом и конкретный StamperFactory, который реализует его, обернув IKernel экземпляр. Свяжите их вместе InSingletonScope. Есть свой бетонный завод return kernel.Get<IStamper>(). Измените фабрику сеанса, чтобы она принимала и удерживала IStamperFactory вместо IStamper. Каждый раз, когда требуется штамп, используйте фабрику для получения нового IStamper экземпляра.

  3. Измените ISessionFactory на InRequestScope. Не рекомендуется.

person Aaronaught    schedule 19.08.2011
comment
Не могли бы вы мне помочь в этом вопросе? stackoverflow.com/questions/20571742/ - person ridermansb; 13.12.2013

Aaronaught, ваш анализ описывает именно то, о чем я подозревал. Однако я обнаружил, что есть четвертое решение, которое, ИМХО, проще и понятнее. Я изменил поставщик сеанса, так что вызов OpenSession принимает экземпляр IInterceptor в качестве аргумента. Как оказалось, прослушиватели событий на самом деле не должны использоваться для аудита (Немного напыщенная речь, но в остальном он прав, по словам Фабио).

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;
    }
}
person Pieter    schedule 22.08.2011
comment
Это очень плохая практика. Зависимости не должны знать о существовании перехватчиков или любой другой системы DI / AOP. ЕСЛИ вы хотите применить этот подход, используйте библиотеку Ninject.Extensions.Interception (желательно реализацию Castle) и добавьте перехватчик к привязке ISession, которая, предположительно, является тем местом, где перехватчик фактически прикрепляется. - person Aaronaught; 23.08.2011