【C#】DIコンテナでFactoryメソッドを使う際の実装パターン
アプリケーションコードを記述する際、クラス間の依存関係を分離するためにDIコンテナがよく使われます。
C#においてもASP.netのDIコンテナが非常に便利ですが弱点もあります。
今回はAsyncLazyを使って弱点をスマートなDIコンテナを実現する方法を紹介します。
Factoryパターンの必要性
C#のコンストラクタはasyncにできません。
public class DatabaseConnection
{
public DatabaseConnection()
{
// ❌ コンストラクタでawaitは使えない
await ConnectAsync();
}
private async Task ConnectAsync()
{
// DB接続処理(非同期I/O)
await Task.Delay(1000);
}
}
なので、初期化時に非同期I/Oを伴う必要があるクラスでは非同期メソッドを使ったFactoryパターンが使われます。
public class DatabaseConnection
{
private string _connectionString;
// コンストラクタはprivate
private DatabaseConnection()
{
}
// 非同期のFactoryメソッド
public static async Task<DatabaseConnection> CreateAsync(string server, int port)
{
var connection = new DatabaseConnection();
// 非同期で初期化処理
await connection.InitializeAsync(server, port);
return connection;
}
private async Task InitializeAsync(string server, int port)
{
Console.WriteLine("DB接続中...");
// 実際のDB接続(非同期I/O)
await Task.Delay(1000); // DB接続をシミュレート
_connectionString = $"Server={server};Port={port}";
Console.WriteLine("DB接続完了");
}
public void ExecuteQuery(string query)
{
Console.WriteLine($"クエリ実行: {query}");
}
}
// 使い方
static async Task Main()
{
// Factoryメソッドで作成(非同期初期化される)
var db = await DatabaseConnection.CreateAsync("localhost", 5432);
db.ExecuteQuery("SELECT * FROM users");
}
ASP.NET CoreのDIコンテナでFactoryパターンを使う際の問題
DIコンテナ(Microsoft.Extensions.DependencyInjection)は基本的に同期初期化しかサポートしていません。なので、非同期Factoryでインスタンスを初期化することはできません。
例えば
public class DatabaseConnection
{
private DatabaseConnection()
{
}
public static async Task<DatabaseConnection> CreateAsync(string connectionString)
{
var connection = new DatabaseConnection();
await connection.InitializeAsync(connectionString);
return connection;
}
private async Task InitializeAsync(string connectionString)
{
// DB接続(非同期I/O)
await Task.Delay(1000);
}
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ❌ これはできない!
// DIコンテナの登録は同期的
builder.Services.AddSingleton<DatabaseConnection>(provider =>
{
// await が使えない!
return await DatabaseConnection.CreateAsync("connection-string");
});
は構文的にも実行的にもアウトです。
解決策はいくつかあります。
解決策1:同期的なFactoryクラスをDIに登録
FactoryクラスをシングルトンとしてDIコンテナに登録しても、依存クラスで毎回時間のかかるI/O処理を伴うCreateAsyncを実行しなければならない。
// Factoryインターフェース
public interface IDatabaseConnectionFactory
{
Task<DatabaseConnection> CreateAsync();
}
public class DatabaseConnectionFactory : IDatabaseConnectionFactory
{
private readonly string _connectionString;
public DatabaseConnectionFactory(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("Default");
}
public Task<DatabaseConnection> CreateAsync()
{
return DatabaseConnection.CreateAsync(_connectionString);
}
}
// DI登録
builder.Services.AddSingleton<IDatabaseConnectionFactory, DatabaseConnectionFactory>();
// 使用例
public class MyController : ControllerBase
{
private readonly IDatabaseConnectionFactory _factory;
public MyController(IDatabaseConnectionFactory factory)
{
_factory = factory;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var connection = await _factory.CreateAsync(); // 毎回初期化処理が走る
return Ok(connection.ConnectionString);
}
}
解決策2:アプリ起動時に初期化してシングルトン登録
Program.csに初期化処理を書く必要があるのでスマートさにかけてしまう。
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// アプリ起動時に初期化
var connectionString = builder.Configuration.GetConnectionString("Default");
var dbConnection = await DatabaseConnection.CreateAsync(connectionString);
// 初期化済みインスタンスを登録
builder.Services.AddSingleton(dbConnection);
var app = builder.Build();
app.Run();
// 使用例
public class MyController : ControllerBase
{
private readonly DatabaseConnection _connection;
public MyController(DatabaseConnection connection)
{
_connection = connection; // 初期化済み
}
[HttpGet]
public IActionResult Get()
{
return Ok(_connection.ConnectionString);
}
}
解決策3:AsyncLazyで初回アクセス時に初期化
いきなりAsyncLazyを出してしまいますが後述します。
// AsyncLazy実装
public class AsyncLazy<T>
{
private readonly Lazy<Task<T>> _lazy;
public AsyncLazy(Func<Task<T>> factory)
{
_lazy = new Lazy<Task<T>>(() => Task.Run(factory));
}
public Task<T> Value => _lazy.Value;
}
// ラッパークラス
public class LazyDatabaseConnection
{
private readonly AsyncLazy<DatabaseConnection> _lazyConnection;
public LazyDatabaseConnection(IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("Default");
_lazyConnection = new AsyncLazy<DatabaseConnection>(() =>
DatabaseConnection.CreateAsync(connectionString)
);
}
public Task<DatabaseConnection> GetConnectionAsync() => _lazyConnection.Value;
}
// DI登録
builder.Services.AddSingleton<LazyDatabaseConnection>();
// 使用例
public class MyController : ControllerBase
{
private readonly LazyDatabaseConnection _lazyConnection;
public MyController(LazyDatabaseConnection lazyConnection)
{
_lazyConnection = lazyConnection;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var connection = await _lazyConnection.GetConnectionAsync(); // 初回のみ初期化
return Ok(connection.ConnectionString);
}
}
解決策4:IHostedServiceで起動時に初期化
Factoryパターンではなくなるが、IHostedServiceを使う方法もある。
// 初期化サービス
public class DatabaseInitializationService : IHostedService
{
private readonly DatabaseConnection _connection;
private readonly IConfiguration _configuration;
public DatabaseInitializationService(DatabaseConnection connection, IConfiguration configuration)
{
_connection = connection;
_configuration = configuration;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var connectionString = _configuration.GetConnectionString("Default");
await _connection.InitializeAsync(connectionString);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
// DI登録
builder.Services.AddSingleton<DatabaseConnection>();
builder.Services.AddHostedService<DatabaseInitializationService>();
// 使用例
public class MyController : ControllerBase
{
private readonly DatabaseConnection _connection;
public MyController(DatabaseConnection connection)
{
_connection = connection; // 起動時に初期化済み
}
[HttpGet]
public IActionResult Get()
{
return Ok(_connection.ConnectionString);
}
}
LazyとAsyncLazyとは??
-
Lazy<T>:同期の遅延初期化(一度だけ)
Lazy<T> は、T を返す任意の関数(Func<T>)を渡して初期化することができ、値ファクトリを実行することでFunc<T>が実行されインスタンスを取得することができる。// T を返すファクトリ(Func<T>)を渡す var lazyFoo = new Lazy<Foo>(() => new Foo(1)); // 初回アクセス時にだけファクトリが実行される Foo foo = lazyFoo.Value; -
Lazy<Task<T>>:非同期の“タスク”を遅延初期化
Lazy<Task<T>> は、Task<T>> を返す任意の関数(Func<Task<T>>>)を渡して初期化することができ、値ファクトリを実行することでFunc<T>が実行されインスタンスを取得することができる。// Task<T> を返すファクトリ(Func<Task<T>>)を渡す var lazyFooTask = new Lazy<Task<Foo>>(() => Foo.CreateAsync(2)); // 必要なときに await(初回だけ起動・以後は同じ Task を共有) Foo foo2 = await lazyFooTask.Value; -
AsyncLazy<T>:使いやすい薄いラッパー
AsyncLazyはLazyをラップしたものです。
Task<T>を返す関数を指定しているので値ファクトリの実行時にawaitを付けることになる。
つまり、非同期処理に対応できるようになる。public sealed class AsyncLazy<T> { private readonly Lazy<Task<T>> _lazy; public AsyncLazy(Func<Task<T>> factory, LazyThreadSafetyMode mode = LazyThreadSafetyMode.ExecutionAndPublication) => _lazy = new Lazy<Task<T>>(factory, mode); public Task<T> Value => _lazy.Value; } // 使い方(非同期ファクトリを渡す → 使うときだけ await) var asyncLazyFoo = new AsyncLazy<Foo>(() => Foo.CreateAsync(3)); Foo foo3 = await asyncLazyFoo.Value;
Discussion