ASP.NET MVC 5 で DI は業務系に要るのか — 入れない派の論点も書く

みなさんこんにちは!ヒロポンです!!

今回は ASP.NET 生存ガイド連載・第5回 の本論記事。WinForms 業務SE がガチで迷うやつ!!の話。

ASP.NET MVC 5 案件のレビューで、若手から「DI 入れてないんですか??」「モダンWeb の必須スキルですよ??」って詰められて動揺したこと、ないっすか??ん?DI ってそんなに必要なんだっけ??って手が止まる瞬間。

俺も流通系SIer 時代に同じ場面を経験しました。DI なしで動いてる ASP.NET MVC 5 案件 を回してた時、別チームの若手から「DI 入れない設計はレガシー」みたいなことを言われて、ちょっと萎縮した時期があるんですよね。でも別案件で Unity DI を入れた中規模案件 も並行で回した結果、業務系には『入れる場面』と『入れない場面』がある という現実が見えてきた。

結論から言うと、DI は業務系で要る場面と要らない場面がある。判断軸は3点だけ:

  • チーム規模が3人以上 + テスト書く文化 → 入れる方が結局楽
  • 1〜2人で短期保守だけの小規模案件 → 入れなくていい、生 new で十分
  • DbContext のライフサイクル管理が綺麗にしたい → 入れる

連載第1回(Form ↔ Razor View)から第4回(ORM 3択)までで View / Controller / Routing / ORM の流れを押さえたので、今回は 「Controller 内部のサービス層構造」 を扱います。連載通奏低音の レイヤー分離思考 が、今回では 「Controller レイヤー / Service レイヤー / Repository レイヤー」の3層分離 に再拡張されます。

この記事では VS2019 / .NET Framework 4.7.2 / C# 7.3 / ASP.NET MVC 5 環境で、DI なし → サービスロケータ → コンストラクタインジェクション の段階構築、入れる派と入れない派の両論点Unity DI コンテナの実例 をコード5本でまとめます。後ろの「現場メモ」で、業務系チームでルール化した時の話も書いてる。

3行で結論:

  • DI は手段、業務系では『入れる/入れない』を案件の性質で選ぶ(宗教論争にしない)
  • 判断軸3点: チーム規模3人以上+テスト文化 → 入れる / 1〜2人短期保守 → 入れない / DbContext 管理キレイにしたい → 入れる
  • 連載通奏低音: 「Controller / Service / Repository の3層分離」で DI が解決する関心事を整理
目次

俺の体験 — DI 入り案件と DI なし案件、両方回した話

正直に書いておきます。俺が流通系SIer 時代に経験した両方の案件:

  • DI なし ASP.NET MVC 5 案件: 2人体制・1年程度の短期保守。Controller 内で new CustomerService() していて、テストは書いてなかった。これでも普通に動いて納品まで届いた。
  • Unity DI 入り ASP.NET MVC 5 案件: 4人体制・中規模・テスト書く文化あり。DbContext は Unity の PerRequestLifetimeManager で登録、Service / Repository も注入。チームメンバー全員が DI を理解していて、追加開発がスムーズだった。

業務系の現実: 「DI = モダンWeb の必須スキル」という煽りは業務系には当てはまらない。チーム規模・テスト文化・保守期間で判断軸が変わる。連載通奏低音の 「煽らない・業務系の現実」 トーンが今回でも一番効くポイントっす。

DI の3段階理解 — DI なし → サービスロケータ → コンストラクタインジェクション

業務SE が DI を理解する時、段階構築で見ると分かりやすい。

段階1: DI なし — Controller 内で直接 new

最もシンプル。動くがテストが書きにくい:

// ✅ 段階1: DI なし(Controller 内で new)
public class CustomerController : Controller
{
    private CustomerService _service;

    public CustomerController()
    {
        _service = new CustomerService(new SqlConnection(connStr));
    }

    public ActionResult Index()
    {
        var customers = _service.GetActive();
        return View(customers);
    }
}

ポイント:

  1. Controller のコンストラクタで Service を直接 new
  2. 動く。それは事実
  3. 欠点: テストで _service を差し替えられない / Controller が CustomerService の具体実装に密結合
  4. メリット: 学習コストゼロ / デバッグでスタックトレースが追いやすい

業務系の小規模案件(1〜2人 + 短期保守)では、これで十分成立します。「DI 入れない選択肢も業務系では現実的」 という俺の判断軸の根拠です。

段階2: サービスロケータ — アンチパターン認定

「グローバルファクトリで Service を取る」パターン。現代ではアンチパターン扱い:

// ❌ 段階2: サービスロケータ(アンチパターン扱い)
public static class ServiceLocator
{
    public static ICustomerService GetCustomerService()
        => new CustomerService(new SqlConnection(ConfigurationManager.ConnectionStrings["MyDb"].ConnectionString));
}

public class CustomerController : Controller
{
    public ActionResult Index()
    {
        var service = ServiceLocator.GetCustomerService();   // ← 静的取得
        return View(service.GetActive());
    }
}

なぜアンチパターンか:

  • 依存関係が外から見えない(コンストラクタを見ても Controller が何に依存しているか分からない)
  • テストで差し替えしづらい(静的メソッドを差し替えるには Mocking フレームワークの黒魔術が要る)
  • DI ありの中途半端な代替にしかならず、結局問題が残る

DI なしの直接 new よりも筋が悪いケースが多いので、業務系では選ばないのが鉄則っす。

段階3: コンストラクタインジェクション — DI コンテナの本命

依存先をコンストラクタで受け取る。DI コンテナが自動配線:

// ✅ 段階3: コンストラクタインジェクション(DI 本命)
public class CustomerController : Controller
{
    private readonly ICustomerService _service;

    public CustomerController(ICustomerService service)
    {
        _service = service;   // ← DI コンテナが注入してくれる
    }

    public ActionResult Index()
    {
        return View(_service.GetActive());
    }
}

ポイント:

  1. コンストラクタ引数で依存関係が明示的(外から見て何に依存しているか一目瞭然)
  2. テストでモックを差し替え可能new CustomerController(new FakeService()) で単体テスト書ける)
  3. DI コンテナが配線(Unity / Autofac が自動で ICustomerService の実装を解決)
  4. Single Responsibility が守りやすい(コンストラクタ引数の数 = 責務の広さの指標)

ASP.NET MVC 5 では、DI コンテナの設定で Controller のコンストラクタ引数を自動解決する仕組みが入ります。

Unity DI コンテナの設定 — ASP.NET MVC 5 への組み込み

ASP.NET MVC 5 で Unity を使う最小コードがこんな感じになります:

PM> Install-Package Unity.Mvc
// ✅ 定石: Unity DI コンテナ設定(App_Start/UnityConfig.cs)
using Unity;
using Unity.Lifetime;
using Unity.AspNet.Mvc;

public static class UnityConfig
{
    public static void RegisterComponents()
    {
        var container = new UnityContainer();

        // インターフェース → 実装の配線
        container.RegisterType<ICustomerService, CustomerService>();
        container.RegisterType<ICustomerRepository, CustomerRepository>();

        // DbContext はリクエストスコープで登録(連載第4回 ORM の話と直結)
        container.RegisterType<MyDbContext>(new PerRequestLifetimeManager());

        // ASP.NET MVC に Unity を組み込む
        DependencyResolver.SetResolver(new UnityDependencyResolver(container));
    }
}

// Global.asax.cs で呼び出し
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    UnityConfig.RegisterComponents();
    RouteConfig.RegisterRoutes(RouteTable.Routes);
}

ポイント:

  1. Install-Package Unity.Mvc で導入(ASP.NET MVC 用のアダプタ込み)
  2. RegisterType<TInterface, TImplementation>() で配線
  3. PerRequestLifetimeManager で DbContext をリクエストスコープに(連載第4回の DbContext ライフサイクル管理と直結)
  4. Global.asax.csRegisterComponents() を呼ぶ

これで Controller のコンストラクタ引数が自動配線されて、Controller のテストもモックで書けるようになる。Autofac でも基本構造は同じで、RegisterTypeRegisterInstance などのメソッド名が変わる程度っす。

DbContext のライフサイクル登録 — 連載第4回との接続

連載第4回(ORM 3択)で扱った DbContext のライフサイクル = リクエスト単位 が、DI コンテナでは PerRequestLifetimeManager(Unity)/ InstancePerRequest(Autofac)で表現されます:

// ✅ DbContext のライフサイクル別の挙動
// Singleton: 複数リクエストで同じインスタンス → ❌ 変更追跡が混ざる事故
container.RegisterType<MyDbContext>(new ContainerControlledLifetimeManager());   // ❌ NG

// Transient: 同一リクエスト内でも別インスタンス → ❌ Service A の変更が Service B から見えない
container.RegisterType<MyDbContext>(new TransientLifetimeManager());   // ❌ NG

// ✅ PerRequest: リクエスト単位で1インスタンス、リクエスト終了で Dispose
container.RegisterType<MyDbContext>(new PerRequestLifetimeManager());   // ✅ 本命

業務系の鉄則: DbContextPerRequestLifetimeManager 一択。Singleton はリクエスト間の状態漏れで一発レッドカード、Transient は同一リクエスト内で変更が反映されない事故になります。

入れる派の論点 — DI を使うメリット4つ

業務系で DI を入れる選択をする時の論点を、4つで整理:

1. テスタビリティ — Controller の単体テストが書ける

new CustomerController(new FakeCustomerService()) でモックを注入して、Controller の Action ロジックを単体テストできる。Service を実 DB に当てずにテストが回せるのは大きい。

2. 依存関係の明示化

コンストラクタ引数を見ると、Controller が何に依存しているか一目瞭然。新人が Controller を読み解く時の認知コストが下がります。

3. DbContext のライフサイクル管理が綺麗

PerRequestLifetimeManager で Controller + Service + Repository が同じ DbContext インスタンスを共有できる。Service 内で using new MyDbContext() を毎回書くより綺麗で、トランザクション管理も楽。

4. 将来の拡張に強い

ICustomerService の実装を CustomerService から CachedCustomerService に切り替える時、DI コンテナの配線1行を変えるだけで全 Controller が新実装に切り替わる。

入れない派の論点 — DI を避ける場面の論点4つ

逆に、業務系で DI を入れない選択を支持する論点も同じくらい妥当:

1. 小規模案件では over-engineering

1〜2人体制・1年程度の保守期間なら、DI コンテナ導入コスト > 得られるメリット。「直接 new」で十分動く

2. DI コンテナの学習コスト

Unity / Autofac の設定構文 + ライフサイクル管理 + 子コンテナの概念は、初学者にとって2〜3週間の学習コスト。チームに DI 経験者がいないと混乱の原因になる。

3. デバッグ時にスタックトレースが追いづらい

DI コンテナが Controller を生成する瞬間、内部でリフレクションを使うので、スタックトレースに Unity 内部のフレームが大量に挟まる。直接 new の方がデバッガで追いやすいケースは普通にある。

4. チーム全員が DI を理解してないと混乱

DI を入れたチームで、メンバーの半分が DI を理解してないと、Controller のコンストラクタを直接呼んで new する人が出てきて、設計が崩れる。チーム全員の理解度を揃えるコストが必要。

DI 採用判断3レベル + DbContext ライフサイクル選択

俺の判断軸 — 中立スタンスで提示

俺が業務系で「入れる/入れない」を判断する時の3軸:

観点 入れる 入れない
チーム規模 3人以上 1〜2人
テスト文化 単体テストを書く テスト未導入
保守期間 2年以上 1年以内
DbContext 管理 キレイにしたい 直接 new で十分
チームの DI 経験 全員理解 半分以下

3つ以上『入れる』側に振れたら入れる、3つ以上『入れない』側に振れたら入れない、というのが業務SE の現実解。「DI 入れない設計はレガシー」という煽り情報に振り回されず、案件の性質で判断するのが本命っす。

ハマりポイント — 実体験ベースの本番事故3点

1. DbContext を Singleton で登録して変更追跡が混ざった(半日デバッガで追ってハマった)

最初に Unity DI を導入した時、DbContextContainerControlledLifetimeManager(Singleton)で登録した事件。ユーザーA の SaveChanges でユーザーB の編集中エンティティが意図せず保存される事故。半日デバッガで追ってハマった末に、PerRequestLifetimeManager に変更して解決。DbContext は PerRequest 一択 を業務系チーム規約に揃えました。

2. DI コンテナの設定漏れで実行時例外(30分溶かした)

新しい INotificationService を追加した時、UnityConfig.cs への RegisterType を書き忘れて、Controller のコンストラクタ解決時に ResolutionFailedException が飛んだ事件。コンパイル時に検出されないのが DI コンテナの弱点で、30分溶かした末に設定漏れと気付いた。UnityConfig.cs を新規 Service 追加時のチェックリストに入れる ルールで再発防止。

3. コンストラクタ引数が増えすぎて Service 分割(数日プロファイラで追った)

ある Controller のコンストラクタが 8個の Service 引数 になっていて、可読性が破綻した事件。数日プロファイラで追った末に「Single Responsibility が破綻している」と判断、3つの Service に分割。コンストラクタ引数 5個以上は Service 責務分割サイン を業務系チーム規約に入れた。

俺の現場メモ — 業務系チームでの DI ルール

流通系SIer時代に過去コードを grep -rnE "DependencyResolver|UnityContainer|RegisterType" . で50箇所近くひっかけたら、DbContext Singleton 登録・サービスロケータ混在・DI コンテナ設定漏れが全部入りだった。後輩と一緒に 3行ルール にまとめた:

  1. チーム規模3人以上 + テスト文化なら DI 寄せ、1〜2人短期保守なら入れない(判断軸を持つ)
  2. DbContext は PerRequestLifetimeManager 一択(Singleton・Transient は禁止)
  3. Service 追加時は UnityConfig.cs への登録をチェックリスト化(実行時例外予防)

このルール化で、DI 周りの本番事故・設定漏れ・コンストラクタ肥大化が消えた。判断軸を持つ + 書き方を揃えるだけで保守工数と事故率が両方下がるおすすめルールっす。

まとめ

状況 推奨
チーム3人以上 + テスト文化 DI 寄せ(コンストラクタインジェクション)
1〜2人 + 短期保守 DI なし(Controller で直接 new で十分)
DbContext ライフサイクル PerRequestLifetimeManager(DI 入れる場合)
サービスロケータ 新規禁止(アンチパターン扱い)
DI コンテナ選定 Unity / Autofac から選ぶ(Ninject は更新停止)
コンストラクタ引数 5個以上 Service 責務分割サイン
新規 Service 追加 UnityConfig.cs への登録チェック

業務系で DI を入れるかの判断は、「案件の性質で決める」「3軸で判断」「DI は手段であって目的じゃない」 の3点で9割困らなくなります。「DI 入れない設計はレガシー」という煽り情報には業務系の現実で対抗する判断軸を持っておくのが業務SE 鉄則。次回(連載第6回)は「業務イントラの認証 — Windows認証 / Forms認証 / Cookie」を扱うので、DI コンテナを認証サービス注入に使う実例も併せて見ていきます。

よくある質問

Q1. ASP.NET MVC 5 の業務系で DI は要りますか?

A. 案件の性質で決まるので一律の答えはありません。俺の判断軸は3点: ①チーム規模が3人以上なら入れる方が結局楽、②テストを書く文化があるチームなら入れる、③1〜2人で短期保守だけなら入れなくていい(生 new で十分)。DI は手段であって目的じゃないので、業務系では「入れない選択肢も成立する」と判断軸を持って選ぶのが現実解です。

Q2. DI コンテナを入れる時、Unity / Autofac / Ninject のどれを選ぶ?

A. ASP.NET MVC 5 + .NET Framework 4.7.2 の業務系では Unity か Autofac が現実的です。Microsoft のサンプルが Unity ベースなので学習コストは Unity が低めですが、Autofac の方が機能豊富で大規模案件向き。チーム既存案件で揃ってるならそれに合わせる、新規ならどちらでもOK。Ninject は更新が止まっているので新規採用は避けるのが業務系の判断軸です。

Q3. DbContext を DI コンテナに登録する時のライフサイクルは?

A. リクエストスコープ(PerRequestLifetimeManager / InstancePerRequest が原則です。Singleton にすると複数リクエストの変更追跡が混ざって事故、Transient にすると同一リクエスト内で別インスタンスが生成されて変更が反映されない。リクエスト単位で1インスタンスを生成して、リクエスト終了で Dispose する形が業務系の鉄板です。連載第4回(ORM 3択)の Controller スコープと同じ考え方を、DI コンテナで自動配線する形になります。

Q4. コンストラクタ引数が増えすぎた時のリファクタは?

A. Service の責務分割サインとして捉えるのが業務SE の判断軸です。コンストラクタに5個以上の Service が並んだら、その Controller / Service の責務が広がりすぎている可能性が高い。Single Responsibility Principle に沿って Service を分割する、または Facade パターンで1つの Service にまとめる、の2択で対処します。「コンストラクタ引数の数 = 責務の広さ」と覚えておくと、リファクタの判断が早くなります。

Q5. サービスロケータパターンと DI は何が違う?

A. 依存関係が明示的かどうかが最大の違いです。サービスロケータは Controller 内で ServiceLocator.Get<IFooService>() のように静的に取得するので、外から見て Controller が何に依存しているか分からない(隠れ依存)。コンストラクタインジェクションだと、コンストラクタ引数から明示的に依存先が分かる。サービスロケータは現代ではアンチパターン扱いで、DI コンテナを使う場合はコンストラクタインジェクション寄せが業務SE 鉄則です。

ここまでで DI の3段階理解・入れる派/入れない派の論点・判断軸は押さえた。次回は業務イントラの認証を扱うので、DI コンテナを認証サービス注入に使う実例も見ていきます。ASP.NET / Interface 抽象化の隣接トピックも貼っておきます。

関連記事

ASP.NET 生存ガイド・連載目次

今回は WinForms 業務SE のための ASP.NET 生存ガイド 全10回の 第5回 です。

以上!

同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次