web-dev-qa-db-ja.com

EntityFrameworkを使用してすべてのデータ変更をログに記録する

顧客は、すべてのデータ変更を、変更を行った実際のユーザーとともにログテーブルに記録する必要があります。アプリケーションは1人の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コマンドには無効です。 EntityParameterの代わりにSqlParameterでも同じです。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();

エラー:クエリ構文が無効です。

ですから、私はEntityFrameworkとADO.NETの間にブリッジを作成することに固執しています。動作させることができれば、概念実証を投稿します。

29
Biri

Contextの処理はどうですか。 SavingChanges

12
Craig Stuntz

私を正しい方向に向けてくれてありがとう。ただし、私の場合、ユーザーによる行レベルのセキュリティを制御するためにコンテキスト情報を使用するビューをクエリしているため、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();
}
13

最後に、Craigの助けを借りて、ここに概念実証があります。さらにテストが必要ですが、一見すると機能しています。

最初に:2つのテーブルを作成しました。1つはデータ用、もう1つはロギング用です。

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

2番目:挿入のトリガーを作成します。

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

また、更新のトリガーを作成する必要があります。これは、変更されたコンテンツについてすべてのフィールドをチェックする必要があるため、もう少し洗練されています。

ログテーブルとトリガーは、作成/変更されたテーブルとフィールドを格納するように拡張する必要がありますが、あなたがアイデアを得たことを願っています。

3番目:ユーザー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();

したがって、変更を保存する前に、オブジェクトの接続を開き、コードを挿入して(この部分では接続を閉じないでください!)、変更を保存します。

そして忘れないでください!これはロギングのニーズに合わせて拡張する必要があり、可能性のみを示しているため、十分にテストする必要があります。

10
Biri

ストアドプロシージャをエンティティモデルに追加してみましたか?

3
Davy Landman

DbContextまたはObjectContextを使用して、SETCONTEXT_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()にアクセスし、それを使用してデータベースフィールドを設定できるデータベーストリガーを設定するだけです。

2
CatGuardian

私たちはこの問題を別の方法で解決しました。

  • 生成されたエンティティコンテナクラスからクラスを継承します
  • 基本エンティティクラスを抽象化します。別のファイルの部分的なクラス定義によってそれを行うことができます
  • 継承されたクラスで、メソッド定義でnewキーワードを使用して、SavingChangesメソッドを独自のもので非表示にします
  • SavingChangesメソッドの場合:

    1. a、エンティティ接続を開きます
    2. ebtityclientを使用してユーザーコンテキストストアドプロシージャを実行します
    3. base.SaveChanges()を呼び出す
    4. エンティティ接続を閉じます

コードでは、継承されたクラスを使用する必要があります。

2
user186682

似たようなシナリオがありましたが、次の手順で解決しました。

  1. まず、次のようなすべてのCRUD操作用の汎用リポジトリを作成します。これは常に適切なアプローチです。パブリッククラスGenericRepository:IGenericRepositoryここで、T:クラス

  2. 次に、「public virtual void Update(TentityToUpdate)」のようなアクションを記述します。

  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]
0
MAQ

これは私が見つけたものです ここ それが機能しなかったので私はそれを変更しました

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
A_Arnold