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仲間いたら、どんどんシェア待ってるぜ!!

執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

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

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

コメント

コメントする

CAPTCHA


目次