Entity Framework Core チートシート
EF Core 10・.NET 10をベースに、CRUD、LINQ、関連データ、マイグレーション、同時実行制御をSQLiteの例でまとめたチートシートです。
EF CoreとEF6の違い
Section titled “EF CoreとEF6の違い”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メジャーバージョンをサポートしているか確認します。
セットアップ
Section titled “セットアップ”SQLiteのコンソールプロジェクトを作ります。ここでは安定版10.0.12を指定し、Microsoft製EFパッケージとツールのバージョンを揃えます。
dotnet new console -n EfCheatSheet --framework net10.0cd EfCheatSheetdotnet package add Microsoft.EntityFrameworkCore.Sqlite --version 10.0.12dotnet package add Microsoft.EntityFrameworkCore.Design --version 10.0.12dotnet new tool-manifestdotnet tool install dotnet-ef --version 10.0.12dotnet ef --version既存リポジトリでツールマニフェストがある場合は、新規作成せず dotnet tool restore を使います。以降のファイルとコマンドは、このプロジェクトのディレクトリを基準にします。
共通モデル・DbContext
Section titled “共通モデル・DbContext”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の機能です。
モデルの属性(Data Annotations)
Section titled “モデルの属性(Data Annotations)”モデルのクラスやプロパティに属性を付けて設定できます。同じ設定が競合する場合の優先順位は 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とは区別します。
属性を使ったモデル例
Section titled “属性を使ったモデル例”以下は別のエンティティの宣言例で、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が対応する既定値・計算列の設定を使います。
モデル設定と入力検証の違い
Section titled “モデル設定と入力検証の違い”EF Coreの SaveChanges は、Data Annotationsによるオブジェクト検証を自動実行しません。例えば [Range] や [EmailAddress] を付けてもDB制約は作られません。ASP.NET Coreの検証や Validator.TryValidateObject などで入力を検証します。Nullable参照型が有効なら string は規約で必須となり、[Required] はそのモデル設定を明示できます。C#の required は初期化時のルールで、別の機能です。長さ・精度のDBでの強制はプロバイダーにも依存します。
モデル設定 / プロパティのマッピング / 関連の属性設定
最小の実行例
Section titled “最小の実行例”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入力をそのまま渡さず、許可した項目だけ反映します。
LINQ・射影・集約
Section titled “LINQ・射影・集約”操作例です。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の選び方
Section titled “取得APIの選び方”用途に合ったメソッドを選びます。
| API | 動作 |
|---|---|
FindAsync(key) |
追跡済みの主キーを先に探し、なければDBを問い合わせる |
FirstOrDefaultAsync() |
最初の1件。なければ既定値 |
SingleAsync() |
必ず1件。0件・複数件なら例外 |
SingleOrDefaultAsync() |
0件なら既定値、複数件なら例外 |
AnyAsync() |
存在だけを確認 |
CountAsync() |
件数を取得 |
ToListAsync() |
結果をメモリへ実体化 |
「最初」を決めるには OrderBy が必要です。FindAsync は追跡済みエンティティを返すことがあるため、常に最新のDB状態を取得するAPIではありません。
変更追跡・AsNoTracking
Section titled “変更追跡・AsNoTracking”操作例です。参照だけの問い合わせには 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 は結果内の同一キーを同じインスタンスへまとめますが、コンテキストに変更を追跡させません。
関連データの読み込み
Section titled “関連データの読み込み”操作例です。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 を渡します。複数列で並べ替えるなら、カーソルの条件にも全ての並べ替え列を含めます。ページサイズは上限を設け、条件と並び順に合うインデックスを検討します。
一括更新・削除
Section titled “一括更新・削除”操作例です。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
トランザクション
Section titled “トランザクション”操作例です。プロバイダーが対応していれば、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;}ロールバックしても追跡済みオブジェクトの値は元に戻りません。必要に応じてコンテキストを作り直します。再試行実行戦略を有効にしたプロバイダーでは、明示的トランザクション全体を実行戦略の単位として扱う必要があります。
楽観的同時実行制御
Section titled “楽観的同時実行制御”操作例です。共通モデルの 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と[Timestamp]
Section titled “SQL Server:rowversionと[Timestamp]”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() は全フィルターを無効にします。フィルターだけを認可境界にせず、必要な認可を行います。
SQLを直接使う
Section titled “SQLを直接使う”操作例です。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には、その型にマッピングされた列が必要です。
マイグレーション
Section titled “マイグレーション”以降は、別のファイル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を使います。接続先に認証情報が必要なら、ソースへ埋め込まず構成やシークレット管理から取得します。
作成・適用・SQL確認
Section titled “作成・適用・SQL確認”プロジェクトディレクトリで実行します。database update は上記ファクトリーの学習用DBを変更します。
dotnet ef migrations add InitialCreate --context BlogContextdotnet ef migrations list --context BlogContextdotnet ef migrations script 0 InitialCreate --output migration.sql --context BlogContextdotnet ef database update --context BlogContextdotnet ef migrations has-pending-model-changes --context BlogContextモデルを変更したら、変更内容を表す名前で次のマイグレーションを追加します。生成されたコードとSQLを確認してから適用します。未適用の最後のマイグレーションを取り消すには dotnet ef migrations remove --context BlogContext を使います。適用済みのものをファイルだけ削除しません。
本番ではレビュー済みSQLやmigration bundleなどをデプロイ工程で適用します。SQLiteは冪等スクリプト --idempotent に対応しません。SQLiteでは上記のように開始・終了マイグレーションを指定します。
既存DBからモデル生成
Section titled “既存DBからモデル生成”マイグレーションで作成済みの blog.db から、別の出力ディレクトリへモデルを生成する例です。
dotnet ef dbcontext scaffold "Data Source=blog.db" Microsoft.EntityFrameworkCore.Sqlite --output-dir Scaffolded --context ReverseContext --no-onconfiguring生成コードは確認してから使います。DBから復元できるのはスキーマ由来の情報で、クエリフィルターやアプリ管理の並行性トークンなどの意図は復元されません。--no-onconfiguring を使った場合は、実行時に接続設定を渡します。
DbContextの寿命・非同期処理
Section titled “DbContextの寿命・非同期処理”DbContext は短い作業単位に使い、使い終わったら破棄します。同じインスタンスを複数スレッドや未完了の非同期処理で共有しません。
- ASP.NET Coreの
AddDbContextは既定でscoped登録です。通常はリクエスト単位で使います。 - バックグラウンド処理など、DIスコープと作業単位が一致しない場合は
IDbContextFactory<T>を検討します。作成したコンテキストは呼び出し側で破棄します。 - 同じコンテキストへのクエリを
Task.WhenAllで同時実行しません。並列処理には別コンテキストを用意します。 ToListAsync(cancellationToken)/SaveChangesAsync(cancellationToken)へキャンセルトークンを渡せます。キャンセルの対応はプロバイダーにも依存します。- SQLiteの非同期APIは基盤の制約により同期的に実行されるため、この例だけで他DBの非同期性能を判断しません。
公式 — DbContextの構成と寿命 / SQLiteの非同期制約
SQL確認・性能のチェック
Section titled “SQL確認・性能のチェック”操作例です。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変換や挙動まで保証するものではありません。