しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

Entity Framework Core 6 の Compiled Models を試した

前から気になっていたのですが、ようやく触る気になったので Entity Framework Core 6 で追加された Compiled Models を一通り試しておきました。Preview 5 の時のブログと Compiled Models の Issue を見れば一通り理解できるはずです。

ブログには主にパフォーマンスに関する情報が載っています。大規模な DbContext を使っている場合には初回起動の高速化にかなり効果があるようです。

特に Issue の方は細かくステータスが管理されているので、目を通しておくのがお勧めです。Backlog にある機能は GA のタイミングでは入らないということなので、既に使っている場合はあきらめましょう。

少し前の EF Community Standup で Compiled Models のデモが行われているので、使い方やパフォーマンス含め気になる方は視聴しておいた方が良いです。

単純に Compiled Models を生成して試すだけでは意味がないので、制約と運用面を含めて確認しておきました。サンプルとして以下のような小さな DbContext を作成しています。

public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
        optionsBuilder.UseSqlite("Data Source=Sample.db");

    public DbSet<Entry> Entries { get; set; }

    public DbSet<User> Users { get; set; }

    public DbSet<Comment> Comments { get; set; }
}

これぐらいの規模ではコンパイルしたところで大した効果は出ないですが、遅くなることはないようです。

dotnet-ef を使ってコード生成を行う

実際に EF Core 6 で Compiled Models の生成を行うには dotnet-ef をインストールして行います。現在はプレリリースなので、バージョンを明示的に指定しないと .NET 5.0 向けが入るので注意です。

コンパイルを行うコマンドは dotnet-ef のインストール含め以下の 2 行で終わります。

dotnet tool update -g dotnet-ef --version 6.0.0-rc.1.21452.10
dotnet ef dbcontext optimize -o CompiledModel -p EFCore6CompiledModel

このときにエラーが発生することがありますが、大体は Microsoft.EntityFrameworkCore.Design が足りないことに対してなので、追加でインストールするだけで通ります。

他には DbContext が作成できないエラーが多いので、以下のドキュメントを参照してデザイン時向けの DbContext を作成出来るように追加の設定を行います。

コンパイルに成功すると、オプションで指定した CompiledModel ディレクトリ内に生成されたコードが追加されます。この辺りはルールが決まっているわけではないですが、コンパイルされたコード専用の分かりやすいディレクトリに出力したほうが良いです。

f:id:shiba-yan:20210912010527p:plain

最後は DbContext の初期化時に UseModel でコンパイルされた Model を指定すれば終わりです。

public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
        optionsBuilder.UseModel(AppDbContextModel.Instance)
                      .UseSqlite("Data Source=Sample.db");

    public DbSet<Entry> Entries { get; set; }

    public DbSet<User> Users { get; set; }

    public DbSet<Comment> Comments { get; set; }
}

手順的には非常にシンプルですが、開発中に常にコンパイルされた Model を使うのは負荷が高いので、リリースビルドの時のみ使用するような設定が必要になると思います。

コード生成に使ったバージョンに注意

.NET 6.0 Preview 7 で生成されたコードから .NET 6.0 RC 1 で再コンパイルする際に警告が表示されました。

The model supplied in the context options was created with EF Core version '6.0.0-preview.7.21378.4', but the context is from version '6.0.0-rc.1.21452.10'. Update the externally built model.

生成されたコードには使用したバージョンが含まれているので、今後互換性のないバージョンが出た際にはコンパイルエラーや実行時エラーになる可能性もありそうです。

Lazy Loading (Proxies) との組み合わせは現在は利用不可

大本の Issue にある Backlog に書いてありますが、EF Core 6 の初期リリースでは Proxies には対応しないため、Lazy Loading を使っている場合には利用不可となります。

EF Core の Lazy Loading については以前書いたエントリを参照してください。

実際に Lazy Loading を設定した DbContext を使ってコンパイルを行うとエラーになりました。

f:id:shiba-yan:20210914013443p:plain

制約としては一番大きい部類だと思うので、Lazy Loading を使っている場合はコンパイルを現時点では諦めましょう。現在は実行時に Castle を使って Proxy を生成していますが、将来的には Lazy Loading 含め丸ごとコンパイル出来るようになりそうです。

GitHub Actions と組み合わせて利用する

こういった事前コンパイル系の機能は如何にして生成されたコードを最新に保つかが重要になりますが、残念ながら最初に思いつくであろう MSBuild の実行時に同時にコード生成する方法は上手く動きません。

該当の Issue は以下になりますが、EF Core チームからはビルド時に常に生成する使い方は想定していないといったコメントが付いているので、CI 側で必要な時にコード生成するのが正しそうです。

GitHub Actions を使って自動的にコンパイル済み Model のコードを生成してコミットすることも可能ではありますが、個人的にはそのアプローチがあまり好きではないので PR に対するチェックを追加し、最新のコードが生成されているか確認する方向で対応します。

具体的には以下のような Workflow を作成して確認しました。単純に dotnet ef dbcontext optimize を実行して差分が出るかを確認しているだけです。

name: Build

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Setup .NET
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 6.0.x
        include-prerelease: true
    - name: Install EF Core tool
      run: dotnet tool update -g dotnet-ef --version 6.0.0-rc.1.21452.10
    - name: Restore dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --no-restore
    - name: Check compiled model
      run: |
        dotnet ef dbcontext optimize -o CompiledModel -p EFCore6CompiledModel
        test -z "$(git status -s)"

この Workflow を使って Model クラスのみ変更すると、以下のようにチェックが失敗するようになるので、Model クラスとコンパイルされた Model で差分が発生するのを防ぐことが出来るようになります。

f:id:shiba-yan:20210914005450p:plain

将来的にはアセンブリにコンパイルされた Model が含まれていれば、自動的にそれを利用するようになるみたいなので、その時にはコンパイル済みの Model をプロジェクトに含めずに、GitHub Actions などで都度生成する形で対応するのが非常にスマートだと思います。

CI でコンパイルする場合は、大前提としてコンパイルした Model の有無で挙動が変わらないことが重要となるので、本番リリース前には必ずステージング環境などでのテストが必須になるはずです。

まずは手動でのコンパイルを行う形で対応して、経験を積んでおいた方が良さそうです。