dependency-injection について
.NET 開発者向けの依存性注入ガイドで、登録、サービスのライフタイム、代表的なパターンを解説します。既存登録のライフタイムや重複、依存リスクを調べる MCP ツールにも触れています。
主な機能
- 01コンストラクター注入とインターフェース登録
- 02サービスのライフタイム選択
- 03キー付きサービスとファクトリーパターン
- 04Scrutor による規約ベース登録
- 05型付きオプションの設定
活用シーン
- 01インターフェースの実装を切り替える
- 02規約に沿ってサービスを登録する
- 03型付きオプションを設定して検証する
- 04ライフタイムの問題や重複登録を調べる
MCP 対応
README では `get_di_registrations` MCP ツールに言及しており、依存性注入登録のライフタイム、重複、キャプティブ依存のリスクを確認できます。
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill dependency-injectionインストール後、Claude Code で/dependency-injectionを入力して読み込まれたか確認します。
詳細
依存性注入
基本原則
- コンストラクターインジェクションをデフォルトにする — 依存関係はコンストラクター経由で注入します(プライマリコンストラクターを使うと簡潔に記述できます)。サービスロケーターやプロパティインジェクションは使用しません。
- ライフタイムを慎重に設定する — シングルトンがスコープ付きサービスや一時サービスに依存してはいけません。これは DI で最もよく起こるバグです。
- インターフェースを登録し、インターフェースで解決する — 具象型ではなく、
services.AddScoped<IOrderService, OrderService>()を登録します。 - ストラテジーパターンにはキー付きサービスを使う — .NET 8+ のキー付きサービスを使えば、実装を選択するための手動のファクトリーパターンを置き換えられます。
パターン
キー付きサービス(.NET 8+)
同じインターフェースの複数の実装を登録および解決するには、キー付きサービスを使用します。
// Registration
builder.Services.AddKeyedScoped<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedScoped<INotificationService, SmsNotificationService>("sms");
builder.Services.AddKeyedScoped<INotificationService, PushNotificationService>("push");
// Resolution via attribute
public class OrderHandler([FromKeyedServices("email")] INotificationService notifier)
{
public async Task Handle(CreateOrder.Command command, CancellationToken ct)
{
// ... create order
await notifier.SendAsync(notification, ct);
}
}
// Resolution via IServiceProvider
public class NotificationRouter(IServiceProvider provider)
{
public INotificationService GetService(string channel)
{
return provider.GetRequiredKeyedService<INotificationService>(channel);
}
}
デコレーターパターン
// Base service
public interface IOrderService
{
Task<Result<Order>> CreateAsync(CreateOrderRequest request, CancellationToken ct);
}
public class OrderService(AppDbContext db, TimeProvider clock) : IOrderService
{
public async Task<Result<Order>> CreateAsync(CreateOrderRequest request, CancellationToken ct)
{
var order = Order.Create(request, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Result.Success(order);
}
}
// Decorator — adds logging
public class LoggingOrderService(IOrderService inner, ILogger<LoggingOrderService> logger) : IOrderService
{
public async Task<Result<Order>> CreateAsync(CreateOrderRequest request, CancellationToken ct)
{
logger.LogInformation("Creating order for customer {CustomerId}", request.CustomerId);
var result = await inner.CreateAsync(request, ct);
if (result.IsSuccess)
logger.LogInformation("Order {OrderId} created", result.Value.Id);
return result;
}
}
// Registration with Scrutor
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.Decorate<IOrderService, LoggingOrderService>();
規約による登録(Scrutor)
// Auto-register all services matching a convention
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.AssignableTo<ITransientService>())
.AsImplementedInterfaces()
.WithTransientLifetime()
.AddClasses(classes => classes.AssignableTo<IScopedService>())
.AsImplementedInterfaces()
.WithScopedLifetime());
ファクトリーパターン
実行時のロジックで実装を選択する必要がある場合に使用します。
builder.Services.AddScoped<IPaymentProcessor>(sp =>
{
var config = sp.GetRequiredService<IOptions<PaymentOptions>>().Value;
return config.Provider switch
{
"stripe" => ActivatorUtilities.CreateInstance<StripeProcessor>(sp),
"paypal" => ActivatorUtilities.CreateInstance<PayPalProcessor>(sp),
_ => throw new InvalidOperationException($"Unknown payment provider: {config.Provider}")
};
});
オプションの登録
// Bind configuration section to a strongly-typed options class
builder.Services.AddOptions<JwtOptions>()
.BindConfiguration("Jwt")
.ValidateDataAnnotations()
.ValidateOnStart();
// Inject as IOptions<T>
public class TokenService(IOptions<JwtOptions> options)
{
private readonly JwtOptions _jwt = options.Value;
}
アンチパターン
シングルトンにスコープ付きサービスを保持させない
// BAD — DbContext is scoped, captured by singleton = memory leak + stale data
builder.Services.AddSingleton<OrderCache>(); // depends on AppDbContext
// GOOD — use IServiceScopeFactory in singleton
public class OrderCache(IServiceScopeFactory scopeFactory)
{
public async Task<Order?> GetAsync(Guid id)
{
await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await db.Orders.FindAsync(id);
}
}
すべてをシングルトンとして登録しない
// BAD — making a service singleton when it holds mutable state
builder.Services.AddSingleton<OrderService>(); // has DbContext dependency
// GOOD — match the lifetime to the service's needs
builder.Services.AddScoped<OrderService>();
選択ガイド
| シナリオ | 推奨事項 |
|---|---|
| ステートレスなサービス | スコープ付き(デフォルト)または一時 |
| 構成/キャッシュ | シングルトン |
| DbContext | スコープ付き(AddDbContext によって登録) |
| 複数の実装 | キー付きサービス(ストラテジーパターン) |
| 横断的な処理 | デコレーターパターン |
| 規約ベースの登録 | Scrutor |
| 実行時の実装選択 | ファクトリーデリゲート |
| 既存の登録の監査 | get_di_registrations MCP ツール — ライフタイム、重複、キャプティブ依存関係のリスクを一度の呼び出しで確認 |
| 強く型付けされた構成 | AddOptions<T>().BindConfiguration() |
コメント
Markdown 対応