しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET Core の Dependency Injection はコンストラクタのオーバーロードに対応していない

ASP.NET Core 1.0 時代に書いた気もしますが、最近は ASP.NET MVC 5 から ASP.NET Core MVC 2.1 への移行を行っていて、いい感じに MVC 5 と Core MVC 2.1 で共存させないといけない部分があり、DI 周りではまったので調べました。

まあ、タイトルの通り ASP.NET Core のデフォルト DI はコンストラクタがオーバーロードを持つ場合にはエラーになります。これはドキュメントにも記載されている挙動です。

Constructor injection requires that only one applicable constructor exist. Constructor overloads are supported, but only one overload can exist whose arguments can all be fulfilled by dependency injection. If more than one exists, your app will throw an InvalidOperationException:

Dependency injection in ASP.NET Core | Microsoft Docs

DI で扱うクラスは 1 つのコンストラクタしか許可されないと、ちゃんと書いてあります。

なので、MVC 5 時代に書かれた以下のようなコードは一発で落ちます。当時は名前を聞いた記憶がないですが、今では Poor Man's DI とか Pure DI とか呼ばれるらしいですね。

public class HomeController : Controller
{
    public HomeController()
        : base(new SampleService())
    {
    }

    public HomeController(ISampleService sampleService)
    {
        _sampleService = sampleService;
    }

    private readonly ISampleService _sampleService;

    public IActionResult Index()
    {
        return View();
    }
}

ちなみに実際のコードはコントローラではありません。試しやすいので選んだだけです。

話を戻します。上のコードを実際に動かすと、ちゃんと InvalidOperationException が投げられました。

f:id:shiba-yan:20180612000028p:plain

全てが Core MVC で完結していれば DI に乗せればよいのですけど、MVC 5 との共存を一部で考える必要があったので、今回はオプション引数を利用して解決しました。

DI は引数のデフォルト値をサポートしていると、これも記載されてます。

Constructors can accept arguments that are not provided by dependency injection, but these must support default values.

Dependency injection in ASP.NET Core | Microsoft Docs

なので上のコードを書き直すと以下のようになります。まあ、分かってしまえば単純なコードです。

public class HomeController : Controller
{
    public HomeController(ISampleService sampleService = null)
    {
        _sampleService = sampleService ?? new SampleService();
    }

    private readonly ISampleService _sampleService;

    public IActionResult Index()
    {
        return View();
    }
}

ちなみに、ASP.NET Core の中でも IOptions<T> を引数に取るコンストラクタの場合は、デフォルトのコンストラクタがあってもエラーにならないようです。

実際に以下のようなコードを書いて試してみました。よくある Configure<T> を使って設定された値を受け取るクラスです。本来なら複数のコンストラクタが定義されているのでエラーになるはずです。

public class SampleService
{
    public SampleService()
    {
        _testValue = "Default";
    }

    public SampleService(IOptions<SampleOption> options)
    {
        _testValue = options.Value.TestValue;
    }

    private string _testValue;

    public string GetValue()
    {
        return _testValue;  
    }
}

ところが実行してみると、特に問題なくインスタンスが作られてコンストラクタが呼び出されました。

f:id:shiba-yan:20180612000731p:plain

さらに Configure<T> の呼び出しをコメントアウトしたとしても、エラーになることなくコンストラクタが呼び出されました。値自体は null になるので、地味にこれははまりそうな予感がします。

f:id:shiba-yan:20180612000838p:plain

当然ですが IOptions<T> で包まない場合は速攻でエラーになります。

MVC 5 から Core MVC 2.1 への移行で非常に辛いのが ConfigurationManager からの移行だったので、上手く終わればそのあたりの対応についてまたブログに書きたいと思います。