30

客户需要将每个数据更改与进行修改的实际用户一起记录到日志表中。应用程序使用一个 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 之间建立一座桥梁。如果我能让它工作,我会发布一个概念证明。

4

8 回答 8

13

如何处理上下文。保存更改

于 2008-11-17T15:28:08.857 回答
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();
}
于 2011-02-25T20:42:10.437 回答
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();

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

不要忘记!这需要根据您的日志记录需求进行扩展,并且需要进行良好的测试,因为这仅显示了可能性!

于 2008-11-20T09:03:22.813 回答
3

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

于 2008-11-19T20:47:38.570 回答
2

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

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

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

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

于 2009-10-08T19:55:31.307 回答
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() 并使用它设置一个数据库字段。

于 2014-08-06T21:44:01.057 回答
0

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

  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]
于 2015-09-09T10:17:40.870 回答
0

这是我在这里找到的我修改了它,因为它不起作用

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; }
    }
于 2018-08-22T19:28:19.787 回答