.NET Core アプリでは Microsoft の Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類を使って Dependency Injection (DI) 機能を実装できるという話を書きます。
Visual Studio 2019 のテンプレートで ASP.NET Core MVC / Razor Page アプリを作成すると DI コンテナを含めて DI に必要な機能がフレームワークに組み込まれ、要求を受けて ASP.NET が Controller を初期化する際、Controller が依存するクラスのインスタンスが自動的に生成され、そのインスタンスへの参照が Controller のコンストラクタの引数に渡されるようになっています。(詳しくは Microsoft のドキュメント「ASP.NET Core での依存関係の挿入」を見てください)
その際に使われているのが Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類だそうです。なので、ASP.NET Core アプリでなくても、例えば .NET Core コンソールアプリでも、上の画像のように NuGet で必要なアセンブリをプロジェクトにインストールすれば DI 機能を実装できます。
知ってました? 実は無知な自分は最近まで知らなかったです。(汗) 試しに .NET 5.0 ベースのコンソールアプリに DI ���能を実装してみましたので、備忘録として書いておくことにした次第です。
サードパーティ製の DI コンテナは巷に多々あるそうで、自分も Simple Injector を使ってみたことがあります。(その話は「SimpleInjector を ASP.NET MVC & Web API で利用」に書きましたので興味があれば見てください)
Simple Injector なら多少の実装経験があるということで、比較のために Simple Injector のドキュメント Quick Start にあるサンプルとほぼ同じコンソールアプリを、Microsoft.Extensions.DependencyInjection 名前空間の ServiceCollection クラスと ServiceProvider クラスを利用して実装してみました。
そのコードは以下の通りです。サービスに DI するクラスを登録するのに AddTransient, AddScoped, AddSingleton の 3 種類を使ってますが、使い分けているわけではなくて、このようなコンソールアプリではどれを使っても同じで、試しに全種類を使ってみただけですのでご注意ください。違いはこの記事の下の方で説明します。
using System;
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleAppDependencyInjection
{
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
{
public Order GetById(Guid orderId);
public 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
{
public 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 アプリはクライアントから要求を受けるたびにスレッドプールからスレッドを取得し、処理に必要なアセンブリをメモリにロードし、処理が完了してクライアントに応答を返すとメモリをクリアし、使ったスレッドをプールに戻すマルチスレッドアプリです。(ググって調べた記事 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<IdentityUser>, UserManager<IdentityUser> を DI する設定になっています。そういうクラスは AddScoped で ServiceCollection に登録しておくのが良さそうです。
AddTransient を使うと、Controller、View、サービス、ミドルウェアその他カスタムクラスで DI 操作が行われるたび、新たにインスタンスを生成してそれへの参照を渡すということになります。Account confirmation and password recovery in ASP.NET Core に書いてあった EmailSender クラスの登録などで例を見ました。(実際に AddTransient を使う必要があるのかは分かりませんが)