Visual Studio 2019 / 2022 のテンプレートで ASP.NET Core MVC / Razor Pages アプリを作成すると DI コンテナを含めて DI に必要な機能がフレームワークに組み込まれ、要求を受けて ASP.NET が Controller / PageModel を初期化する際、Controller / PageModel が依存するクラスのインスタンスが自動的に生成され、そのインスタンスへの参照が Controller / PageModel のコンストラクタの引数に渡されるようになっています。
その際に使われているのが Microsoft.Extensions.DependencyInjection 名前空間にあるにあるクラス類だそうです。なので、ASP.NET Core アプリでなくても、例えばコンソールアプリでも、NuGet で必要なアセンブリをプロジェクトにインストールすれば DI 機能を実装できます。
Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類は、.NET Core / .NET アプリでなくても、.NET Framework 版アプリでもバージョン 4.6.1 以降であれば利用できるそうですので、.NET Framework 4.8 のコンソールアプリで試してみました。
サードパーティ製の DI コンテナは巷に多々あるそうで、自分も Simple Injector を使ってみたことがあります。(その話は「SimpleInjector を ASP.NET MVC & Web API で利用」に書きましたので興味があれば見てください)
Simple Injector なら多少の実装経験があるということで、比較のために Simple Injector のドキュメント Quick Start にあるサンプルとほぼ同じコンソールアプリを、Microsoft.Extensions.DependencyInjection 名前空間の ServiceCollection クラスと ServiceProvider クラスを利用して実装してみました。
まず、Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET Framework 4.8 でコンソースアプリを作成し、NuGet から Microsoft.Extensions.DependencyInjection をインストールします。バージョンはこの記事を書いた時点での最新版 8.0.0 です。
他の Micosoft.Bcl.AsyncInterfaces などのパッケージは Microsoft.Extensions.DependencyInjection をインストールした時に同時に自動的にインストールされたものです。
コードは以下の通りです。サービスに DI するクラスを登録するのに AddTransient, AddScoped, AddSingleton の 3 種類を使ってますが、使い分けているわけではなくて、このようなコンソールアプリではどれを使っても同じで、試しに全種類を使ってみただけですのでご注意ください。違いはこの記事の下の方で説明します。
using Microsoft.Extensions.DependencyInjection;
using System;
namespace ConsoleAppDependencyInjection
{
internal class Program
{
static void Main(string[] args)
{
IServiceCollection services = new ServiceCollection();
services.AddTransient<IOrderRepository, SqlOrderRepository>();
services.AddSingleton<ILogger, Logger>();
services.AddScoped<IEventPublisher, EventPublisher>();
services.AddTransient<CancelOrderHandler>();
var provider = services.BuildServiceProvider();
var handler = provider.GetRequiredService<CancelOrderHandler>();
var orderId = Guid.NewGuid();
var command = new Order { OrderId = orderId };
handler.Handle(command);
}
}
public class CancelOrderHandler
{
private readonly IOrderRepository repository;
private readonly ILogger logger;
private readonly IEventPublisher publisher;
// Use constructor injection for the dependencies
public CancelOrderHandler(IOrderRepository repository,
ILogger logger,
IEventPublisher publisher)
{
this.repository = repository;
this.logger = logger;
this.publisher = publisher;
}
public void Handle(Order command)
{
this.logger.Log($"Cancelling order {command.OrderId}");
var order = this.repository.GetById(command.OrderId);
order.OrderStatus = "Cancelled";
this.repository.Save(order);
this.publisher.Publish(order);
}
}
public interface IOrderRepository
{
Order GetById(Guid orderId);
void Save(Order order);
}
public class SqlOrderRepository : IOrderRepository
{
private readonly ILogger logger;
// Use constructor injection for the dependencies
public SqlOrderRepository(ILogger logger)
{
this.logger = logger;
}
public Order GetById(Guid orderId)
{
this.logger.Log($"Getting Order {orderId}");
// Retrieve from db.・・・のつもり
var order = new Order
{
OrderId = orderId,
ProductName = "911-GT3",
OrderStatus = "Ordered"
};
return order;
}
public void Save(Order order)
{
this.logger.Log($"Saving order {order.OrderId}");
// Save to db.
}
}
public interface ILogger
{
void Log(string log);
}
public class Logger : ILogger
{
public void Log(string log)
{
Console.WriteLine(log);
}
}
public interface IEventPublisher
{
void Publish(Order order);
}
public class EventPublisher : IEventPublisher
{
public void Publish(Order order)
{
Console.WriteLine($"Publish order {order.OrderId}, " +
$"{order.ProductName}, {order.OrderStatus}");
}
}
public class Order
{
public Guid OrderId { get; set; }
public string ProductName { get; set; }
public string OrderStatus { get; set; }
}
}
実行結果は以下の画像のようになります。
上のコードでサービスに DI するクラスを登録する AddSingleton, AddScoped, AddTransient メソッドの違いは、Microsoft.Extensions.DependencyInjection Deep Dive という記事の「生成と破棄」のセクションに詳しく書いてあります。
ASP.NET Web アプリで説明すると以下のようになります。
-
AddSingleton: 一度 DI されると、アプリケーションが終了するまで、最初の DI で生成されたインスタンスを使いまわす
-
AddScoped: 一つの HTTP 要求から応答を返すまでの間では、最初の DI で生成されたインスタンスをその後の DI でも使いまわす
-
AddTransient: DI が行われるたびに新しいインスタンスを生成する
ASP.NET アプリはクライアントから要求を受けるたびにスレッドプールからスレッドを取得し、処理に必要なアセンブリをメモリにロードし、処理が完了してクライアントに応答を返すとメモリをクリアし、使ったスレッドをプールに戻すマルチスレッドアプリです。(ググって調べた stackoverflow 記事 Is Kestrel using a single thread for processing requests like Node.js? などを見た限りですが Kestrel も同じだそうです。)
AddSingleton を選んだ場合、ワーカープロセスが立ち上がった後の最初の DI でインスタンスが生成されると、ワーカープロセスがリサイクルされるまで、すべての要求に同じインスタンスが使い回されるということになります。初期化するのに時間がかかるとかメモリなどリソースを大量に消費するクラスを DI する場合には AddSingleton の利用を考えるのがよさそうです。
AddScoped の使い道、即ち一回の要求から応答を返すまでの間に同じクラスを複数回 DI するケースというのは、View への DI もサポートされていること(詳しくは「ASP.NET Core でのビューへの依存関係の挿入」参照)、サービス、ミドルウェアその他カスタムクラスにも DI 機能を実装することができることを考えるといろいろありそうです。
例えば、Visual Studio で作成したプロジェクトでは、ASP.NET Identity を利用した認証関係の Razor Class Library (RCL) のページモデルと _LoginPartial.cshtml では、SignInManager, UserManager を DI する設定になっています。そういうクラスは AddScoped で ServiceCollection に登録しておくのが良さそうです。
AddTransient を使うと、Controller、View、サービス、ミドルウェアその他カスタムクラスで DI 操作が行われるたび、新たにインスタンスを生成してそれへの参照を渡すということになります。Account confirmation and password recovery in ASP.NET Core に書いてあった EmailSender クラスの登録などで例を見ました。(実際に AddTransient を使う必要があるのかは分かりませんが)