【问题标题】:Logging every data change with Entity Framework使用 Entity Framework 记录每个数据更改
【发布时间】:2018-10-28 21:13:14
【问题描述】:

客户需要将每次数据更改记录到记录表中,并与进行修改的实际用户一起记录。应用程序使用一个 SQL 用户访问数据库,但我们需要记录“真实”用户 ID。

我们可以在 t-sql 中通过为每个表插入和更新编写触发器并使用 context_info 来存储用户 ID 来做到这一点。我们将用户 ID 传递给存储过程,将用户 ID 存储在 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();

错误:查询语法无效。

所以我在这里,坚持在实体框架和 ADO.NET 之间建立一座桥梁。 如果我可以让它工作,我会发布一个概念证明。

【问题讨论】:

  • 用户真的希望您使用 EF,然后要求您坚持使用 SQL 2000?
  • 有一篇关于审计here的有趣帖子,你怎么看?
  • 六年后,我仍然认为这是一个很好的方法。触发器比应用程序级代码更适合审计,这提供了一种在不污染应用程序级代码的情况下将“真实”用户信息获取到数据库层的绝妙方法。
  • 这里davecallan.com/passing-userid-delete-trigger-entity-framework/# 使用事务方法来确保使用相同的连接。

标签: entity-framework logging


【解决方案1】:

如何处理 Context.SavingChanges

【讨论】:

  • 是的,这就是我想要避免的。 :-) 自动处理所有这些会很好。我们已经有触发器生成器来处理日志记录部分。缺少的链接是我们无法将用户 ID 传递给触发器。
  • 您不能使用 SavingChanges 在上下文信息中设置用户 ID? msdn.microsoft.com/en-us/library/ms187768.aspx
  • 哦,废话。最简单的解决方案,我们正在考虑一些非常复杂的东西。谢谢你让我大开眼界。
  • 对不起,我必须撤销接受状态,因为它不起作用。连接关闭(分离对象),如果我打开一个新连接并填写 context_info,它不会影响保存期间打开的连接。 :-(
  • 您可以提供自己的连接供 EF 使用。见:msdn.microsoft.com/en-us/library/bb738540.aspx
【解决方案2】:

感谢您为我指明正确的方向。但是,就我而言,我还需要在执行 select 语句时设置上下文信息,因为我正在查询使用上下文信息来控制用户行级安全性的视图。

我发现附加到连接的 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();
}

【讨论】:

  • 我喜欢这种方法,因为当从 EF 调用存储过程时它也可以工作。据推测,它在每个选择上增加了不幸的额外开销,因为它增加了往返过程执行。最好有办法避免这种情况,特别是对于不需要为选择设置 CONTEXT_INFO 的人。
  • 也许可以创建一个新的DbExecutionStrategy 来执行此过程与任何其他执行内联,从而消除额外的往返?
  • @Rory - TBH,这已经超过 5 年了,我什至不记得写过它。显然我做到了,但它现在已经从我的脑海中消失了。 ;)
  • 你可能忘记的比大多数人所知道的还要多:)
【解决方案3】:

最后在 Craig 的帮助下,这是一个概念证明。它需要更多测试,但乍一看它是有效的。

首先:我创建了两张表,一张用于数据,一张用于记录。

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

您还应该创建一个更新触发器,它会更复杂一些,因为它需要检查每个字段的更改内容。

应该扩展日志表和触发器以存储创建/更改的表和字段,但我希望你明白了。

第三:创建一个存储过程,将用户id填写到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.
【解决方案4】:

您是否尝试将存储过程添加到您的实体模型中?

【讨论】:

  • 是的,我做到了。克雷格指出了正确的方向,所以接下来我将发布 POC。
【解决方案5】:

使用 DbContext 或 ObjectContext 强制执行 SET CONTEXT_INFO:

...
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() 并使用它设置数据库字段的数据库触发器。

【讨论】:

    【解决方案6】:

    我们以不同的方式解决了这个问题。

    • 从生成的实体容器类继承一个类
    • 使基本实体类抽象。您可以通过单独文件中的部分类定义来完成
    • 在继承的类中,用你自己的方法隐藏 SavingChanges 方法,在方法定义中使用 new 关键字
    • 在您的 SavingChanges 方法中:

      1. a、打开实体连接
      2. 用ebtityclient执行用户上下文存储过程
      3. 调用base.SaveChanges()
      4. 关闭实体连接

    在你的代码中你必须使用继承的类。

    【讨论】:

      【解决方案7】:

      我有一些类似的情况,我通过以下步骤解决了:

      1. 首先为以下所有 CRUD 操作创建一个通用存储库,这始终是一个好方法。 公共类 GenericRepository : IGenericRepository where 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]
      

      【讨论】:

        【解决方案8】:

        这是我用的发现here我修改它,因为它不起作用

        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; }
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-02-26
          • 2014-11-11
          • 2018-03-21
          • 2021-12-14
          • 1970-01-01
          相关资源
          最近更新 更多