前回までの章では、DI(Dependency Injection)の一般的なパターンや原則について解説しましたが、特定のDIコンテナを利用した具体的な実装手法については深く掘り下げていませんでした。本章では、.NETエコシステムで長く支持されてきたDIコンテナ「Autofac」に焦点を当て、これまでに学んだデザインパターンを実際のコードにどう適用するかを解説します。
Autofacは、堅牢なAPIと柔軟な設定オプションを備えた成熟したDIコンテナです。本章を通じて、基本的なオブジェクトの解決から、ライフサイクル管理、高度な登録パターン(デコレータやコンポジットなど)まで、Autofacを使いこなすための実践的な知識を習得できます。
Autofacの基本と導入
Autofacを使用する流れはシンプルです。まずContainerBuilderを用いてコンポーネントを登録し、そのビルダーからIContainerを生成します。重要な点として、Autofacではルートコンテナ自体から直接オブジェクトを解決することは推奨されず、必ずライフタイムスコープ(Lifetime Scope)を作成しそこから解決を行います。これは、メモリリークや並行性の問題を防ぐための重要なベストプラクティスです。
以下のコードリストは、Autofacを用いた最も基本的な登録と解決の例です。ここでは、料理のメタファーではなく、より一般的なソフトウェアの例としてIMessageServiceとその実装クラスを使用します。
// コンテナのビルダーをインスタンス化
var builder = new ContainerBuilder();
// 具象クラスを登録
builder.RegisterType<EmailMessageService>()
.As<IMessageService>();
// コンテナを構築
var container = builder.Build();
// ライフタイムスコープを作成(必須)
using (var scope = container.BeginLifetimeScope())
{
// スコープからサービスを解決
var service = scope.Resolve<IMessageService>();
service.SendMessage("Hello World");
}
抽象型と具象型のマッピング
疎結合な設計を実現するために、インターフェース(抽象型)を具象クラスにマッピングします。RegisterTypeメソッドとAsメソッドを組み合わせることで、IMessageServiceが要求された際にEmailMessageServiceのインスタンスを提供するように設定できます。
型安全性を高めるために、複数のインターフェースや型自体に対して一度に登録することも可能です。
弱い型付けによるサービスの解決
実行時まで型が決まらないシナリオ、例えばMVCコントローラーのファクトリなどでは、ジェネリック版のResolve<T>ではなく、Typeオブジェクトを使用した非ジェネリック版のResolveメソッドを使用する必要があります。
Type requestedType = typeof(IMessageService);
var service = (IMessageService)scope.Resolve(requestedType);
コンテナの設定方法
DIコンテナの設定方法は大きく分けて「コードによる設定」、「規約による自動登録」、「設定ファイル(XML/JSON)」の3つがあります。Autofacはこれらすべてをサポートしており、プロジェクトのニーズに合わせて組み合わせることができます。
規約による自動登録
コンポーネントが増えると、個別の登録は煩雑になります。RegisterAssemblyTypesメソッドを使用すると、指定したアセンブリ内の型をスキャンし、条件に一致するものを一括登録できます。
<codevar assembly = typeof(EmailMessageService).Assembly;
// IMessageServiceを実装するすべてのパブリック型を登録
builder.RegisterAssemblyTypes(assembly)
.AsClosedTypesOf(typeof(ICommandHandler<>));
</code>
この例では、ICommandHandler<T>を実装するすべてのクローズドジェネリック型を自動的に検索し、登録しています。これにより、新しいハンドラーを追加してもコンテナの設定コードを変更する必要がなくなります。
ジェネリック型の登録
オープンジェネリック型を登録するには、RegisterGenericメソッドを使用します。これにより、任意のクローズドジェネリック型の要求に対して、共通の実装を提供できるようになります。
設定ファイルによる構成
コンパイルなしで構成を変更する必要がある場合、外部の設定ファイルを使用できます。Autofac.Configurationパッケージを利用することで、appsettings.jsonなどから読み込むことが可能です。
<codevar configuration = new ConfigurationBuilder()
.AddJsonFile("autofac_config.json")
.Build();
builder.RegisterModule(new ConfigurationModule(configuration));
</code>
ライフサイクルの管理
Autofacでは、インスタンスの生存期間(ライフサイクル)を「インスタンススコープ」と呼ばれる概念で管理します。主要なスコープには以下のものがあります。
| Autofacのスコープ名 | 一般的な名称 | 説明 |
|---|---|---|
| InstancePerDependency | Transient | 要求されるたびに新しいインスタンスが生成されます(デフォルト)。 |
| SingleInstance | Singleton | アプリケーション全体で唯一のインスタンスが共有されます。 |
| InstancePerLifetimeScope | Scoped | 同一のライフタイムスコープ内では同じインスタンスが共有されます。 |
ライフサイクルの設定は、登録時にチェーンメソッドを呼び出すことで行います。
特にInstancePerLifetimeScopeは、WebアプリケーションにおけるHTTPリクエストごとの生存期間などに対応するために使用されます。スコープが破棄(Dispose)されると、そのスコープ内で生成されたインスタンスも適切に破棄されます。
警告: 前述の通り、IContainer(ルートコンテナ)から直接Transientな依存関係を解決することは避けてください。ルートコンテナはアプリケーションの終了まで生存するため、そこで解理されたTransientなインスタンスはルートコンテナによって参照され続け、メモリリークの原因になる可能性があります。
複雑な登録シナリオへの対応
現実の開発では、単純なコンストラクタインジェクションだけでは対処できないケースが存在します。ここでは、プリミティブ型の依存性や、特定のファクトリメソッドを持つクラスの扱いについて解説します。
プリミティブ型の依存性の注入
コンストラクタが文字列や整数などのプリミティブ型に依存している場合、WithParameterメソッドを使用して値を明示的に指定できます。
また、より複雑なロジックが必要な場合は、ResolvedParameterクラスを使用してパラメータ名や型に基づいて動的に値を解決することも可能です。
ラムダ式による登録
コンストラクタが非公開であったり、静的ファクトリメソッドを使用してインスタンスを生成する必要がある場合、Registerメソッドにラムダ式を渡してインスタンス化のロジックをカスタマイズできます。
この方法では、手動でインスタンスを生成するため自動配線(Auto-wiring)の利点は失われますが、生成ロジックを完全に制御できるという利点があります。
同一サービスの複数実装の取り扱い
1つのインターフェースに対して複数の実装クラスが存在する場合、Autofacはいくつかの方法でこれを解決できます。
コレクションとしての解決
IEnumerable<T>または配列として解消することで、そのインターフェースの全登録インスタンスを取得できます。
名前付き・キー付きサービス
特定の実装を識別するために、名前や任意のキーオブジェクトを付与して登録できます。
また、Keyed<T>を使用することで、文字列以外のオブジェクトをキーとして使用することも可能です。
デコレータの適用
デコレータパターンは、既存のサービスに横断的な機能(ロギング、キャッシングなど)を追加するのに便利です。AutofacにはRegisterDecoratorメソッドが用意されており、簡潔にデコレータを適用できます。
登録された順序に従ってデコレータが適用されます。上記の例では、CommandServiceがLoggingCommandServiceDecoratorでラップされ、さらにその外側がRetryCommandServiceDecoratorでラップされます。
コンポジットパターンの実装
複数の実装を集約して1つのインターフェースとして扱うコンポジットパターンも、IEnumerable<T>をコンストラクタで受け取ることで実現できます。
<codepublic class CompositeValidator : IValidator
{
private readonly IEnumerable<IValidator> _validators;
public CompositeValidator(IEnumerable<IValidator> validators)
{
_validators = validators;
}
public void Validate()
{
foreach (var validator in _validators)
{
validator.Validate();
}
}
}
// 登録処理
builder.RegisterAssemblyTypes(assembly)
.As<IValidator>()
.Except<CompositeValidator>(); // コンポジット自身を除外
builder.Register(c => new CompositeValidator(
c.Resolve<IEnumerable<IValidator>>()))
.As<IValidator>();
</code>
自動登録を行う際は、コンポジット自身がコレクションに含まれないよう、Exceptメソッドやフィルタリングを使用して除外する必要があります。循環依存を防ぐための重要な措置です。
以上でAutofacの主要な機能の解説は終了です。Autofacは柔軟かつ強力なAPIを提供しており、これらの機能を組み合わせることで、複雑な依存関係も整理された状態で管理することができます。