NInject, nHibernate и аудит в ASP.NET MVC - PullRequest
1 голос
/ 17 августа 2011

Я работаю над унаследованным приложением, которое использует 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;
    }

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

Ответы [ 2 ]

4 голосов
/ 20 августа 2011

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

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 и передает его в качестве аргумента constant для инициализации фабрики сеанса.

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

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

Кто бы ни написал это, получил только половину правильного уравнения. 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. Не рекомендуется , потому что это ухудшит производительность и может испортить генераторы идентификаторов, если вы не будете использовать идентификаторы, сгенерированные БД, но это решит вашу проблему аудита.

1 голос
/ 22 августа 2011

Аарона, твой анализ описывает именно то, что я подозревал. Тем не менее, я обнаружил, что есть четвертое решение, которое проще и понятнее, ИМХО. Я изменил sessionprovider, так что вызов 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;
    }
}
...