使用Entity Framework记录每个数据更改

时间:2008-11-17 14:41:24

标签: entity-framework logging

客户需要将每个数据更改记录到与进行修改的实际用户的日志记录表中。应用程序使用一个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();

错误:查询语法无效。

所以我在这里,坚持在Entity Framework和ADO.NET之间建立一座桥梁。 如果我能让它发挥作用,我会发布一个概念证明。

8 个答案:

答案 0 :(得分:13)

感谢您指点我正确的方向。但是,在我的情况下,我还需要在执行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();
}

答案 1 :(得分:12)

如何处理上下文。SavingChanges

答案 2 :(得分:10)

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

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

-- 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();

所以在保存更改之前,我们打开对象的连接,注入我们的代码(不要关闭这部分的连接!)并保存我们的更改。

不要忘记!这需要根据您的日志记录需求进行扩展,并且需要经过充分测试,因为这只显示了这种可能性!

答案 3 :(得分:3)

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

答案 4 :(得分:2)

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

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

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

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

答案 5 :(得分:2)

只需使用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 :(得分:0)

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

  1. 首先为所有CRUD操作创建一个通用存储库,如下所示,这始终是一种很好的方法。 public class GenericRepository:IGenericRepository,其中T:class

  2. 现在编写您的行为,例如“public virtual void Update(T entityToUpdate)”。

  3. 您需要记录/审核的任何地方;只需调用用户定义的函数,如下所示“LogEntity(entityToUpdate,”U“);”。
  4. 请参阅以下粘贴的文件/类以定义“LogEntity”功能。在此函数中,在更新和删除的情况下,我们将通过主键将旧实体插入到审计表中。为了识别主键并获得其值,我使用了反射。
  5. 查找以下完整课程的参考:

     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]
    

答案 7 :(得分:0)

这就是我发现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; }
    }