LoginSignup
22
19

More than 1 year has passed since last update.

[ASP.NET]多層システムにおけるDbContextのバケツリレーをDIコンテナで解消する

Last updated at Posted at 2021-08-03

はじめに

基本、自分用のメモです。

多層システムで構築されたウェブアプリケーションにおいて、DbContextやLoginInfoのようなリクエスト毎に生成・破棄し、全体で共有するオブジェクトを誰が生成し誰が破棄する責務をもつのか、またそれらを各オブジェクトがどこから取得するのかという問題を、DI(依存性オブジェクトの注入)を使って解決する、という記事になります。

既にDIを使い倒している方には今更な内容かと思いますが、私のように古いシステムのメンテナンスをしていた人には有用かと思います。

この記事をかいた理由

ASP.NETでDIコンテナ使ってDbContextを注入するサンプルをいくら検索しても、ControllerでDbContextを受け取っていきなりそこでDBにアクセスする初歩的な実装例しか出てこなくて、「コントローラでDB層にアクセスするとか、そんないい加減なシステムがあってたまるか! 普通データアクセス層はコントローラから隠蔽されてるだろ!! DAO層にどうやってDbContextを注入するのか知りたいんだよ! DIコンテナ使ったことないからわかんねぇんだよ…ウッウッ」と嗚咽を漏らしながら海外のブログ記事などを読み漁った結果、「こうするのかな?」ということは分かったのですが、同じように悩んでる人はいるかもしれないと、自分でこの記事をまとめるに至りました。

この記事でわかること

  • DIコンテナを使わない昔の多層システムでどういう課題があったか
  • ASP.NET Coreの標準DIコンテナで、どうその課題を解決するか
  • ASP.NET(無印) でDIコンテナ「AutoFac」を使って同じことをする為の情報
  • おまけで、トランザクションスクリプトの話
  • おまけで、オブジェクトマッパー「Mapster」の話

概要

ASP.NET MVC 5を使って何年も前に構築されたシステムをなるべく最新の環境でリプレースしていくにあたり、それまで課題だった部分を解決していきます。ASP.NET Core と、ASP.NET無印に両対応しています。

ASP.NETのソースコードはVB.NETですが、ASP.NET CoreのサンプルはC#になっています。

現在の構成

対象となるソリューションは、ASP.NET MVC 5、データアクセス層とビジネスロジック層を分離する為に以下の多層構造を取っており、Entity Framework 6(EF6)のDbContextの生成と破棄の責務をどうするかという問題を抱えています。

Controller -> Logic -> DAO(Data Access Object) -> EF6 or SQL

EF6のDbContextは、EF6のツールによって既存のDBから自動生成されたものです。

規定のコンストラクタでconfigファイルから接続文字列名(以下の例では"MyDbContext")経由で接続文字列を取得するようになっており、例えばテスト用のDBに接続先を切り替えたい場合には、Debug用の環境設定を用意することで、コードを修正せずに対応できるようになっています。

MyDbContext.vb
Partial Public Class MyDbContext
    Inherits DbContext

    Public Sub New()
        MyBase.New("name=MyDbContext")
    End Sub

    Public Overridable Property Employee As DbSet(Of Employee)
    :

DbContextはDAO内でしか利用せず、Logic層からは隠ぺいする為、DAOクラスのメンバ変数としてDbContextを持ち、DAOクラスが生成された時に一緒にDbContextが生成される、という作りになっています。

P101Dao.vb
Public Class P101Dao
    Private db As New MyDbContext   ' MyDbContextの生成を各Daoが受け持つ

    Public Function GetEmployees() As List(Of EmployeeDto)
        Return db.Employee.Where(条件).Select(
            Function(row) New EmployeeDto With {
                .EmpId = row.EmpId
                .EmpName = row.EmpName
                 :
            }).ToList()
    End Function

そして、ControllerがアクションメソッドからLogicを生成し、処理を委譲します。LogicはDAOからDTOとしてデータを取得し、ViewModelに転写して返します。

P101Controller.vb
Public Class P101Controller
    Inherits Controller

    Public Function EmployeeList() As ActionResult
        Dim logic As New P101Logic
        Dim viewmodel = logic.GetEmployees()
        Return View(viewmodel)
    End Function
End Class
P101Logic.vb
Public Class P101Logic
    Public Function GetEmployees() As List(Of EmployeeViewModel)
        Dim dao As New P101Dao
        Return dao.GetEmployees().Select(
            Function(row) New EmployeeViewModel With {
                .EmpId = row.EmpId
                .EmpName = row.EmpName
                 :
            }).ToList()

    End Function
End Class

ロジック層がコントローラーに直接ViewModelを渡している部分などは気になる方もいらっしゃるかと思うのですが、今回はMVCアーキテクチャで、画面側ではこのViewModelをJSON形式で受け取った上でjavascriptライブラリを利用して画面を構築・処理しており、ここで渡されるViewModelはUIに直接依存するというよりは、ビジネスドメインの外部との入出力DTOに近いものです。問題が無いとは言えませんが、そもそもトランザクションスクリプトで作られていますし、落としどころとしては妥当かと思います。

Controllerはほぼ、入力をそのままLogicに渡し、Logicから出力されるViewModelをView(*.vbhtml)に渡すのみとなっています。
処理の多くはLogicとDaoが中心となり、Logicはビジネスロジックというよりは、DBに依存する部分をDAOに移した残りという感じです。

トランザクションスクリプト・アーキテクチャ(余談)

対象となるシステムは、ドメインモデルではなくトランザクションスクリプトというアーキテクチャを採用しています。トランザクションスクリプトとは、トランザクション単位で設計を行う、オブジェクト指向設計とは異なる考え方のアーキテクチャです。

ですので、Logic層は基本的に状態を持たず、逆にモデルはフィールドのみ、つまりDTO(Data Transfer Object)としての役割しか持っていません。

余談になりますが、最初私は、このシステムでオブジェクト指向設計が成されていない事に落胆しました。しかしメンテナンスしていくにつれて、トランザクションスクリプト・アーキテクチャが徹底されたこの構成にも利点はあるのだと理解するに至りました。

トランザクションスクリプト・アーキテクチャでは、View->Controller->Logic->Daoという流れが全て一本道で独立して一貫しており、適切な画面IDを用いてクラス名を管理すれば、ある画面の変更がどのLogicやDaoに関係するのかが一目でわかります。つまりP101Viewの修正は、P101LogicやP101Daoにのみ影響し、その逆もまたしかり、ということです。システムはシンプルな縦割りの階層構造をなしており、横の連携はほぼ起きません。

ですので、初めてそのシステムをメンテナンスする人も、その画面IDがついた各層のクラスだけ見ていけばよく、処理の流れも把握しやすいのです。

縦割りの弊害として、トランザクションスクリプトで良く言われる「処理の重複」については実際に発生しており、これがメンテナンス性の低下を引き起こしていることは否めません。また、ビジネスドメインという視点で機能が分割されていない為、ある業務仕様の変更がどのLogicに影響しているのか把握するのは困難です(トランザクションスクリプトにおいてビジネスドメインに最も似ているのはDBのテーブル構成である為、テーブル名や列名でソース全文検索を行い、検出されたDAOから遡って画面への影響範囲を調べます)。これはビジネスドメインのコア部分の変更時ほど顕著で、トランザクションスクリプトの大きな欠点だと思います。

もしドメインモデルで設計されていたならば、このビジネスドメインの変更はたった一か所の変更で済んだだろうに、と思うことはよくあります。

しかし、それを差し引いても、「画面単位」、良く言えばユースケース単位で設計されたシステムを機械的に実装に落とし込む手法として、トランザクションスクリプト・アーキテクチャのシンプルな考え方は優れているように思います。この方法ならば、難易度の高いオブジェクト指向モデリングに精通した人材がいなくとも、誰でもシステムを構築し、メンテナンスしていけると思います。安価で安全確実、というわけです。

但し、やはりトランザクションスクリプトは、他のシステムに応用が利かない「その場限りのコード」が大量に生産されがちです。前述の通り機能の重複も多く、システムが大きくなればなるほどこの重複部分の修正漏れのリスクが増大し、システムの修正はしにくくなっていきます。処理の流れが分かりやすいとは書きましたが、あくまでも「その流れでの処理」について分かるだけで、システム全体として何がしたいのかは、いつまで経ってもコードから読み取ることはできません。

個人的には、オブジェクト指向モデリングに精通した人材を確保した上で、ドメインモデル・アーキテクチャを採用した方が、システムが長期に渡って再利用できる「資産」になりうるとは思います。

以上、余談でした。

現在の課題

DAOが個別にDbContextの生成を行っている為、以下の問題点があります。

  1. 複数のDaoを同時に利用した場合、無駄にDbContextが生成される
  2. 複数のDaoをまたいだトランザクション管理ができない(UnitOfWorkの実現が面倒)
  3. DbContextのDisposeがGC任せになっており非効率

1.については、リソース管理がシビアでなければ問題とはならない(実際、現在の運用では特に問題になっていない)のですが、2.は割と面倒で、そのような場合には単一のDbContextインスタンスを各DAOにコンストラクタ経由で渡すようにしなければなりません。そして、そのコンストラクタにDbContextを生成して渡す責務は誰が追うのかというと、また別の上位のDAOクラスとなります(UnitOfWork的な役割のクラスです)。

その場合、「今利用しているDbContextは誰が責務を負っているのか」という問題が常に付きまとい、無用なバグの温床になりかねません。

このうち 3.については、DAOクラスおよびLogicクラスをDisposableにして、ControllerのDispose()が呼びされた時にDbContextのDisposeを確実に呼び出すようにするという方法もあります(ControllerのDispose()は、URLのレスポンスが返った後に必ず呼び出されます)。しかし、いちいちDisposeの連鎖を作るのは正直しんどいわけです。

解決方法(?)

DbContextをControllerで生成し、そこから呼び出す各オブジェクトへとコンストラクタ経由でdbcontextのインスタンスを伝搬させていく(DbContextのバケツリレー)。DbContextの生成と破棄をControllerの責務とする。

具体的には次のような感じです。

P101Controller.vb
Public Class P101Controller
    Inherits Controller

    Private db As MyDbContext

    Public Sub New()
        db = New MyDbContext
    End Sub

    Public Function EmployeeList() As ActionResult
        Dim logic As New P101Logic(db)
        Dim viewmodel = logic.GetEmployees()
        Return View(viewmodel)
    End Function

    Public Sub Dispose() Implements IDisposable.Dispose
        db.Dispose()
    End Sub 

End Class
P101Logic.vb
Public Class P101Logic
    Private db As MyDbContext
    Public Sub New(pdb As MyDbContext)
        db = pdb
    End Sub

    Public Function GetEmployees() As List(Of EmployeeViewModel)
        Dim dao As New P101Dao(db)
        Return dao.GetEmployees().Select(
            Function(row) New EmployeeViewModel With {
                .EmpId = row.EmpId
                .EmpName = row.EmpName
                 :
            }).ToList()

    End Function
End Class

P101Daoの実装までは書きませんが、P101Daoも、コンストラクタで渡されたdbを内部でそのまま使うようになります。これで、DbContextのインスタンスはリクエスト毎に1つとなり、トランザクション管理もOK、生成と破棄についても、Controllerに任せておけば安心、という事になります。

ただ、ここで一つ問題があります。

ControllerやLogicがMyDbContextに依存するようになっている

本来、ControllerやLogicはdbを意識する必要はありません。ここで無駄な依存関係が発生してしまうと、もし将来DBの変更が起こった時、いちいちControllerやLogicまで影響を受けてしまいますし、下手をすると、よくわかっていない開発者が「コントローラーでDbContextを触れるじゃないか。じゃあ直接ここでDBにアクセスしちゃおう」と考えかねません。

ASP.NET Core ではDIを使って解決できる

調べていくと、ASP.NET CoreではDIコンテナがフレームワークとして用意されており、これを使って解決できるようです。

DIとは依存性注入(dependency injection)の略で、あるクラスが依存している別のオブジェクトの生成と破棄を、DIコンテナにお任せしてしまおう、というものです。ですので、DIコンテナを使いたい場合には、もうNew MyDbContextとは書きません。MyDbContextのインスタンスは常に外部から与えてもらうようにします。

DIコンテナについては、登場当時から便利だという話は聞いていたものの、これまでDIコンテナを使ったシステムに携わったことがなく、どのようにして使うのか、改めて調べてみました。

検索すると、基本的にほとんどのサイトで次のようなサンプルコードがヒットします。
(サンプルはC#のみでした。VB.NETは本当に消えゆく言語ですね)

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<MyDbContext>(
        options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
}
P101Controller.cs
public class P101Controller : Controller
{
    private readonly MyDbContext _context;

    public P101Controller(MyDbContext context)
    {
        _context = context;
    }
}

どういうことかというと、Controllerのインスタンス生成時に、Controllerのコンストラクタ引数のMyDbContextを、DIコンテナが自動的に生成して渡してくれるということです。コンストラクタ引数を全部生成してくれるわけではなく、Startupで指定した型(ここではMyDbContext)のみ、指定した具象型で生成して渡してくれます。

これは、ASP.NET Coreに搭載されているMicrosoft Dependency InjectionというDIコンテナの機能です。本来は、生成するオブジェクトのインタフェースのみを依存する側が参照し、そのインタフェースに対応する具象クラスをDIコンテナ側に設定してインスタンスを切り替えるという使い方が本筋かと思いますが、この記事では「インスタンスの生成と破棄の責務」のみをDIコンテナに委譲する目的で使う為、インタフェースは定義していません。

DbContextについてはAddDbContextという専用のDIコンテナへの追加メソッドが用意されていますが、例えばP101Logicをここに登録することもできます。

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<MyDbContext>(
        options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));

    services.AddTransient<P101Logic>();
    services.AddTransient<P101Dao>();
}

AddTransient()は、Tで指定したクラスがControllerのコンストラクタ引数にあった場合に、「その都度」インスタンスを生成して渡してくれるというものです。

他にも、リクエスト毎に1つのインスタンスを生成してくれるAddScoped()や、システム全体で1つのインスタンスを渡してくれるAddSingleton()もあります。

DbContextのようにDisposableなものは、AddScopedでライフタイム管理をしたほうが良いでしょう。

ともかくも、こう書く事で、P101LogicとかP101Daoという型のパラメータがControllerのコンストラクタ引数にあれば、リクエストの際に自動的にDIコンテナがインスタンスを生成して渡してくれるようになりました。

依存性注入をされる側の各クラスは、次のようになります。コンストラクタ引数のツリー構造が形成されています。

P101Controller.cs
public class P101Controller : Controller
{
    private readonly P101Logic _logic;

    public P101Controller(P101Logic logic)
    {
        _logic = logic;
    }
}
P101Logic.cs
public class P101Logic
{
    private P101Dao _dao;

    public P101Logic(P101Dao dao)
    {
        _dao = dao;
    }
}
P101Dao.cs
public class P101Dao
{
    private MyDbContext _db;

    public P101Dao(MyDbContext db)
    {
        _db = db;
    }
}

P101Controllerに対するリクエストが発生した時、DIコンテナが裏で何をするかというと、こんな感じでしょうか。

new P101Controler(
    new P101Logic(
        new P101Dao(
            new MyDbContext(options))));

マトリョーシカですね。
見事に全てのオブジェクトが、自分が依存するインスタンスの生成責任を外部に丸投げしています。
そして、その責任を一身に背負うのが、DIコンテナというわけです。

DIコンテナがP101ControllerのコンストラクタにP101Logicがあるのを見つけると、「P101LogicはDIコンテナで生成するんだったな」と、今度はP101Logicのコンストラクタ引数も見に行きます。そこにP101Daoがあるのを見つけて、「それも確かDIしろって言われてたな」と判断し、自動的に生成してくれるのです。

さらにはなんと、DIコンテナは、コンテナが生成したDisposableなインスタンス、例えばここではMyDbContextを、適切なタイミングでDisposeしてくれます。至れり尽くせりですね。

上記のP101Controllerを見てみると、MyDbContextへの依存が消えています。Logicへの依存のみとなり、すっきりしました。P101Logicもしかりです。

結果、各クラスに存在する依存性は、全て単一の場所(ここではStartup.cs)に集約されるようになりました。

また各クラスは単に自分に渡されるインスタンスを黙って使って、使いっぱなしにすればOKになりました。依存オブジェクトの管理という責務から解き放たれたわけです。

アクションメソッドに依存性注入する

しかし正直なところ、P101Controllerが使うlogicは、コンストラクタで受け取ってわざわざメンバ変数に格納しておくほどのスコープを必要としません。アクションメソッド単位で依存性の注入を行ってくれないものでしょうか?

調べると、ASP.NET Core ならそれも可能でした。アクションメソッドの引数に[FromServices]を付けると、DIコンテナがよきに計らってくれます。

P101Controller.cs
public class P101Controller : Controller
{
    public IActionResult EmployeeList([FromServices] P101Logic _logic)
    {
        var viewmodel = _logic.GetEmployees();
        return View(viewmodel);
    }
}

ただ、調べるとコンストラクタでの依存性注入の方が一般的で、メソッドへの注入はあまりメジャーではないようです。
理由はテストのし易さのようです。コンストラクタでの注入にしておけばインスタンス生成のみをDIコンテナに任せておけばよいが、メソッドでの注入にするとメソッド呼び出しもDIコンテナ任せになって煩雑になる、ということのようです。

個人的にはそれでそこまで煩雑になるとも思いませんし、明らかに便利な技術だと思いますので、積極的に使っていきたいと思いました。

DIコンテナを使えば複雑な依存性注入もシンプルに管理できる

ところで、MyDbContextだけだと、「別にDIコンテナなんて使わなくてもコンストラクタ経由で渡していけばいいのでは」と思うかもしれません。

しかし、共通のロジックやログイン情報など、システム全体で共有したいオブジェクトは結構あります。ログイン情報は通常、HttpContext.Sessionに保持しておくべきものかもしれませんが、かといって、LogicがHttpContextに依存するのは違うでしょう。それらはLogicの外部から渡される必要があります。ControllerはHttpContextへの依存を持っていておかしくありませんから、Controllerがその責務を負うのも良いと思いますが、定形的な処理になるので、そういうものの生成はどこか他のクラスにお願いしたいところです。

こういうものは共通関数やSingletonパターンを使ってどこからでも取得できるようにするのがDI以前のやり方だったかもしれませんが、そのやり方をするとテストが非常にしにくくなりますし、特定のシステムに依存した、移植性の低いコードになってしまいます。

ですから、これらも全てDIを使うやり方に切り替えていった方が良いでしょう。

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<MyDbContext>(
        options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));

    services.AddTransient<P101Logic>();
    services.AddTransient<CommonLogic>();
    services.AddTransient<LoginInfo>();
}
P101Controller.cs
public class P101Controller : Controller
{
    public IActionResult EmployeeList([FromServices] P101Logic _logic, [FromServices] CommonLogic _comlogic)
    {
        var viewmodel = _logic.GetEmployees();
        viewmodel.SetColorConfig(_comlogic.GetColorConfig());
        return View(viewmodel);
    }
}
P101Logic.cs
public class P101Logic
{
    private P101Dao _dao;
    private LoginInfo _loginInfo;

    public P101Logic(LoginInfo loginInfo, P101Dao dao){
        _dao = dao;
        _loginInfo = loginInfo;
    }
}

こんな感じでどんどん依存性を外部に出していき、DIコンテナに生成と破棄の責任を担当して貰えば、各機能の独立性が向上し、テストもしやすくなることでしょう。

今回はAddTransientに直接具象クラスを指定していますが、インタフェースを定義して次のように指定すれば、テスト時にテスト用のドライバやスタブを使うのも簡単になります。

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<MyDbContext>(
        options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));

    services.AddTransient<IP101Logic, P101Logic>();
    services.AddTransient<ICommonLogic, CommonLogic>();
    services.AddTransient<ILoginInfo, LoginInfo>();
}

ASP.NET(Coreじゃない方)ではどうするのか

ASP.NETには、ASP.NET CoreのようなDIコンテナが標準ではついていません。

今回の記事と同じことをするには、どのようなDIコンテナを導入すればよいでしょうか。

  • Controllerのコントラクタに依存性注入ができる
  • Controllerのアクションメソッドに依存性注入ができる
  • インスタンスのライフサイクル管理方法として「リクエスト単位」「都度生成」が選べる
  • .NET Frameworkと.NET Core/5+両方に対応している。

こんなところでしょうか。

調べてみると、AutoFacというDIコンテナが上記全てに対応しており、利用者も多く、メンテナンスも活発のようです。

global.asax
protected void Application_Start()
{
  var builder = new ContainerBuilder();

  // Register your MVC controllers. (MvcApplication is the name of
  // the class in Global.asax.)
  builder.RegisterControllers(typeof(MvcApplication).Assembly);

  // OPTIONAL: Register model binders that require DI.
  builder.RegisterModelBinders(typeof(MvcApplication).Assembly);
  builder.RegisterModelBinderProvider();

  // OPTIONAL: Register web abstractions like HttpContextBase.
  builder.RegisterModule<AutofacWebTypesModule>();

  // OPTIONAL: Enable property injection in view pages.
  builder.RegisterSource(new ViewRegistrationSource());

  // OPTIONAL: Enable property injection into action filters.
  builder.RegisterFilterProvider();

  // OPTIONAL: Enable action method parameter injection (RARE).
  builder.InjectActionInvoker();

  // Set the dependency resolver to be Autofac.
  var container = builder.Build();
  DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}

builder.RegisterType<MyDbContext>().InstancePerLifetimeScope();
builder.RegisterType<P101Logic>().As<IP101Logic>();
builder.RegisterType<LoginInfo>() 

AutoFacをASP.NET Coreで使う方法については、以下の英語記事が参考になりそうです。
Tips on using Autofac in .NET Core 3.x

その他の懸念事項

DIコンテナの初期化処理の増大

システムが膨大になってくると、これらのクラスを全てDIコンテナに登録していくのはかなり大変です。
Logicが百個近くなってくると、新しいLogicの登録し忘れなども起きてくるでしょう。
複数人で開発している場合、Git等のチーム開発において、単一のファイルを全員で編集することになり、コンフリクトの発生率も上がってしまいます。

調べると、AutoFacにはRegisterAssemblyTypes(asm)というメソッドが用意されており、例えば以下のように使うことで、指定したアセンブリに含まれる、DI対象のクラスをまとめて登録することができるようです(Microsoft Dependency Injectionには同等の機能を見つけられませんでしたが、恐らく外部ライブラリで対応できるものと思われます)。

Assembly Scannin

var dataAccess = Assembly.GetExecutingAssembly();

builder.RegisterAssemblyTypes(dataAccess)
       .Where(t => t.Name.EndsWith("Logic"))
       .AsImplementedInterfaces();

上記の処理は、現在実行中のアセンブリを取得し、その中に含まれる全てのパブリックなクラスのうち、クラス名が「"Logic"で終わるクラス全てについてDIコンテナに登録する、というものです。

DAOやLogicが別のプロジェクトにパッケージングされている場合には、AppDomain.CurrentDomain.GetAssemblies()を使って、アプリケーションドメインに読み込まれているすべてのアセンブリを取得し、このやり方でDIに登録することができるのではないかと思います(未確認)。

DIコンテナへの依存を単一の場所に集約する

DIコンテナへの依存部分は、ここでは「Startup.cs」のDIコンテナの初期化コードのことです。この場所に、今回DIコンテナを使用するにあたっての解決すべき依存性が全て記述されているようにしなければなりません。この単一の場所に、依存性を集約するのです。

例えば今回、P101Logicから使うDAOをP101Daoとしていますが、特定のメソッドがCommonDaoみたいな他のオブジェクトを必要とする場面はあるかもしれません。

その際には、P101LogicのコンストラクタにCommonDaoを追加します。もちろんDIコンテナの初期化にもservices.AddTransient();を追加します。

しかし、実は次のような事もできてしまいます。

P101Logic.cs
public class P101Logic
{
    private IServiceProvider _serviceProvider;
    private LoginInfo _loginInfo;

    public P101Logic(LoginInfo loginInfo, IServiceProvider serviceProvider){
        _serviceProvider = serviceProvider;
        _loginInfo = loginInfo;
    }

    public P101ViewModel GetSpecialInfo() {
        CommonDao comDao = _serviceProvider.GetService(typeof(CommonDao));
        P101ViewModel vm = new();
        vm.UserLevel = comDao.GetUserLevel(_loginInfo.UserId);
        return vm;
    }
}

コンストラクタ引数で、DIコンテナのサービスプロバイダを受け取って、自らDIコンテナを操り、インスタンスを取得しています。Logic内で自由に好きなDaoをDIコンテナから取得できるので、柔軟性があって良いように思えます。

しかしこれは、Logicから依存性を排除するというそもそもの目的に反してDIコンテナにLogicを依存させてしまっている為、避けるべきやり方です。

別の言い方をするならば、依存性を注入される側は、自分が依存性注入されているという意識を持ってはいけないということです。依存オブジェクトは、あくまでも「ただコンストラクタから渡される」だけであり、DIコンテナなどなくてもテスト可能でなくてはなりません。

ちなみにこれを「サービスロケーターパターン」と呼び、依存性を注入するという目的に反する使い方である為、アンチパターンだといわれています。
サービスロケーターパターンを使ってまでオブジェクトの生成をDIコンテナに任せたい場合、果たしてそこまでする価値があるのかよく考えるべきでしょう。

DTOオブジェクトの転写について(オブジェクトマッパーの利用)

今回の記事には関係ありませんが、DTOオブジェクトの転写は毎回面倒だと思います。
まったく同じプロパティならば、1行で全部転写してほしいものです。

P101Dao.vb
Public Class P101Dao
    Private db As New MyDbContext

    Public Function GetEmployees() As List(Of EmployeeDto)
        Return db.Employee.Where(条件).Select(
            Function(row) New EmployeeDto With {
                .EmpId = row.EmpId
                .EmpName = row.EmpName
                 :
            }).ToList()
    End Function

その為のライブラリとして有名なものに「AutoMapper」があります。
しかしこれは、マッピングする側とされる側のクラスを事前に登録してやる必要があります。

var config = new MapperConfiguration(cfg => cfg.CreateMap<Order, OrderDto>());

これは正直面倒です。特に、多層システムとして複数のレイヤーに分割している場合、誰がこの登録処理の責務を負うのかという問題があります。プロパティのマッピングなどというものはソースコード上の問題であって、DLLの利用者にお願いしなければいけないようなものであってはならないでしょう。

かといって、上記の初期化処理をDLL側で責務を負って行うには、「アセンブリがロードされた時に行う初期化時処理」とでもいうようなものが存在しなければならず、これは.NETでは今のところまだ現実的ではないようです(「モジュール初期化子」)。

現実的には、各DLL側にProfileクラスを継承したpublicな「初期化処理用のクラス」を用意し、そのクラスをアセンブリ参照を用いて生成する、という、結構面倒なやり方になるようです。(Assembly Scanning for auto configuration

どちらにせよ、使う側が予めこの処理を行わないとそのDLLが使えないとうのはあまり好ましくないと思います。また、同様の理由から、この初期化処理をDIコンテナを使って行うべきものでもないと感じます。

調べてみると、このあたりについて改善した、「Mapster」というオブジェクトマッパーがあるようです。

初期化処理は不要で、いきなりこう書けます。

var destObject = sourceObject.Adapt<Destination>();

TDestination .Adapt() の形式で使える拡張メソッドがObjectに対して定義されています。
もちろん、マッピング時に細かい調整をすることもできます。

TypeAdapterConfig<TSource, TDestination>
    .NewConfig()
    .Map(dest => dest.FullName,
        src => string.Format("{0} {1}", src.FirstName, src.LastName));

NewConfig()というのが面白くて、過去の設定を全てクリアするというものなので、常に「今、ここで設定したもの」以外の影響を受けない独立したコードが書けます。

ただ、やはりせっかくのConfigは一か所にまとめたいですよね。
そんな場合には、以下の書き方ができそうです。

class MapperConfig {
    private static TypeAdapterConfig  config;

    static MapperConfig()
    {
        MapperConfig.config = new TypeAdapterConfig();
        MapperConfig.config.ForType(Of Source, Destination)
            .Map(
                dest => dest.FullName,
                src => string.Format("{0} {1}", src.FirstName, src.LastName)
        );
    }

    public static TypeAdapterConfig Instance => config;
}
var destObject = sourceObject.Adapt<Destination>(MapperConfig.Instance);

MapperConfigクラスはモジュール内プライベートにしておいて、各モジュール内でこのconfigを使えば、モジュール内に閉じた形で運用できそうです。

本当に全ての依存関係をDIコンテナに任せられるか?

中には、条件に応じて異なるLogicに分岐をするようなケースもあるでしょう。そのような場合に、分岐の可能性のある全てのLogicのインスタンスをControllerのコンストラクタなりアクションメソッドなりに列挙するのでしょうか?

それともそういう場合には、Logicを集約する親Logicクラスを作ってそこにカプセル化する、などするのでしょうか。

DIコンテナを使うと、大元となる処理のエントリポイント(ASP.NETではController)を起点として、全てのインスタンスを一気に生成してオブジェクトの構造を作ってしまってから、処理を開始するようなイメージがあります。

しかし、中には処理の開始時には必要かどうか未定で、入力パラメータの条件によっては必要になるオブジェクト、というようなものもあるでしょう。

そういう場合には、ひょっとして遅延実行というか、オブジェクトを返す関数のみを注入したりして、必要な時はその関数を実行してインスタンスを得るような形にするのでしょうか。

この辺どうすべきなのかよくわからず少し不安なところはありますが、なんとかなるような気もします。


以上、調べた内容をつらつらとまとめてみました。
ある程度サンプルプロジェクトを作って動作を確かめていますが、想像で書いたコードも多い為、間違っていたらご指摘下さい。

また、そもそも考え方が違う、もっとこうした方がいいなどありましたら、ご教授頂ければ幸いです。

22
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
19