アプリケーションのすべてのSQLテーブルには、 "CreatedDate"および "ModifiedDate"列があります。 DBファーストのアプローチを使用しています。データを保存するときに、これらの2つの列が自動的に入力されるようにします。 1つの方法は、SQLテーブル列自体にデフォルト値をgetdate()
としておくことです。これで問題は部分的に解決されます。つまり、エンティティが新しい場合、CreatedDateとModifiedDateが設定されます。ただし、エンティティを編集/更新しているときは、ModifiedDate
のみを更新する必要があります。
Code Firstアプローチを使用してそれを行う記事はたくさんあります。しかし、私たちはDBの最初のアプローチを使用しています。
ここに私のオプションは何ですか?
OnSaveをオーバーライドする場合は、すべての保存メソッドをオーバーライドする必要があります。EFCore 2.1では、ChangeTrackedおよびイベントでより良いソリューションを使用できます。例えば:
以下の例のように、インターフェースまたは基本クラスを作成できます。
public interface IUpdateable
{
DateTime ModificationDate{ get; set; }
}
public class SampleEntry : IUpdateable
{
public int Id { get; set; }
DateTime ModificationDate{ get; set; }
}
次に、コンテキストの作成時に、変更トラッカーにイベントを追加します。
context.ChangeTracker.StateChanged += context.ChangeTracker_StateChanged;
そして方法:
private void ChangeTracker_StateChanged(object sender, Microsoft.EntityFrameworkCore.ChangeTracking.EntityStateChangedEventArgs e)
{
if(e.Entry.Entity is IUpdateable && e.Entry.State == EntityState.Modified)
{
var entry = ((IUpdateable)e.Entry.Entity);
entry.ModyficationDate = DateTime.Now;
}
}
方が簡単で、すべてのメソッドをオーバーライドする必要はありません。
非同期システム(SaveChangesAsync)と.netコアを使用している場合は、以下のコードをDbContextクラスに追加することをお勧めします。
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
var AddedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Added).ToList();
AddedEntities.ForEach(E =>
{
E.Property("CreationTime").CurrentValue = DateTime.Now;
});
var EditedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Modified).ToList();
EditedEntities.ForEach(E =>
{
E.Property("ModifiedDate").CurrentValue = DateTime.Now;
});
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
また、以下のようにクラスまたはインターフェースを定義することもできます。
public class SaveConfig
{
public DateTime CreationTime { get; set; }
public DateTime? ModifiedDate { get; set; }
}
saveConfigエンティティからエンティティを継承するだけです。
DbContextのSaveChangesメソッドをオーバーライドして、エンティティの追加および変更のリストを取得できます。追加された場合は、CreatedDateを設定し、変更された場合はModifiedDateを設定します。
public override int SaveChanges()
{
var AddedEntities = ChangeTracker.Entries<Entity>().Where(E => E.State == EntityState.Added).ToList();
AddedEntities.ForEach(E =>
{
E.CreatedDate = DateTime.Now;
});
var ModifiedEntities = ChangeTracker.Entries<Entity>().Where(E => E.State == EntityState.Modified).ToList();
ModifiedEntities.ForEach(E =>
{
E.ModifiedDate = DateTime.Now;
});
return base.SaveChanges();
}
2つのDateTimeプロパティを持つインターフェイスを記述して、エンティティにそれらを継承させることもできます。
interface IEntityDate
{
DateTime AddedDate { set; get;}
DateTime ModifiedDate { set; get;}
}
class Entity : IEntityDate
{
public DateTime AddedDate { set; get;}
public DateTime ModifiedDate { set; get;}
}
私はこのように使用されるより一般的なアプローチが好きです:
interface IEntityDate
{
DateTime CreatedDate { get; set; }
DateTime UpdatedDate { get; set; }
}
public abstract class EntityBase<T1>: IEntityDate
{
public T1 Id { get; set; }
public virtual DateTime CreatedDate { get; set; }
public virtual string CreatedBy { get; set; }
public virtual DateTime UpdatedDate { get; set; }
public virtual string UpdatedBy { get; set; }
}
public override int SaveChanges()
{
var now = DateTime.Now;
foreach (var changedEntity in ChangeTracker.Entries())
{
if (changedEntity.Entity is IEntityDate entity)
{
switch (changedEntity.State)
{
case EntityState.Added:
entity.CreatedDate = now;
entity.UpdatedDate = now;
break;
case EntityState.Modified:
Entry(entity).Property(x => x.CreatedDate).IsModified = false;
entity.UpdatedDate = now;
break;
}
}
}
return base.SaveChanges();
}
更新:
CreatedBy
およびUpdatedBy
を処理するには、次のようにDbContextのラッパーを使用します。
public interface IEntity
{
DateTime CreatedDate { get; set; }
string CreatedBy { get; set; }
DateTime UpdatedDate { get; set; }
string UpdatedBy { get; set; }
}
public interface ICurrentUser
{
string GetUsername();
}
public class ApplicationDbContextUserWrapper
{
public ApplicationDbContext Context;
public ApplicationDbContextUserWrapper(ApplicationDbContext context, ICurrentUser currentUser)
{
context.CurrentUser = currentUser;
this.Context = context;
}
}
public class ApplicationDbContext : DbContext
{
public ICurrentUser CurrentUser;
public override int SaveChanges()
{
var now = DateTime.Now;
foreach (var changedEntity in ChangeTracker.Entries())
{
if (changedEntity.Entity is IEntity entity)
{
switch (changedEntity.State)
{
case EntityState.Added:
entity.CreatedDate = now;
entity.UpdatedDate = now;
entity.CreatedBy = CurrentUser.GetUsername();
entity.UpdatedBy = CurrentUser.GetUsername();
break;
case EntityState.Modified:
Entry(entity).Property(x => x.CreatedBy).IsModified = false;
Entry(entity).Property(x => x.CreatedDate).IsModified = false;
entity.UpdatedDate = now;
entity.UpdatedBy = CurrentUser.GetUsername();
break;
}
}
}
return base.SaveChanges();
}
...
EF Core 2.1で解決した方法は次のとおりです。
私は次のようなインターフェイスを継承する基本モデルを持っています:
public interface IAuditableModel
{
DateTime Created { get; }
DateTime? Updated { get; set; }
}
public class BaseModel : IAuditableModel
{
public BaseModel()
{
}
public int Id { get; set; }
public DateTime Created { get; private set; }
public DateTime? Updated { get; set; }
}
次に、私のDbContextで:
public override int SaveChanges()
{
var added = ChangeTracker.Entries<IAuditableModel>().Where(E => E.State == EntityState.Added).ToList();
added.ForEach(E =>
{
E.Property(x => x.Created).CurrentValue = DateTime.UtcNow;
E.Property(x => x.Created).IsModified = true;
});
var modified = ChangeTracker.Entries<IAuditableModel>().Where(E => E.State == EntityState.Modified).ToList();
modified.ForEach(E =>
{
E.Property(x => x.Updated).CurrentValue = DateTime.UtcNow;
E.Property(x => x.Updated).IsModified = true;
E.Property(x => x.Created).CurrentValue = E.Property(x => x.Created).OriginalValue;
E.Property(x => x.Created).IsModified = false;
});
return base.SaveChanges();
}
これにより、プロパティ名をハードコーディングする必要がなくなり、選択したモデルに特定のインターフェイスを柔軟に適用できます。私の場合、これはほとんどのモデルで必要なので、他のすべてのモデルが継承するBaseModelに適用しました。
同様に、これらのモデルを編集したビューで、ページにロードしていなかったため、作成されたタイムスタンプが消去されていました。その結果、モデルがポストバックにバインドされると、そのプロパティが消去され、作成したタイムスタンプが実質的に0に設定されました。
私は@Arashがパブリックオーバーライド "Task SaveChangesAsync"で提案したものと同様のことをしましたが、私のセットアップでは、実際にCreatedDate/ModifiedDateプロパティを持つエンティティを更新したいだけで、これが私が思いついたものです。 @Arashによく似ていますが、誰かを助けることができるかもしれません。プロパティの名前を定義する2つの定数EntityCreatedPropertyNameおよびEntityModifiedPropertyNameを追加しました。これにより、使用する場所で重複した文字列を使用する必要がなくなります。
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
#region Automatically set "CreatedDate" property on an a new entity
var AddedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Added).ToList();
AddedEntities.ForEach(E =>
{
var prop = E.Metadata.FindProperty(EntityCreatedPropertyName);
if (prop != null)
{
E.Property(EntityCreatedPropertyName).CurrentValue = DateTime.Now;
}
});
#endregion
#region Automatically set "ModifiedDate" property on an a new entity
var EditedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Modified).ToList();
EditedEntities.ForEach(E =>
{
var prop = E.Metadata.FindProperty(EntityModifiedPropertyName);
if (prop != null)
{
E.Property(EntityModifiedPropertyName).CurrentValue = DateTime.Now;
}
});
#endregion
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}