Регистрация каждого изменения данных с Entity Framework - PullRequest
29 голосов
/ 17 ноября 2008

Требуется, чтобы клиент регистрировал каждое изменение данных в таблице журналов с фактическим пользователем, который внес изменение. Приложение использует одного пользователя 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. Если я смогу заставить его работать, я опубликую подтверждение концепции.

Ответы [ 8 ]

13 голосов
/ 25 февраля 2011

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

Мне было проще всего присоединиться к событию StateChanged соединения и просто наблюдать за изменением с не открытого на открытое. Затем я вызываю proc, который устанавливает контекст, и он работает каждый раз, даже если 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();
}
12 голосов
/ 17 ноября 2008

Как насчет обработки контекста. SavingChanges ?

10 голосов
/ 20 ноября 2008

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

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

-- 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 должен содержать таблицу datastuff (или таблицы, за которыми нужно следить за изменениями) и SP.

Теперь сделайте что-нибудь с объектом-сущностью (например, добавьте новый объект datastuff) и подключите к событию 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();

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

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

3 голосов
/ 19 ноября 2008

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

2 голосов
/ 07 августа 2014

Просто форсируйте выполнение 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 (), и устанавливаете поле базы данных, используя его.

2 голосов
/ 08 октября 2009

Мы решили эту проблему по-другому.

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

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

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

0 голосов
/ 22 августа 2018

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

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; }
    }
0 голосов
/ 09 сентября 2015

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

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

  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]
...