シャドウプロパティ

シャドウプロパティは、エンティティclass側には存在しないがDBのテーブル側には存在する列を作る、使うための機能です。

public partial class Blog
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Text { get; set; }
}

こういうclassに対して、Fluent APIでシャドウプロパティを追加で定義できます。

builder.Property(typeof(DateTimeOffset), "Created")
    .HasColumnType("datetimeoffset");
    
builder.Property(typeof(DateTimeOffset), "Modified")
    .HasColumnType("datetimeoffset");

こうすると、EF CoreはCreatedやModifiedをテーブルに列が存在するものとして扱います。 Migrationすればちゃんとテーブルに列が作られますし、select * from BlogでちゃんとCreated列もModified列も読み込まれますし、ChangeTrackerのAPI経由で値の取得や更新も可能です。 普通にアプリを書いている分にはエンティティclass経由で操作することがほぼ全てだと思われますので、通常のアプリコードからは隠蔽されます。

シャドウプロパティは、他のテーブルへのリレーションのキーとして使われたりするようです。

public partial class Blog
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Text { get; set; }
    public OtherEntity OtherEntity { get; set; } //シャドウプロパティでOtherEntityId列が隠れている
}

システム列として論理削除列、作成日時、更新日時などアプリから隠蔽したい日付を持たせたい場合も有効です。というかMigrationを考えるとシャドウプロパティにするほかありません。

シャドウプロパティに初期値を与える

シャドウプロパティに初期値を与えたい場合、定数であればFluentAPIのHasDefaultValueを使うことができます。

builder.Property(typeof(DateTimeOffset), "Created")
    .HasColumnType("datetimeoffset")
    .HasDefaultValue(DateTimeOffset.MinValue);

あるいはSQLのDEFAULT制約を用いることもできます。

builder.Property(typeof(DateTimeOffset), "Created")
    .HasColumnType("datetimeoffset")
    .HasDefaultValueSql("getdate()");

コードで動的に値を与えたい場合は、ValueGeneratorを使います。

using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ValueGeneration;

public class CurrentDateTimeOffsetValueGenerator : ValueGenerator<DateTimeOffset>
{
    public override bool GeneratesTemporaryValues => false;

    public override DateTimeOffset Next([NotNull] EntityEntry entry)
    {
        return DateTimeOffset.Now;
    }
}
builder.Property(typeof(DateTimeOffset), "Created")
    .HasColumnType("datetimeoffset")
    .HasValueGenerator<CurrentDateTimeOffsetValueGenerator>();

更新時にシャドウプロパティも更新する

基本的に、DB側でTriggerやそれに類する仕組みを使用すべきです。 コードで行う場合、EF Coreにはそれを行う正規の手順はありません。ValueGeneratorは、エンティティの新規作成時のみしか動作しません。

ただしどうしても行いたいなら、DbContext.SaveChanges()をオーバーライドして中でChangeTrackerをごにょることで実現することは可能です。おすすめはしませんが。※非同期の場合はSaveChangesAsyncも

public override int SaveChanges()
{
    var now = DateTimeOffset.Now;
    foreach(var x in ChangeTracker.Entries()
        .Where(x => x.State == EntityState.Modified || x.State == EntityState.Added))
    {
        //全てのテーブルにModifiedが存在する前提
        x.Property("Modified").CurrentValue = now;
    }

    return base.SaveChanges();
}

DB側でTriggerなどを用いて新規作成時や更新時に値をいれる列では、アプリ側で値を入れるのは無駄です。 EF Coreでは、そのようなときに、これはDB側で更新される列だから余計なことをしないように、と伝える方法があります。

builder.Property(typeof(DateTimeOffset), "Created")
    .HasColumnType("datetimeoffset")
    .ValueGeneratedOnAdd();

builder.Property(typeof(DateTimeOffset), "Modified")
    .HasColumnType("datetimeoffset")
    .ValueGeneratedOnAddOrUpdate();

ValueGeneratedOnAddOrUpdate()というメソッド名をみると、先述したValueGeneratorがUpdate時も動いてくれそうな印象がありますが、これはまったくValueGeneratorとは無関係です。ValueGeneratorはエンティティ新規作成時しか起動されません。