Регистрация каждого изменения данных с помощью Entity Framework

Заказчик должен регистрировать каждое изменение данных в таблице регистрации с фактическим пользователем, который внес изменение. Приложение использует одного пользователя SQL для доступа к базе данных, но нам нужно зарегистрировать «настоящий» идентификатор пользователя.

Мы можем сделать это в t-sql, написав триггеры для каждой вставки и обновления таблицы и используя context_info для хранения идентификатора пользователя. Мы передали идентификатор пользователя в хранимую процедуру, сохранили идентификатор пользователя в contextinfo, и триггер может использовать эту информацию для записи строк журнала в таблицу журнала.

Я не могу найти место или способ, где и как я могу сделать что-то подобное с помощью EF. Итак, основная цель: если я внесу изменение в данные через EF, я хотел бы записать точное изменение данных в таблицу полуавтоматическим способом (поэтому я не хочу проверять каждое поле на наличие изменений перед сохранение объекта). Мы используем EntitySQL.

К сожалению, мы должны придерживаться SQL 2000, поэтому функция отслеживания изменений данных, представленная в SQL2008, не является вариантом (но, возможно, это тоже не правильный путь для нас).

Есть идеи, ссылки или отправные точки?

[Редактировать] Некоторые примечания: используя обработчик событий ObjectContext.SavingChanges, я могу получить точку, в которой я могу ввести оператор SQL для инициализации contextinfo. Однако я не могу смешивать EF и стандартный SQL. Таким образом, я могу получить EntityConnection, но не могу выполнить с его помощью оператор T-SQL. Или я могу получить строку подключения EntityConnection и создать на ее основе SqlConnection, но это будет другое соединение, поэтому contextinfo не повлияет на сохранение, сделанное EF.

Я пробовал в обработчике SavingChanges следующее:

testEntities te = (testEntities)sender;
DbConnection dc = te.Connection;
DbCommand dcc = dc.CreateCommand();
dcc.CommandType = CommandType.StoredProcedure;
DbParameter dp = new EntityParameter();
dp.ParameterName = "userid";
dp.Value = textBox1.Text;
dcc.CommandText = "userinit";
dcc.Parameters.Add(dp);
dcc.ExecuteNonQuery();

Ошибка: значение EntityCommand.CommandText недопустимо для команды StoredProcedure. То же самое с SqlParameter вместо EntityParameter: нельзя использовать SqlParameter.

StringBuilder cStr = new StringBuilder("declare @tx char(50); set @tx='");
cStr.Append(textBox1.Text);
cStr.Append("'; declare @m binary(128); set @m = cast(@tx as binary(128)); set context_info @m;");

testEntities te = (testEntities)sender;
DbConnection dc = te.Connection;
DbCommand dcc = dc.CreateCommand();
dcc.CommandType = CommandType.Text;
dcc.CommandText = cStr.ToString();
dcc.ExecuteNonQuery();

Ошибка: неверный синтаксис запроса.

Итак, я застрял, чтобы создать мост между Entity Framework и ADO.NET. Если я смогу заставить его работать, я опубликую доказательство концепции.

Серьезно, пользователь хочет, чтобы вы использовали EF, а затем требует, чтобы вы придерживались SQL 2000?

Jason Short 22.08.2009 23:43

Есть интересный пост по поводу аудита здесь, как вы думаете?

ibiza 25.01.2013 22:41

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

Rory 14.10.2016 02:32

Хорошая ясная запись здесь davecallan.com/passing-userid-delete-trigger-entity-framewor‌ k / # с использованием транзакционного подхода для обеспечения того же соединения.

Rory 08.11.2016 18:11
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
29
4
21 790
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

Ответ принят как подходящий

Как насчет обработки Context.Сохранение изменений?

Ага, вот чего я бы хотел избежать. :-) Было бы неплохо справиться со всем этим в автоматическом режиме. У нас уже есть триггер-генератор для обработки части регистрации. Недостающее звено в том, что мы не можем передать идентификатор пользователя триггеру.

Biri 17.11.2008 20:23

Вы не можете использовать SavingChanges для установки идентификатора пользователя в контекстной информации? msdn.microsoft.com/en-us/library/ms187768.aspx

Craig Stuntz 17.11.2008 20:31

Вот дерьмо. Самое простое решение, и мы думали о чем-то очень сложном. Спасибо, что открыли мне глаза.

Biri 17.11.2008 20:42

Извините, мне нужно отозвать принятый статус, потому что он не работает. Соединение закрывается (отдельные объекты), и если я открываю новое соединение и заполняю context_info, это не влияет на соединение, открытое во время сохранения. :-(

Biri 18.11.2008 15:41

Вы можете предоставить собственное соединение для использования EF. См .: msdn.microsoft.com/en-us/library/bb738540.aspx

Craig Stuntz 18.11.2008 16:16

Да, но вы не можете выполнять стандартные операторы sql с помощью EntityConnection, и вы не можете преобразовать EntityConnection в SqlConnection, поэтому я не могу смешивать эти два метода.

Biri 18.11.2008 16:51

EntityConnection - это DbConnection, поэтому вы должны иметь возможность вызывать CreateDbCommand. Я не пробовал, но в документации это перечислено и не сказано, чтобы вы этого не делали.

Craig Stuntz 18.11.2008 18:48

Да, можно, но что с этим делать? Я не могу запустить t-sql. Я предоставлю вам больше примеров в исходном вопросе.

Biri 19.11.2008 11:25

Хммм ... Вроде надо использовать StoreConnection (см. Ссылку). Не могли бы вы воспользоваться процедурой? Нравится? blogs.msdn.com/meek/archive/2008/03/26/…

Craig Stuntz 19.11.2008 17:28

Вы пробовали добавить хранимую процедуру в свою модель сущности?

Да, я сделал. Крейг указал в правильном направлении, поэтому я опубликую POC следующим.

Biri 20.11.2008 11:25

Наконец, с помощью Крейга, вот доказательство концепции. Требуется дополнительное тестирование, но, на первый взгляд, он работает.

Во-первых: я создал две таблицы, одну для данных, другую для регистрации.

-- This is for the data
create table datastuff (
    id int not null identity(1, 1),
    userid nvarchar(64) not null default(''),
    primary key(id)
)
go

-- This is for the log
create table naplo (
    id int not null identity(1, 1),
    userid nvarchar(64) not null default(''),
    datum datetime not null default('2099-12-31'),
    primary key(id)
)
go

Во-вторых: создать триггер для вставки.

create trigger myTrigger on datastuff for insert as

    declare @User_id int,
        @User_context varbinary(128),
        @User_id_temp varchar(64)

    select @User_context = context_info
        from master.dbo.sysprocesses
        where spid=@@spid

    set @User_id_temp = cast(@User_context as varchar(64))

    declare @insuserid nvarchar(64)

    select @insuserid=userid from inserted

    insert into naplo(userid, datum)
        values(@User_id_temp, getdate())

go

Вы также должны создать триггер для обновления, который будет немного сложнее, потому что он должен проверять каждое поле на предмет измененного содержимого.

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

В-третьих: создайте хранимую процедуру, которая подставляет идентификатор пользователя в контекстную информацию SQL.

create procedure userinit(@userid varchar(64))
as
begin
    declare @m binary(128)
    set @m = cast(@userid as binary(128))
    set context_info @m
end
go

Мы готовы со стороной SQL. А вот и часть C#.

Создайте проект и добавьте в проект EDM. EDM должен содержать таблицу данных (или таблицы, которые необходимо отслеживать на предмет изменений) и SP.

Теперь сделайте что-нибудь с объектом сущности (например, добавьте новый объект данных) и подключитесь к событию SavingChanges.

using (testEntities te = new testEntities())
{
    // Hook to the event
    te.SavingChanges += new EventHandler(te_SavingChanges);

    // This is important, because the context info is set inside a connection
    te.Connection.Open();

    // Add a new datastuff
    datastuff ds = new datastuff();

    // This is coming from a text box of my test form
    ds.userid = textBox1.Text;
    te.AddTodatastuff(ds);

    // Save the changes
    te.SaveChanges(true);

    // This is not needed, only to make sure
    te.Connection.Close();
}

Внутри SavingChanges мы вводим наш код, чтобы установить контекстную информацию соединения.

// Take my entity
testEntities te = (testEntities)sender;

// Get it's connection
EntityConnection dc = (EntityConnection )te.Connection;

// This is important!
DbConnection storeConnection = dc.StoreConnection;

// Create our command, which will call the userinit SP
DbCommand command = storeConnection.CreateCommand();
command.CommandText = "userinit";
command.CommandType = CommandType.StoredProcedure;

// Put the user id as the parameter
command.Parameters.Add(new SqlParameter("userid", textBox1.Text));

// Execute the command
command.ExecuteNonQuery();

Поэтому перед сохранением изменений мы открываем соединение объекта, внедряем наш код (не закрывайте соединение в этой части!) И сохраняем наши изменения.

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

Закрытие соединения важно из-за System.ArgumentException: EntityConnection can only be constructed with a closed DbConnection.

Jan 'splite' K. 20.09.2011 15:06

Нам пришлось решить эту проблему по-другому.

  • Наследовать класс от созданного вами класса контейнера сущностей
  • Сделайте базовый класс сущности абстрактным. Вы можете сделать это путем частичного определения класса в отдельном файле.
  • В унаследованном классе скройте метод SavingChanges своим собственным, используя ключевое слово new в определении метода.
  • В вашем методе SavingChanges:

    1. a, открыть соединение с сущностью
    2. выполнить хранимую процедуру контекста пользователя с помощью ebtityclient
    3. вызов base.SaveChanges ()
    4. закрыть соединение

Тогда в вашем коде вы должны использовать унаследованный класс.

Спасибо, что указали мне правильное направление. Однако в моем случае мне также нужно установить контекстную информацию при выполнении операторов выбора, потому что я запрашиваю представления, которые используют контекстную информацию для управления безопасностью на уровне строк пользователем.

Мне показалось, что проще всего присоединиться к событию StateChanged соединения и просто наблюдать за изменением состояния с закрытого на открытое. Затем я вызываю процедуру, которая устанавливает контекст, и она работает каждый раз, даже если EF решает сбросить соединение.

private int _contextUserId;

public void SomeMethod()
{
    var db = new MyEntities();
    db.Connection.StateChange += this.Connection_StateChange;
    this._contextUserId = theCurrentUserId;

    // whatever else you want to do
}

private void Connection_StateChange(object sender, StateChangeEventArgs e)
{
    // only do this when we first open the connection
    if (e.OriginalState == ConnectionState.Open ||
        e.CurrentState != ConnectionState.Open)
        return;

    // use the existing open connection to set the context info
    var connection = ((EntityConnection) sender).StoreConnection;
    var command = connection.CreateCommand();
    command.CommandText = "proc_ContextInfoSet";
    command.CommandType = CommandType.StoredProcedure;
    command.Parameters.Add(new SqlParameter("ContextUserID", this._contextUserId));
    command.ExecuteNonQuery();
}

Мне нравится этот подход тем, что он также будет работать, когда хранимые процедуры вызываются из EF. Предположительно, он добавляет досадные дополнительные накладные расходы на каждый выбор, поскольку он добавляет выполнение процедуры туда и обратно. Было бы неплохо иметь способ избежать этого, особенно для людей, которым не нужен набор CONTEXT_INFO для выбора.

Rory 09.11.2016 18:38

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

Rory 09.11.2016 18:47

@Rory - TBH, этому больше 5 лет, и я даже не помню, чтобы это писал. Ясно, что так и было, но теперь это ушло из моей головы. ;)

Matt Johnson-Pint 09.11.2016 23:26

вы, наверное, забыли больше, чем многие люди когда-либо знали :)

Rory 10.11.2016 12:57

Просто принудительно выполните SET CONTEXT_INFO, используя свой DbContext или ObjectContext:

...
FileMoverContext context = new FileMoverContext();
context.SetSessionContextInfo(Environment.UserName);
...
context.SaveChanges();

FileMoverContext наследуется от DbContext и имеет метод SetSessionContextInfo. Вот как выглядит мой SetSessionContextInfo (...):

public bool SetSessionContextInfo(string infoValue)
{
   try
   {
      if (infoValue == null)
         throw new ArgumentNullException("infoValue");

      string rawQuery =
                   @"DECLARE @temp varbinary(128)
                     SET @temp = CONVERT(varbinary(128), '";

      rawQuery = rawQuery + infoValue + @"');
                    SET CONTEXT_INFO @temp";
      this.Database.ExecuteSqlCommand(rawQuery);

      return true;
   }
   catch (Exception e)
   {
      return false;
   }
}

Теперь вы просто настроили триггер базы данных, который может обращаться к CONTEXT_INFO (), и установить поле базы данных с его помощью.

У меня был похожий сценарий, который я решил, выполнив следующие шаги:

  1. Сначала создайте общий репозиторий для всех операций CRUD, таких как отслеживание, что всегда является хорошим подходом. открытый класс GenericRepository: IGenericRepository, где T: class

  2. Теперь напишите свои действия, такие как «Public virtual void Update (T entityToUpdate)».

  3. Где бы вам ни потребовалось ведение журнала / аудит; просто вызовите пользовательскую функцию следующим образом: «LogEntity (entityToUpdate,« U »);».
  4. Обратитесь к вставленному ниже файлу / классу, чтобы определить функцию «LogEntity». В этой функции в случае обновления и удаления мы получим старую сущность через первичный ключ для вставки в таблицу аудита. Чтобы определить первичный ключ и получить его значение, я использовал отражение.

Найдите ссылку на полный класс ниже:

 public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    internal SampleDBContext Context;
    internal DbSet<T> DbSet;

    /// <summary>
    /// Constructor to initialize type collection
    /// </summary>
    /// <param name = "context"></param>
    public GenericRepository(SampleDBContext context)
    {
        Context = context;
        DbSet = context.Set<T>();
    }

    /// <summary>
    /// Get query on current entity
    /// </summary>
    /// <returns></returns>
    public virtual IQueryable<T> GetQuery()
    {
        return DbSet;
    }

    /// <summary>
    /// Performs read operation on database using db entity
    /// </summary>
    /// <param name = "filter"></param>
    /// <param name = "orderBy"></param>
    /// <param name = "includeProperties"></param>
    /// <returns></returns>
    public virtual IEnumerable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>,
                                            IOrderedQueryable<T>> orderBy = null, string includeProperties = "")
    {
        IQueryable<T> query = DbSet;

        if (filter != null)
        {
            query = query.Where(filter);
        }

        query = includeProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Aggregate(query, (current, includeProperty) => current.Include(includeProperty));

        if (orderBy == null)
            return query.ToList();
        else
            return orderBy(query).ToList();
    }

    /// <summary>
    /// Performs read by id operation on database using db entity
    /// </summary>
    /// <param name = "id"></param>
    /// <returns></returns>
    public virtual T GetById(object id)
    {
        return DbSet.Find(id);
    }

    /// <summary>
    /// Performs add operation on database using db entity
    /// </summary>
    /// <param name = "entity"></param>
    public virtual void Insert(T entity)
    {
        //if (!entity.GetType().Name.Contains("AuditLog"))
        //{
        //    LogEntity(entity, "I");
        //}
        DbSet.Add(entity);
    }

    /// <summary>
    /// Performs delete by id operation on database using db entity
    /// </summary>
    /// <param name = "id"></param>
    public virtual void Delete(object id)
    {
        T entityToDelete = DbSet.Find(id);
        Delete(entityToDelete);
    }

    /// <summary>
    /// Performs delete operation on database using db entity
    /// </summary>
    /// <param name = "entityToDelete"></param>
    public virtual void Delete(T entityToDelete)
    {
        if (!entityToDelete.GetType().Name.Contains("AuditLog"))
        {
            LogEntity(entityToDelete, "D");
        }

        if (Context.Entry(entityToDelete).State == EntityState.Detached)
        {
            DbSet.Attach(entityToDelete);
        }
        DbSet.Remove(entityToDelete);
    }

    /// <summary>
    /// Performs update operation on database using db entity
    /// </summary>
    /// <param name = "entityToUpdate"></param>
    public virtual void Update(T entityToUpdate)
    {
        if (!entityToUpdate.GetType().Name.Contains("AuditLog"))
        {
            LogEntity(entityToUpdate, "U");
        }
        DbSet.Attach(entityToUpdate);
        Context.Entry(entityToUpdate).State = EntityState.Modified;
    }

    public void LogEntity(T entity, string action = "")
    {
        try
        {
            //*********Populate the audit log entity.**********
            var auditLog = new AuditLog();
            auditLog.TableName = entity.GetType().Name;
            auditLog.Actions = action;
            auditLog.NewData = Newtonsoft.Json.JsonConvert.SerializeObject(entity);
            auditLog.UpdateDate = DateTime.Now;
            foreach (var property in entity.GetType().GetProperties())
            {
                foreach (var attribute in property.GetCustomAttributes(false))
                {
                    if (attribute.GetType().Name == "KeyAttribute")
                    {
                        auditLog.TableIdValue = Convert.ToInt32(property.GetValue(entity));

                        var entityRepositry = new GenericRepository<T>(Context);
                        var tempOldData = entityRepositry.GetById(auditLog.TableIdValue);
                        auditLog.OldData = tempOldData != null ? Newtonsoft.Json.JsonConvert.SerializeObject(tempOldData) : null;
                    }

                    if (attribute.GetType().Name == "CustomTrackAttribute")
                    {
                        if (property.Name == "BaseLicensingUserId")
                        {
                            auditLog.UserId = ValueConversion.ConvertValue(property.GetValue(entity).ToString(), 0);
                        }
                    }
                }
            }

            //********Save the log in db.*********
            new UnitOfWork(Context, null, false).AuditLogRepository.Insert(auditLog);
        }
        catch (Exception ex)
        {
            Logger.LogError(string.Format("Error occured in [{0}] method of [{1}]", Logger.GetCurrentMethod(), this.GetType().Name), ex);
        }
    }
}

CREATE TABLE [dbo].[AuditLog](
[AuditId] [BIGINT] IDENTITY(1,1) NOT NULL,
[TableName] [nvarchar](250) NULL,
[UserId] [int] NULL,
[Actions] [nvarchar](1) NULL,
[OldData] [text] NULL,
[NewData] [text] NULL,
[TableIdValue] [BIGINT] NULL,
[UpdateDate] [datetime] NULL,
 CONSTRAINT [PK_DBAudit] PRIMARY KEY CLUSTERED 
(
[AuditId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = 
OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Это то, что я использовал, нашел здесь Я изменил его, потому что он не работал

private object GetPrimaryKeyValue(DbEntityEntry entry)
        {
            var objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);
            object o = objectStateEntry.EntityKey.EntityKeyValues[0].Value;
            return o;
        }

         private bool inExcludeList(string prop)
        {
            string[] excludeList = { "props", "to", "exclude" };
            return excludeList.Any(s => s.Equals(prop));
        }

        public int SaveChanges(User user, string UserId)
        {
            var modifiedEntities = ChangeTracker.Entries()
                .Where(p => p.State == EntityState.Modified).ToList();
            var now = DateTime.Now;

            foreach (var change in modifiedEntities)
            {

                var entityName = ObjectContext.GetObjectType(change.Entity.GetType()).Name;
                var primaryKey = GetPrimaryKeyValue(change);
                var DatabaseValues = change.GetDatabaseValues();

                foreach (var prop in change.OriginalValues.PropertyNames)
                {
                    if (inExcludeList(prop))
                    {
                        continue;
                    }

                    string originalValue = DatabaseValues.GetValue<object>(prop)?.ToString();
                    string currentValue = change.CurrentValues[prop]?.ToString();

                    if (originalValue != currentValue)
                    {
                        ChangeLog log = new ChangeLog()
                        {
                            EntityName = entityName,
                            PrimaryKeyValue = primaryKey.ToString(),
                            PropertyName = prop,
                            OldValue = originalValue,
                            NewValue = currentValue,
                            ModifiedByName = user.LastName + ", " + user.FirstName,
                            ModifiedById = UserId,
                            ModifiedBy = user,
                            ModifiedDate = DateTime.Now
                        };

                        ChangeLogs.Add(log);
                    }
                }
            }
            return base.SaveChanges();
        }



public class ChangeLog 
    {
        public int Id { get; set; }
        public string EntityName { get; set; }
        public string PropertyName { get; set; }
        public string PrimaryKeyValue { get; set; }
        public string OldValue { get; set; }
        public string NewValue { get; set; }
        public string ModifiedByName { get; set; }



        [ForeignKey("ModifiedBy")]
        [DisplayName("Modified By")]
        public string ModifiedById { get; set; }
        public virtual User ModifiedBy { get; set; }


        [Column(TypeName = "datetime2")]
        public DateTime? ModifiedDate { get; set; }
    }

Другие вопросы по теме