Entity Framework CoreのDbContextが解決できないエラーをASP.NET Core(.NET 8)で直す:Unable to resolve serviceの原因と対処・設計ベストプラクティス

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&lt;SccContext&gt; options) : base(options) {}
    public DbSet&lt;User&gt; Users =&gt; Set&lt;User&gt;();
}

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&lt;User?&gt; FindByLoginAsync(string login, CancellationToken ct = default);
}

public sealed class UserRepository : IUserRepository
{
private readonly SccContext _db;
public UserRepository(SccContext db) => _db = db;


public Task&lt;User?&gt; FindByLoginAsync(string login, CancellationToken ct = default) =&gt;
    _db.Users.AsNoTracking().FirstOrDefaultAsync(u =&gt; 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) =&gt; _repo = repo;

    [HttpPost("sign-in")]
    public async Task&lt;IActionResult&gt; SignIn([FromBody] LoginRequest req, CancellationToken ct)
    {
        var user = await _repo.FindByLoginAsync(req.Login, ct);
        return user is null ? Unauthorized() : Ok();
    }
}
</code></pre>

<h2>実装チェックリスト(.NET&nbsp;8 / EF&nbsp;Core)</h2>
<ol>
  <li><strong>派生コンテキストの継承とコンストラクタ</strong>
    <pre><code class="language-csharp">public class SccContext : DbContext
{
    public SccContext(DbContextOptions&lt;SccContext&gt; options) : base(options) {}
}
</code></pre>
  </li>
  <li><strong>DI 登録の順序</strong>:<code>AddDbContext&lt;SccContext&gt;()</code> は、<code>SccContext</code> を用いるサービスの登録よりも前に書く。</li>
  <li><strong>ログ出力の確認</strong>:例外が出たら、ログ(Serilog など)で「解決しようとした型」と「アクティベート対象」を確認(どのクラスのコンストラクタで失敗したのか)。</li>
  <li><strong>検証を厳格化</strong>:ビルド時にDIミスを早期検出。
    <pre><code class="language-csharp">builder.Host.UseDefaultServiceProvider(o =&gt;
{
    o.ValidateScopes = true;
    o.ValidateOnBuild = true;
});

スコープ外利用をしない:DbContext をシングルトンやバックグラウンドスレッドに保持しない。

補足:AddDbContext / AddDbContextPool / AddDbContextFactory の使い分け

APIライフタイム用途注意点
AddDbContext<T>ScopedWebリクエストごとの標準的な使用スコープ外に持ち出さない
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&lt;DbContext&gt;(sp =&gt; sp.GetRequiredService&lt;DBContext&gt;());

実運用で効く追加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&lt;LoginController&gt;();
        Assert.NotNull(controller);
    }
}

この類のテストを1本入れておくと、DIの破壊的変更をCIで即検出できます。

InMemoryプロバイダーでの高速検証

builder.Services.AddDbContext&lt;SccContext&gt;(opts =&gt;
    opts.UseInMemoryDatabase("SccTestDb"));

永続層のI/Oを避けたスモークテストに有効です(本番はNpgsqlに差し替え)。

マイグレーション用のデザイン時ファクトリ

public class SccContextFactory : IDesignTimeDbContextFactory&lt;SccContext&gt;
{
    public SccContext CreateDbContext(string[] args)
    {
        var options = new DbContextOptionsBuilder&lt;SccContext&gt;()
            .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 : DbContextYes
コンストラクタSccContext(DbContextOptions<SccContext> options)Yes
DI登録AddDbContext<SccContext>(...)Yes
注入型コントローラ/サービスで 具体型 を注入Yes(またはB案のマッピング)
命名DBContext のような曖昧名を避けるYes
検証ValidateOnBuild と ValidateScopes を有効化Yes

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次