ASP.NET Core(.NET 8)+Entity Framework Coreで「Unable to resolve service for type ‘Microsoft.EntityFrameworkCore.DbContext’」が出る――このエラーは、DI(依存性注入)に登録した型と、コンストラクタで要求している型が“微妙に”違うだけで発生します。本記事では、なぜ起きるのかを内部動作から解説し、現場で最短で直す手順(具体型の注入、基底型マッピング、命名改善、アーキテクチャ改善)をコード付きで網羅します。Cost Control Systemの実例も交え、チェックリストや落とし穴、テスト戦略まで一気通貫で整理しました。
エラーの概要と再現条件
System.InvalidOperationException:
Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContext'
while attempting to activate 'SCC.Controllers.LoginController'.
状況:Program.cs では次のように DBContext(自作の派生型)を登録しています。
builder.Services.AddDbContext<DBContext>(options =>
options.UseNpgsql(connectionString) /* 省略 */);
一方、LoginController のコンストラクタでは 基底型の DbContext を要求していました。
public class LoginController : Controller
{
private readonly DbContext _db; // ← 基底型を要求
public LoginController(DbContext context)
{
_db = context;
}
}
この「登録されたサービスの型」と「要求された型」の不一致が、まさに例外の原因です。
原因:DI登録と要求型の不一致(型一致は厳密)
AddDbContext<TContext> は、TContext(= あなたの派生コンテキスト型)と、DbContextOptions<TContext> を スコープド(Scoped) で登録します。ここで重要なのは、DIコンテナは基本的に「完全一致」または「明示マップ」された型しか解決しないということです。
DbContext(基底)を要求すると、DIはDbContextとして登録されたサービスを探します。- しかし登録されているのは
DBContext(派生)であって、DbContextそのものではありません。 - よって「解決できない(Unable to resolve)」という例外になります。
これは .NET の「型安全なDI」の設計に沿った自然な挙動です。基底型を要求して暗黙に派生型へフォールバックすることはありません。
最短解決:推奨アプローチと代替策
| 対応 | 内容 | 効果 |
|---|---|---|
| A. コントラクタで具体型を注入(推奨) | LoginController(DbContext) を LoginController(DBContext) に変更 | 登録済みサービスと一致し、例外が解消 |
| B. 基底型で受けたい場合 | AddScoped<DbContext>(sp => sp.GetRequiredService<DBContext>()) を追加 | DIが DbContext を解決可能に |
| C. 名前衝突の回避 | DBContext は DbContext と紛らわしい。SccContext などに改名 | 可読性向上・誤認防止 |
| D. アーキテクチャ改善 | コントローラに直接 DbContext を入れず、IUserRepository 等のサービス越しにデータ操作 | テスト容易性・責務分離が向上 |
A. コントラクタで具体型を注入(推奨)
最もシンプルで安全です。コントローラのコンストラクタで、登録した派生型を受け取るように変更します。
[ApiController]
[Route("api/[controller]")]
public class LoginController : ControllerBase
{
private readonly DBContext _db;
public LoginController(DBContext db) => _db = db;
[HttpPost("sign-in")]
public async Task<IActionResult> SignIn([FromBody] LoginRequest req, CancellationToken ct)
{
var user = await _db.Users.AsNoTracking()
.FirstOrDefaultAsync(u => u.Login == req.Login, ct);
return user is null ? Unauthorized() : Ok();
}
}
Program.cs はそのままでOKです(既に AddDbContext<DBContext> しているため)。
B. それでも基底型 DbContext で受けたい場合
既存コードを広範に変更したくないときや、抽象化の都合で基底型を受けたい場合は、明示マッピングを追加します。
builder.Services.AddDbContext<DBContext>(options =>
options.UseNpgsql(connectionString));
// 基底型 -> 派生型へのマッピングを明示
builder.Services.AddScoped(sp => sp.GetRequiredService());
こうすると、同一スコープ内では DbContext も DBContext も 同じインスタンスを指します(二重生成はされません)。ただし「どの派生型を代表させるか」が暗黙になりやすいので、プロジェクトが大きくなるほど A案(具体型注入) の方が読みやすく安全です。
C. 紛らわしい命名をやめる(例:DBContext → SccContext)
例外メッセージの Microsoft.EntityFrameworkCore.DbContext と、あなたの DBContext は一文字違い。人間にもツールにも判別しづらく、レビューやログ読解時にミスを誘発します。命名をプロジェクト文脈に寄せると(例:SccContext、CostControlContext)可読性が大幅に上がります。
public class SccContext : DbContext
{
public SccContext(DbContextOptions<SccContext> options) : base(options) {}
public DbSet<User> Users => Set<User>();
}
builder.Services.AddDbContext<SccContext>(opts => opts.UseNpgsql(connectionString)); </code></pre>
<h3>D. アーキテクチャ改善:コントローラに <code>DbContext</code> を直接注入しない</h3>
<p>業務ロジックの肥大化やテスト性低下を防ぐため、コントローラでは <strong>アプリケーションサービス/リポジトリ</strong> を注入し、コンテキストは下位レイヤーで閉じ込める構成を推奨します。</p>
<pre><code class="language-csharp">public interface IUserRepository
{
Task<User?> FindByLoginAsync(string login, CancellationToken ct = default);
}
public sealed class UserRepository : IUserRepository
{
private readonly SccContext _db;
public UserRepository(SccContext db) => _db = db;
public Task<User?> FindByLoginAsync(string login, CancellationToken ct = default) =>
_db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Login == login, ct);
}
// DI 登録
builder.Services.AddScoped<IUserRepository, UserRepository>(); </code></pre>
<pre><code class="language-csharp">[ApiController]
[Route("api/[controller]")]
public class LoginController : ControllerBase
{
private readonly IUserRepository _repo;
public LoginController(IUserRepository repo) => _repo = repo;
[HttpPost("sign-in")]
public async Task<IActionResult> SignIn([FromBody] LoginRequest req, CancellationToken ct)
{
var user = await _repo.FindByLoginAsync(req.Login, ct);
return user is null ? Unauthorized() : Ok();
}
}
</code></pre>
<h2>実装チェックリスト(.NET 8 / EF Core)</h2>
<ol>
<li><strong>派生コンテキストの継承とコンストラクタ</strong>
<pre><code class="language-csharp">public class SccContext : DbContext
{
public SccContext(DbContextOptions<SccContext> options) : base(options) {}
}
</code></pre>
</li>
<li><strong>DI 登録の順序</strong>:<code>AddDbContext<SccContext>()</code> は、<code>SccContext</code> を用いるサービスの登録よりも前に書く。</li>
<li><strong>ログ出力の確認</strong>:例外が出たら、ログ(Serilog など)で「解決しようとした型」と「アクティベート対象」を確認(どのクラスのコンストラクタで失敗したのか)。</li>
<li><strong>検証を厳格化</strong>:ビルド時にDIミスを早期検出。
<pre><code class="language-csharp">builder.Host.UseDefaultServiceProvider(o =>
{
o.ValidateScopes = true;
o.ValidateOnBuild = true;
});
スコープ外利用をしない:DbContext をシングルトンやバックグラウンドスレッドに保持しない。
補足:AddDbContext / AddDbContextPool / AddDbContextFactory の使い分け
| API | ライフタイム | 用途 | 注意点 |
|---|---|---|---|
AddDbContext<T> | Scoped | Webリクエストごとの標準的な使用 | スコープ外に持ち出さない |
AddDbContextPool<T> | Scoped(内部的にプーリング) | 高頻度リクエストでの割当コスト削減 | コンテキストに状態を持たせない前提 |
AddDbContextFactory<T> | Factory(IDbContextFactory<T> を注入) | バックグラウンド処理、明示的にコンテキストを寿命管理したいとき | using / await using で毎回破棄する |
バックグラウンド処理の例:
builder.Services.AddDbContextFactory<SccContext>(opts => opts.UseNpgsql(connectionString));
public class ReportWorker : BackgroundService
{
private readonly IDbContextFactory _factory;
public ReportWorker(IDbContextFactory factory) => _factory = factory;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await using var db = await _factory.CreateDbContextAsync(stoppingToken);
// ここで安全にDBアクセス
}
}
症状別:原因の当たりをつける早見表
| ログ/症状 | ありがちな原因 | 対処 |
|---|---|---|
Unable to resolve service for type 'DbContext' while attempting to activate 'XxxController' | 基底型を要求/派生型のみ登録 | AまたはBで型一致を取る |
スコープ外で ObjectDisposedException | コンテキストをシングルトンへ注入/バックグラウンドで使い回し | IDbContextFactory<T> を採用 |
DbContextOptions<OtherContext> required | オプションのジェネリック型が不一致 | 登録と要求の型を厳密一致させる |
| マイグレーション時に接続文字列が取れない | デザイン時と実行時の設定差 | IDesignTimeDbContextFactory<T> を用意 |
命名とパッケージ構成のベストプラクティス
- コンテキスト名は「機能+Context」(例:
SccContext)。DBContextのような紛らわしい一般名は避ける。 - コンテキストは
Infrastructure(もしくはPersistence)層へ配置し、Domainへ漏らさない。 - エンティティは
DbSet<T>を 必ず派生コンテキスト側に定義し、DbContext側には置かない。
最小再現コード(Before → After)
Before(エラーになる)
// Program.cs
builder.Services.AddDbContext<DBContext>(o => o.UseNpgsql(connectionString));
// LoginController.cs
public class LoginController : ControllerBase
{
public LoginController(DbContext db) { } // ← 基底型
}
After(修正案A:具体型で受ける)
public class LoginController : ControllerBase
{
public LoginController(DBContext db) { } // ← 派生型で一致
}
After(修正案B:基底型マッピング)
builder.Services.AddScoped<DbContext>(sp => sp.GetRequiredService<DBContext>());
実運用で効く追加Tips
検証の自動化(疎通テスト)
public class CompositionRootTests
{
[Fact]
public void ServiceProvider_Should_Resolve_LoginController()
{
var app = Program.CreateAppForTests(); // WebApplicationFactory などで自前実装
using var scope = app.Services.CreateScope();
var controller = scope.ServiceProvider.GetRequiredService<LoginController>();
Assert.NotNull(controller);
}
}
この類のテストを1本入れておくと、DIの破壊的変更をCIで即検出できます。
InMemoryプロバイダーでの高速検証
builder.Services.AddDbContext<SccContext>(opts =>
opts.UseInMemoryDatabase("SccTestDb"));
永続層のI/Oを避けたスモークテストに有効です(本番はNpgsqlに差し替え)。
マイグレーション用のデザイン時ファクトリ
public class SccContextFactory : IDesignTimeDbContextFactory<SccContext>
{
public SccContext CreateDbContext(string[] args)
{
var options = new DbContextOptionsBuilder<SccContext>()
.UseNpgsql("Host=localhost;Database=Scc;Username=postgres;Password=pass")
.Options;
return new SccContext(options);
}
}
実行環境の構成に依存せず、dotnet ef コマンドを安定化できます。
ライフタイムとアンチパターン
| やってしまいがち | なぜダメか | 代替 |
|---|---|---|
DbContext をシングルトンサービスに注入 | スレッド不安全/接続リーク/破棄タイミング不定 | シングルトンには IDbContextFactory を注入 |
| コントローラから直接巨大なLINQを連発 | 責務過多・テスト困難 | アプリケーションサービス/CQRSなどで切り分け |
複数の DbContext を基底型で一括受け | どの実体が来るか不明瞭で保守困難 | 各ユースケースで具体型を明示 |
Npgsql(PostgreSQL)を使う際の実務ノート
- 接続プールは標準で有効。
DbContextの寿命は短く、接続はプールへ返るため、むしろ「早く捨てる」のが正解。 UseNpgsqlのオプション(タイムアウト、リトライ、コマンドバージョニングなど)は、派生コンテキストの登録時にまとめて設定する。- トランザクションは必要最小限に。複数APIをまたがる大域トランザクションは避け、集約内で完結させる。
「基底型で受けたい」場面を見極める
どうしても抽象化したいときは インターフェイス を定義し、派生コンテキストを隠したサービスを注入しましょう。DbContext 自体を抽象化の単位にするのではなく、ユースケースを抽象化の単位にするのが、長期的にメンテしやすいアプローチです。
ログで確認すべきポイント
- ActivationExceptionの箇所:どのクラスのコンストラクタで落ちているか。
- 要求された型:
Microsoft.EntityFrameworkCore.DbContextになっていないか。 - 登録済みの型:
DBContext/SccContextなど派生型のみになっていないか。
まとめ:原因は単純、治し方は明確
- DIは登録した型と要求する型の一致が大前提。
- 最短修正は具体型をコンストラクタで注入(A案)。
- 既存コード保持なら基底型マッピング(B案)でも可。
- 命名を改善(C案)、構造を整える(D案)と、再発しにくい設計に。
結果:本記事のケースでは「A. コンストラクタで具体型を注入する」を採用し、問題は解決しました。
付録:参考実装ひな型(.NET 8 Minimal Hosting)
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("Default");
// 1) DbContext 登録
builder.Services.AddDbContext(opts => opts.UseNpgsql(connectionString));
// 2) リポジトリ・サービス
builder.Services.AddScoped();
// 3) コントローラ
builder.Services.AddControllers();
// 4) DI検証を強化(任意)
builder.Host.UseDefaultServiceProvider(o => { o.ValidateOnBuild = true; o.ValidateScopes = true; });
var app = builder.Build();
app.MapControllers();
app.Run();
// SccContext.cs
public class SccContext : DbContext
{
public SccContext(DbContextOptions options) : base(options) {}
public DbSet Users => Set();
}
// LoginController.cs
[ApiController]
[Route("api/[controller]")]
public class LoginController : ControllerBase
{
private readonly IUserRepository _repo;
public LoginController(IUserRepository repo) => _repo = repo;
[HttpPost("sign-in")]
public async Task<IActionResult> SignIn([FromBody] LoginRequest req, CancellationToken ct)
=> (await _repo.FindByLoginAsync(req.Login, ct)) is null ? Unauthorized() : Ok();
}
チェックリスト(再掲:貼って使える版)
| 項目 | 確認内容 | OK条件 |
|---|---|---|
| コンテキスト継承 | public class SccContext : DbContext | Yes |
| コンストラクタ | SccContext(DbContextOptions<SccContext> options) | Yes |
| DI登録 | AddDbContext<SccContext>(...) | Yes |
| 注入型 | コントローラ/サービスで 具体型 を注入 | Yes(またはB案のマッピング) |
| 命名 | DBContext のような曖昧名を避ける | Yes |
| 検証 | ValidateOnBuild と ValidateScopes を有効化 | Yes |

コメント