[Entity Framework] メタデータ列（データ作成/更新日時など）の値設定

データ作成/更新日時などメタデータ列の値設定は共通処理にすると便利です。
Entity Framework ではどのタイミングでどのように設定すればよいでしょうか。

※データベースのトリガーにしないこと、ローカルのシステム日付を使用することの是非についてはここでは触れません。

方法はいくつかあります（一番きれいなのは最後の方式です）。

# データバインドコントロールのイベント

たとえば ASP.NET Web フォームの場合、以下のハンドラで e.Values（Insert の場合）または e.NewValues（Update の場合）の各カラム要素に値を設定すると反映されます。

- GridView.RowUpdating イベント
- ListView.ItemInserting/ItemUpdating イベント
- DetailsView.ItemInserting/ItemUpdating イベント
- FormView.ItemInserting/ItemUpdating イベント

# データソースコントロールのイベント
たとえば ASP.NET Web フォームの場合、以下のハンドラでエンティティを取得してプロパティに設定することができます。

#### ObjectDataSource.Inserting/Updating イベント
e.InputParameters[0] にエンティティが格納されています。
#### EntityDataSource.Inserting/Updating イベント
e.Entity にエンティティが格納されています。

# SaveChanges メソッド（dynamic 方式）
個別のデータや操作に依存した値を設定するのには向きませんが、データ作成/更新日時の設定であれば、今回ご紹介した中で最も確実で実装効率のよい方法と言えます。
ここでは DbContext（EF 4.1 ～）の SaveChanges メソッドをオーバーライドして作成日時を設定する例をご紹介します。

```c#:DbContextの部分クラス
public partial class SampleEntities
{
    public override int SaveChanges()
    {
        SetCreatedDateTime();
 
        return base.SaveChanges();
    }
 
    private void SetCreatedDateTime()
    {
        DateTime now = DateTime.Now;
 
        // 追加エンティティのうち、CreatedDateTime プロパティを持つものを抽出
        var entities = this.ChangeTracker.Entries()
            .Where(e => (e.State & EntityState.Added) != 0 && e.CurrentValues.PropertyNames.Contains("CreatedDateTime"))
            .Select(e => e.Entity);
 
        foreach (dynamic entity in entities)
        {
            entity.CreatedDateTime = now;
        }
    }
}
```

# SaveChanges メソッド（インターフェイス方式）
エンティティの部分クラス定義（インターフェイス実装）を手動で行う必要がありますが、dynamic 方式よりきれいです。
エンティティを追加したときなど、部分クラス定義を忘れないように注意が必要です（そのためのユニットテストが一番下にあります）。

```c#:IEntity.cs（エンティティのインターフェイス）
public interface IEntity
{
    int? CreatedUserId { get; set; }
    DateTime? CreatedDateTime { get; set; }
    int? UpdatedUserId { get; set; }
    DateTime? UpdatedDateTime { get; set; }
}
```
```c#:EntityPartials.cs（エンティティの部分クラス定義）
public partial class Foo : IEntity {}
public partial class Bar : IEntity {}
 : 
```
スキャフォールドテンプレートをカスタマイズすれば部分クラスでのインターフェイス指定は不要になります。
Code First なら BaseEntity 等の抽象クラスに定義するのがいいですね。
```c#:DbContextの部分クラス
public partial class SampleEntities
{
    public override int SaveChanges()
    {
        SetMetaFields();
 
        return base.SaveChanges();
    }
 
    private void SetMetaFields()
    {
        DateTime now = DateTime.Now;
 
        foreach (var entry in this.ChangeTracker.Entries<IEntity>())
        {
            if ((entry.State & EntityState.Added) != 0)
            {
                entry.Entity.CreatedDateTime = now;
            }
 
            if ((entry.State & (EntityState.Added | EntityState.Modified)) != 0)
            {
                entry.Entity.UpdatedDateTime = now;
            }
        }
    }
}
```
以下は EF Core での実装例です。
```c#:
public class SampleContext : DbContext
{
     :
    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        SetMetaFields();
 
        return base.SaveChanges(acceptAllChangesOnSuccess);
    }
 
    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
    {
        SetMetaFields();
 
        return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }
 
    private void SetMetaFields()
    {
        DateTime now = DateTime.Now;
 
        foreach (var entry in this.ChangeTracker.Entries<EntityBase>())
        {
            if ((entry.State & EntityState.Added) != 0)
            {
                entry.Entity.CreatedDateTime = now;
            }
 
            if ((entry.State & (EntityState.Added | EntityState.Modified)) != 0)
            {
                entry.Entity.UpdatedDateTime = now;
            }
        }
    }
}
```
```c#:IEntityの実装漏れを検出するユニットテスト
[TestMethod]
public void EntitiesShouldImplementIEntity()
{
    var entityTypes = typeof(SampleEntities)
        .GetProperties(BindingFlags.Public | BindingFlags.Instance)
        .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
        .Select(p => p.PropertyType.GetGenericArguments().Single());
 
    foreach (var type in entityTypes)
    {
        if (type == typeof(Baz))
        {
            // 除外エンティティ
            continue;
        }
 
        Assert.IsTrue(typeof(IEntity).IsAssignableFrom(type), String.Format("{0} は IEntity を実装していません。", type.FullName));
    }
}
```