コンテンツにスキップ

Entity Framework Core チートシート

EF Core 10・.NET 10をベースに、CRUD、LINQ、関連データ、マイグレーション、同時実行制御をSQLiteの例でまとめたチートシートです。


Entity Frameworkは.NETのオブジェクトとデータベースを対応付けるORMです。このページはEF Coreを対象にします。EF6は別の製品系列で、パッケージを置き換えるだけでは移行できません。

項目 EF Core EF6
主な名前空間 Microsoft.EntityFrameworkCore System.Data.Entity
主なパッケージ Microsoft.EntityFrameworkCore.* EntityFramework
モデル定義 コードと設定。既存DBからコード生成も可能 Code First、EDMXデザイナーなど
本ページの対象 EF Core 10 / .NET 10 API例の対象外

EF Core 10は.NET 10を必要とします。EF Core 8・9は.NET 8を対象にした系列です。既存アプリでは、使用するDBプロバイダーが対象のEF Coreメジャーバージョンをサポートしているか確認します。

公式 — EF6とEF Coreの比較 / リリース情報


SQLiteのコンソールプロジェクトを作ります。ここでは安定版10.0.12を指定し、Microsoft製EFパッケージとツールのバージョンを揃えます。

Terminal window
dotnet new console -n EfCheatSheet --framework net10.0
cd EfCheatSheet
dotnet package add Microsoft.EntityFrameworkCore.Sqlite --version 10.0.12
dotnet package add Microsoft.EntityFrameworkCore.Design --version 10.0.12
dotnet new tool-manifest
dotnet tool install dotnet-ef --version 10.0.12
dotnet ef --version

既存リポジトリでツールマニフェストがある場合は、新規作成せず dotnet tool restore を使います。以降のファイルとコマンドは、このプロジェクトのディレクトリを基準にします。


BlogContext.cs を作成します。DbContext はDBへの問い合わせと変更追跡を担当し、DbSet<T> はエンティティの問い合わせ・追加などの入口です。

using Microsoft.EntityFrameworkCore;
public sealed class Blog
{
public int Id { get; set; }
public string Name { get; set; } = "";
public List<Post> Posts { get; set; } = [];
}
public sealed class Post
{
public int Id { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; } = null!;
public string Title { get; set; } = "";
public int Views { get; set; }
public bool IsDeleted { get; set; }
public Guid Version { get; set; } = Guid.NewGuid();
}
public sealed class BlogContext(DbContextOptions<BlogContext> options) : DbContext(options)
{
public DbSet<Blog> Blogs => Set<Blog>();
public DbSet<Post> Posts => Set<Post>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(entity =>
{
entity.Property(b => b.Name).HasMaxLength(100).IsRequired();
entity.HasIndex(b => b.Name).IsUnique();
});
modelBuilder.Entity<Post>(entity =>
{
entity.Property(p => p.Title).HasMaxLength(200).IsRequired();
entity.Property(p => p.Version).IsConcurrencyToken();
entity.HasIndex(p => new { p.BlogId, p.Id });
entity.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasQueryFilter("SoftDelete", p => !p.IsDeleted);
});
}
}

Id は規約によって主キーになります。BlogId は外部キー、Blog / Posts はナビゲーションです。null! はEFによる設定を想定したコンパイラーへの指定で、関連データを自動ロードしません。

HasMaxLength などはDBのモデル設定です。EF自体の入力検証ではなく、SQLiteでは文字列長の上限も強制されないため、入力検証を別途行います。名前付きクエリフィルターはEF Core 10の機能です。

公式 — モデルの作成


モデルのクラスやプロパティに属性を付けて設定できます。同じ設定が競合する場合の優先順位は Fluent API > 属性 > 規約 です。単純なマッピングは属性でモデルの近くに書け、複雑な設定や永続化設定の分離にはFluent APIが向いています。

属性 用途 対応するFluent API
[Table("Articles")] テーブル名 ToTable("Articles")
[Column("title")] 列名 HasColumnName("title")
[Key] 単一プロパティの主キー HasKey(e => e.Id)
[PrimaryKey(nameof(A), nameof(B))] クラスに付ける複合主キー(EF Core 7以降) HasKey(e => new { e.A, e.B })
[Required] NULLを許可しないモデル設定 IsRequired()
[MaxLength(200)] 最大長 HasMaxLength(200)
[StringLength(200)] 最大長。MinimumLengthはDB制約にならない HasMaxLength(200)
[Precision(18, 2)] 数値の精度・スケール HasPrecision(18, 2)
[Unicode(false)] 非Unicode設定(プロバイダー依存) IsUnicode(false)
[Index(nameof(Code), IsUnique = true)] クラスに付ける一意インデックス HasIndex(e => e.Code).IsUnique()
[ForeignKey(nameof(BlogId))] ナビゲーションに対応する外部キー HasForeignKey(e => e.BlogId)
[InverseProperty(nameof(Blog.Posts))] 対となるナビゲーションを特定 WithMany(b => b.Posts)
[NotMapped] マッピングから除外 Ignore(e => e.DisplayName)
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] 追加時に値を生成 ValueGeneratedOnAdd()
[DatabaseGenerated(DatabaseGeneratedOption.Computed)] 追加・更新時に値を生成 ValueGeneratedOnAddOrUpdate()
[ConcurrencyCheck] 元の値を更新・削除条件に含める IsConcurrencyToken()
[Timestamp] 追加・更新時のDB生成と並行性トークン IsRowVersion()

Key、Required、MaxLength、StringLength、ConcurrencyCheck、Timestamp は System.ComponentModel.DataAnnotations、Table、Column、ForeignKey、InverseProperty、NotMapped、DatabaseGenerated はその .Schema 名前空間にあります。EF Coreの PrimaryKey、Index、Precision、Unicode は Microsoft.EntityFrameworkCore にあり、EF6の同名・類似APIとは区別します。

以下は別のエンティティの宣言例で、Program.cs 末尾に挿入する操作例ではありません。使用する場合はコンテキストに DbSet<AnnotatedArticle> を登録し、スキーマを更新します。共通の Blog / Post モデルを置き換えるものではありません。

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
[Table("Articles")]
[Index(nameof(Code), IsUnique = true)]
public sealed class AnnotatedArticle
{
[Key]
public int Id { get; set; }
[Required, MaxLength(32)]
public string Code { get; set; } = "";
[Column("title")]
[Required, MaxLength(200)]
public string Title { get; set; } = "";
[Precision(18, 2)]
public decimal Price { get; set; }
[ConcurrencyCheck]
public Guid Version { get; set; } = Guid.NewGuid();
[NotMapped]
public string DisplayName => $"{Code}: {Title}";
}

[ConcurrencyCheck] は値を自動生成しません。共通モデルと同様に、保護したい更新で Version を新しく設定します。[DatabaseGenerated] は値生成の設定であり、それだけで計算列のSQL式を定義したり、全ての型を自動採番にしたりはできません。必要に応じてDBが対応する既定値・計算列の設定を使います。

EF Coreの SaveChanges は、Data Annotationsによるオブジェクト検証を自動実行しません。例えば [Range] や [EmailAddress] を付けてもDB制約は作られません。ASP.NET Coreの検証や Validator.TryValidateObject などで入力を検証します。Nullable参照型が有効なら string は規約で必須となり、[Required] はそのモデル設定を明示できます。C#の required は初期化時のルールで、別の機能です。長さ・精度のDBでの強制はプロバイダーにも依存します。

モデル設定 / プロパティのマッピング / 関連の属性設定


Program.cs を以下で置き換えます。接続を開いたままにすることで、インメモリSQLiteをコンテキスト間で共有できます。

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<BlogContext>()
.UseSqlite(connection)
.Options;
await using var db = new BlogContext(options);
await db.Database.EnsureCreatedAsync();
var blog = new Blog
{
Name = "Development",
Posts = [new Post { Title = "Hello EF Core", Views = 10 }]
};
db.Blogs.Add(blog);
await db.SaveChangesAsync();
db.ChangeTracker.Clear();
// 以下に各節の操作例を挿入します。
Console.WriteLine(await db.Posts.CountAsync());

dotnet run で実行すると 1 が表示されます。以降の「操作例」は、このコード末尾に1つずつ挿入して試します(宣言済みの db、options、blog を使います)。ファイル全体の例は別途明記します。各実行で新しいDBが作られるため、例を順番に実行した状態には依存しません。

EnsureCreatedAsync は学習用・一時DB向けです。マイグレーション履歴を作らず、既存スキーマを更新しません。後述のマイグレーションとは同じDBで混用しません。


追加・取得・更新・削除(CRUD)

Section titled “追加・取得・更新・削除(CRUD)”

操作例です。通常の更新では、取得したエンティティのプロパティを変更して SaveChangesAsync を呼びます。

var post = new Post { BlogId = blog.Id, Title = "Second post" };
db.Posts.Add(post);
await db.SaveChangesAsync();
var found = await db.Posts.SingleAsync(p => p.Id == post.Id);
Console.WriteLine(found.Title);
found.Title = "Updated post";
found.Version = Guid.NewGuid();
await db.SaveChangesAsync();
db.Posts.Remove(found);
await db.SaveChangesAsync();
Console.WriteLine(await db.Posts.CountAsync());

Add / Update / Remove は通常、追跡状態を変更し、SQLの実行は SaveChanges 時です。自動生成キーを使う場合でも、AddAsync が常に必要なわけではありません。Update はグラフ全体の状態を変更し得るため、API入力をそのまま渡さず、許可した項目だけ反映します。


操作例です。SQLへ変換する式を組み立て、ToListAsync などで実行します。必要な列だけを取得するには Select を使います。

var query = db.Posts
.AsNoTracking()
.Where(p => p.Views >= 5)
.OrderByDescending(p => p.Views)
.ThenBy(p => p.Id)
.Select(p => new { p.Id, p.Title, BlogName = p.Blog.Name });
var rows = await query.ToListAsync();
Console.WriteLine(rows[0].BlogName);
Console.WriteLine(await db.Posts.AnyAsync(p => p.Views > 0));
Console.WriteLine(await db.Posts.SumAsync(p => p.Views));
Console.WriteLine(query.ToQueryString());

IQueryable<T> のまま条件を組み立てます。ToList や AsEnumerable より後の処理はメモリ上で行われます。任意のC#メソッドがSQLに変換できるわけではなく、変換不能な条件は通常、実行時に例外になります。


用途に合ったメソッドを選びます。

API 動作
FindAsync(key) 追跡済みの主キーを先に探し、なければDBを問い合わせる
FirstOrDefaultAsync() 最初の1件。なければ既定値
SingleAsync() 必ず1件。0件・複数件なら例外
SingleOrDefaultAsync() 0件なら既定値、複数件なら例外
AnyAsync() 存在だけを確認
CountAsync() 件数を取得
ToListAsync() 結果をメモリへ実体化

「最初」を決めるには OrderBy が必要です。FindAsync は追跡済みエンティティを返すことがあるため、常に最新のDB状態を取得するAPIではありません。


操作例です。参照だけの問い合わせには AsNoTracking を使えます。追跡しないエンティティの変更は、そのままでは保存されません。

var detached = await db.Posts.AsNoTracking().SingleAsync();
detached.Title = "Not saved";
Console.WriteLine(db.Entry(detached).State);
Console.WriteLine(await db.SaveChangesAsync());
var tracked = await db.Posts.SingleAsync();
tracked.Title = "Saved";
tracked.Version = Guid.NewGuid();
await db.SaveChangesAsync();

同じコンテキストは、原則として同じ主キーのインスタンスを1つだけ追跡します。AsNoTrackingWithIdentityResolution は結果内の同一キーを同じインスタンスへまとめますが、コンテキストに変更を追跡させません。


操作例です。Include は関連エンティティを一緒に取得します。射影先に関連の値だけ必要なら、Select 内で参照すればよく、Include は不要です。

var blogs = await db.Blogs
.AsNoTracking()
.Include(b => b.Posts.Where(p => p.Views >= 5))
.AsSplitQuery()
.ToListAsync();
Console.WriteLine(blogs[0].Posts.Count);
var one = await db.Blogs.SingleAsync();
await db.Entry(one).Collection(b => b.Posts).LoadAsync();
Console.WriteLine(one.Posts.Count);

多段の関連は ThenInclude を使います。AsSplitQuery はコレクションを別クエリで読み込み、JOINによる行数増加を抑えますが、往復回数が増え、同時更新時の一貫性にも注意が必要です。

追跡ありのfiltered Includeでは、以前追跡した関連エンティティがナビゲーションに混ざる場合があります。遅延読み込みは別途設定が必要で、ループ中のアクセスによるN+1クエリにも注意します。

公式 — 関連データの一括読み込み


操作例です。オフセット方式はページ番号に向き、キーセット方式は次ページへの移動に向きます。並び順を一意にします。

int pageSize = 20;
int pageNumber = 1;
var page = await db.Posts.AsNoTracking()
.OrderBy(p => p.Id)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
int lastId = 0;
var next = await db.Posts.AsNoTracking()
.Where(p => p.Id > lastId)
.OrderBy(p => p.Id)
.Take(pageSize)
.ToListAsync();
Console.WriteLine($"{page.Count}, {next.Count}");

実際の次ページには前ページ最後の Id を渡します。複数列で並べ替えるなら、カーソルの条件にも全ての並べ替え列を含めます。ページサイズは上限を設け、条件と並び順に合うインデックスを検討します。


操作例です。ExecuteUpdateAsync / ExecuteDeleteAsync はEF Core 7以降のリレーショナルDB向けAPIです。エンティティを読み込まずに直接SQLを実行します。

int updated = await db.Posts.Where(p => p.Views < 100)
.ExecuteUpdateAsync(setters => setters.SetProperty(p => p.Views, p => p.Views + 1));
int deleted = await db.Posts.Where(p => p.Views == 0).ExecuteDeleteAsync();
Console.WriteLine($"{updated}, {deleted}");

SaveChanges は不要です。変更トラッカーとは同期されないため、同じ行の追跡あり更新と混在させるなら再取得などが必要です。各呼び出しは別の操作で、複数の一括操作が自動的に1つのトランザクションになるわけではありません。並行性トークンも自動確認・更新されません。

公式 — ExecuteUpdate / ExecuteDelete


操作例です。プロバイダーが対応していれば、1回の SaveChanges は既定でトランザクション内で実行されます。複数の保存や直接SQLをまとめるには明示的なトランザクションを使います。

await using var transaction = await db.Database.BeginTransactionAsync();
try
{
db.Blogs.Add(new Blog { Name = "Transactions" });
await db.SaveChangesAsync();
await db.Posts.ExecuteUpdateAsync(s => s.SetProperty(p => p.Views, p => p.Views + 1));
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}

ロールバックしても追跡済みオブジェクトの値は元に戻りません。必要に応じてコンテキストを作り直します。再試行実行戦略を有効にしたプロバイダーでは、明示的トランザクション全体を実行戦略の単位として扱う必要があります。


操作例です。共通モデルの Version を使い、他のコンテキストによる先行更新を検出します。SQLiteにはSQL Serverの rowversion 相当がないため、ここではアプリがトークンを更新します。

await using var other = new BlogContext(options);
var mine = await db.Posts.SingleAsync();
var theirs = await other.Posts.SingleAsync();
theirs.Title = "Changed elsewhere";
theirs.Version = Guid.NewGuid();
await other.SaveChangesAsync();
mine.Title = "My change";
mine.Version = Guid.NewGuid();
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
await db.Entry(mine).ReloadAsync();
Console.WriteLine(mine.Title);
}

この例は自分の変更を破棄して再読み込みします。実際にはユーザーへの競合通知、値のマージ、条件付き再試行などを選びます。トークンは保護対象の更新経路全てで更新します。

公式 — 競合の処理

SQL Serverの rowversion は、楽観ロックに使えるDB自動生成の8バイトのバイナリ値です。属性名は [Timestamp] ですが、日時ではありません。RowVersion は単なるプロパティ名で、[Timestamp] または .IsRowVersion() による設定が必要です。

以下はSQLiteの操作例とは別の SQL Server専用 の宣言です。Microsoft.EntityFrameworkCore.SqlServer 10.0.12を追加し、UseSqlServer(connectionString) でコンテキストのオプションを作り、SQL Server向けのマイグレーションでスキーマを作成します。接続文字列は構成から取得します。

using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
public sealed class SqlServerDocument
{
public int Id { get; set; }
[MaxLength(200)]
public string Title { get; set; } = "";
[Timestamp]
public byte[] RowVersion { get; set; } = [];
}
public sealed class SqlServerContext(DbContextOptions<SqlServerContext> options)
: DbContext(options)
{
public DbSet<SqlServerDocument> Documents => Set<SqlServerDocument>();
}

SQL ServerはINSERT時と行のUPDATE時に新しい値を生成します。同じ値を設定するUPDATEでも変わります。EFは追跡ありの更新・削除で、主キーと読み込み時のトークンを条件に含めます。他の処理が行を更新・削除し、対象が0件になった場合は DbUpdateConcurrencyException になります。保存成功後はEFが生成値を取得するため、アプリで新しいトークンを代入しません。

指定 値の更新 主な用途
[Timestamp] / IsRowVersion() DBで生成 SQL Serverの byte[] / rowversion
[ConcurrencyCheck] / IsConcurrencyToken() この設定だけでは生成されない アプリ管理の Guid や任意のプロパティ

SQLiteに [Timestamp] を付けてもrowversionの自動生成機能は追加されません。前述のアプリ管理トークンを使います。ExecuteUpdate / ExecuteDelete ではEFの自動競合検出を通らないため、元のトークンを明示的に条件へ入れ、影響行数を確認します。

API・編集画面から更新する場合

Section titled “API・編集画面から更新する場合”

ユーザーが編集を開始した時点のトークンを、例えばBase64としてクライアントへ返し、更新要求で受け取って byte[] に戻します。保存時に最新の行を読み直すだけでは、画面を開いてから保存するまでの競合を見逃します。以下のヘルパーは、上記SQL Serverの宣言と同じプロジェクトの別ファイルに置きます。

using Microsoft.EntityFrameworkCore;
public static class DocumentUpdates
{
public static async Task<byte[]> UpdateTitleAsync(
SqlServerContext db, int id, string title, byte[] originalRowVersion,
CancellationToken cancellationToken = default)
{
var document = await db.Documents.SingleAsync(d => d.Id == id, cancellationToken);
db.Entry(document).Property(d => d.RowVersion).OriginalValue = originalRowVersion;
document.Title = title;
await db.SaveChangesAsync(cancellationToken);
return document.RowVersion;
}
}

保存後の新しいトークンを戻します。呼び出し側で入力検証・認可を行い、対象行がない場合と競合を区別し、DbUpdateConcurrencyException はHTTP 409や再読み込みの案内などで扱います。変更が検出されない場合、SaveChanges はUPDATEを送らず、競合チェックも行いません。競合後はコンテキストの状態を解決または破棄し、そのまま無条件に再試行しません。

EF Coreの競合処理 / SQL Serverのrowversion


ソフトデリート・クエリフィルター

Section titled “ソフトデリート・クエリフィルター”

操作例です。共通モデルの名前付きフィルターにより、IsDeleted がtrueの投稿は通常の問い合わせから除外されます。

var post = await db.Posts.SingleAsync();
post.IsDeleted = true;
post.Version = Guid.NewGuid();
await db.SaveChangesAsync();
Console.WriteLine(await db.Posts.CountAsync());
var all = await db.Posts.IgnoreQueryFilters(["SoftDelete"]).ToListAsync();
Console.WriteLine(all.Count);

フィルターは Remove をソフトデリートへ変換しません。削除フラグを設定する処理が必要です。引数なしの IgnoreQueryFilters() は全フィルターを無効にします。フィルターだけを認可境界にせず、必要な認可を行います。

公式 — グローバルクエリフィルター


操作例です。FromSql には補間文字列を直接渡すと、値がパラメーターとして渡されます。

string title = "Hello EF Core";
var posts = await db.Posts
.FromSql($"SELECT * FROM Posts WHERE Title = {title}")
.AsNoTracking()
.ToListAsync();
Console.WriteLine(posts.Count);

外部入力を文字列連結して FromSqlRaw に渡しません。列名などの識別子はパラメーター化できないため、動的にするなら許可リストで選びます。エンティティを返すSQLには、その型にマッピングされた列が必要です。

公式 — SQLクエリ


以降は、別のファイルDB blog.db を使う手順です。BlogContextFactory.cs を追加します。設計時ファクトリーにより、ツールが学習用 Program.cs を実行せずにコンテキストを作れます。

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
public sealed class BlogContextFactory : IDesignTimeDbContextFactory<BlogContext>
{
public BlogContext CreateDbContext(string[] args)
{
var options = new DbContextOptionsBuilder<BlogContext>()
.UseSqlite("Data Source=blog.db")
.Options;
return new BlogContext(options);
}
}

実際のアプリが blog.db を使う場合は、実行時にも同じDBを指定します。上記の学習用 Program.cs は引き続きインメモリDBを使います。接続先に認証情報が必要なら、ソースへ埋め込まず構成やシークレット管理から取得します。

プロジェクトディレクトリで実行します。database update は上記ファクトリーの学習用DBを変更します。

Terminal window
dotnet ef migrations add InitialCreate --context BlogContext
dotnet ef migrations list --context BlogContext
dotnet ef migrations script 0 InitialCreate --output migration.sql --context BlogContext
dotnet ef database update --context BlogContext
dotnet ef migrations has-pending-model-changes --context BlogContext

モデルを変更したら、変更内容を表す名前で次のマイグレーションを追加します。生成されたコードとSQLを確認してから適用します。未適用の最後のマイグレーションを取り消すには dotnet ef migrations remove --context BlogContext を使います。適用済みのものをファイルだけ削除しません。

本番ではレビュー済みSQLやmigration bundleなどをデプロイ工程で適用します。SQLiteは冪等スクリプト --idempotent に対応しません。SQLiteでは上記のように開始・終了マイグレーションを指定します。

公式 — マイグレーションの適用 / SQLiteの制約


マイグレーションで作成済みの blog.db から、別の出力ディレクトリへモデルを生成する例です。

Terminal window
dotnet ef dbcontext scaffold "Data Source=blog.db" Microsoft.EntityFrameworkCore.Sqlite --output-dir Scaffolded --context ReverseContext --no-onconfiguring

生成コードは確認してから使います。DBから復元できるのはスキーマ由来の情報で、クエリフィルターやアプリ管理の並行性トークンなどの意図は復元されません。--no-onconfiguring を使った場合は、実行時に接続設定を渡します。


DbContext は短い作業単位に使い、使い終わったら破棄します。同じインスタンスを複数スレッドや未完了の非同期処理で共有しません。

  • ASP.NET Coreの AddDbContext は既定でscoped登録です。通常はリクエスト単位で使います。
  • バックグラウンド処理など、DIスコープと作業単位が一致しない場合は IDbContextFactory<T> を検討します。作成したコンテキストは呼び出し側で破棄します。
  • 同じコンテキストへのクエリを Task.WhenAll で同時実行しません。並列処理には別コンテキストを用意します。
  • ToListAsync(cancellationToken) / SaveChangesAsync(cancellationToken) へキャンセルトークンを渡せます。キャンセルの対応はプロバイダーにも依存します。
  • SQLiteの非同期APIは基盤の制約により同期的に実行されるため、この例だけで他DBの非同期性能を判断しません。

公式 — DbContextの構成と寿命 / SQLiteの非同期制約


操作例です。ToQueryString はSQLの確認用で、クエリを実行しません。実際の実行SQLや時間はログ・DB側の実行計画も確認します。

var query = db.Posts
.TagWith("Posts list")
.AsNoTracking()
.Where(p => p.BlogId == blog.Id)
.OrderBy(p => p.Id)
.Take(20);
Console.WriteLine(query.ToQueryString());
  • 必要な列だけ射影し、全件取得を避けます。
  • N+1、不要な Include、巨大なJOIN、オフセットの大きいページングを確認します。
  • EnableSensitiveDataLogging は値をログへ含めます。常時有効にせず、必要な開発環境に限定します。
  • 実行計画を見てインデックスを判断します。EFのAPI名だけで性能を決めません。

操作例です。保存後に別コンテキストで読み直し、追跡済みオブジェクトだけを検証しないようにします。

db.Posts.Add(new Post { BlogId = blog.Id, Title = "Persistence test" });
await db.SaveChangesAsync();
await using var verification = new BlogContext(options);
var saved = await verification.Posts.AsNoTracking()
.SingleAsync(p => p.Title == "Persistence test");
if (saved.Id <= 0 || saved.BlogId != blog.Id)
throw new InvalidOperationException("Persistence check failed.");
Console.WriteLine("Persistence check passed.");

これは例外で失敗を示す最小の実行確認です。テストプロジェクトではMSTestなどのAssertへ置き換えます。

SQLiteのインメモリDBはリレーショナルな制約やSQLの一部を試せますが、本番のSQL ServerやPostgreSQLの代替にはなりません。重要なクエリ、照合順序、トランザクション、マイグレーションは本番と同じDBエンジンでも検証します。EF Core InMemoryプロバイダーはリレーショナルDBの動作を再現しません。

公式 — テスト戦略


バージョン・プロバイダーごとの注意点

Section titled “バージョン・プロバイダーごとの注意点”

このページの例はEF Core 10のSQLiteプロバイダーを前提にしています。

機能・用途 注意点
一括更新・削除 ExecuteUpdate / ExecuteDelete はEF Core 7以降
名前付きクエリフィルター EF Core 10以降。旧版では1つの式に && でまとめる
SQL Serverの並行性制御 DB生成の rowversion を利用可能。SQLiteでは同じ指定を流用しない
文字列長・数値精度 DBによって制約・型・変換可能な操作が異なる
SQLiteのスキーマ変更 一部の操作はテーブル再構築が必要。スキーマ・シーケンスは非対応
DBプロバイダー SQL Serverは UseSqlServer、SQLiteは UseSqlite、Npgsqlは UseNpgsql。別パッケージが必要

メジャー更新時は破壊的変更とプロバイダーの対応状況を確認します。このページの例だけで他プロバイダーのSQL変換や挙動まで保証するものではありません。

EF Core 10の新機能 / プロバイダー一覧


EF Core公式ドキュメント