dotnet-csharp-dependency-injection について
.NET アプリケーションで Microsoft.Extensions.DependencyInjection を使う開発者向けのリファレンススキルです。サービスのライフタイム、登録と解決、ホステッドサービスに加え、キー付きサービス、デコレーション、スコープ検証を扱います。
主な機能
- 01サービスのライフタイムと依存関係
- 02ファクトリーデリゲートと複数登録
- 03net8.0+ のキー付きサービス
- 04サービスのデコレーションパターン
- 05ホステッドサービスとスコープ検証
活用シーン
- 01シングルトンへの短いライフタイムのサービス取り込み防止
- 02キーによる実装の登録と解決
- 03サービスへのログ記録やキャッシュの追加
- 04バックグラウンドサービス用の DI スコープ作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-csharp-dependency-injectionインストール後、Claude Code で/dotnet-csharp-dependency-injectionを入力して読み込まれたか確認します。
詳細
dotnet-csharp-dependency-injection
.NET アプリケーション向けの高度な Microsoft.Extensions.DependencyInjection パターンです。サービスのライフタイム、キー付きサービス(net8.0 以降)、デコレーション、ファクトリーデリゲート、スコープ検証、ホステッドサービスの登録について説明します。
相互参照:[skill:dotnet-csharp-async-patterns](BackgroundService の非同期パターン)、[skill:dotnet-csharp-configuration](IOptions<T> のバインド)。
サービスのライフタイム
| ライフタイム | 登録 | 使用する場面 |
|---|---|---|
| Transient | AddTransient<T>() | 軽量でステートレスなサービスです。注入されるたびに新しいインスタンスが作成されます。 |
| Scoped | AddScoped<T>() | リクエスト単位の状態(EF Core の DbContext、ユニット・オブ・ワーク)に使用します。 |
| Singleton | AddSingleton<T>() | スレッドセーフ、ステートレス、または共有状態(キャッシュ、構成)に使用します。 |
ライフタイムの不一致(キャプティブ依存関係)
ライフタイムの短いサービスを、ライフタイムの長いサービスに注入しないでください。
// WRONG -- scoped DbContext captured in singleton = same context for all requests
builder.Services.AddSingleton<OrderService>(); // singleton
builder.Services.AddScoped<AppDbContext>(); // scoped -- CAPTIVE!
// CORRECT -- use IServiceScopeFactory in singletons
public sealed class OrderService(IServiceScopeFactory scopeFactory)
{
public async Task ProcessAsync(CancellationToken ct = default)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Orders.Where(o => o.IsPending).ToListAsync(ct);
}
}
スコープ検証を有効にする(開発環境)
var builder = WebApplication.CreateBuilder(args);
// In Development, ValidateScopes is already true by default.
// For non-web hosts:
var host = Host.CreateDefaultBuilder(args)
.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true;
options.ValidateOnBuild = true; // Validates all registrations at startup
})
.Build();
登録パターン
インターフェイスと実装のペア
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
複数の実装
// Register multiple implementations
builder.Services.AddScoped<INotifier, EmailNotifier>();
builder.Services.AddScoped<INotifier, SmsNotifier>();
builder.Services.AddScoped<INotifier, PushNotifier>();
// Inject all -- order matches registration order
public sealed class CompositeNotifier(IEnumerable<INotifier> notifiers)
{
public async Task NotifyAsync(string message, CancellationToken ct = default)
{
foreach (var notifier in notifiers)
{
await notifier.NotifyAsync(message, ct);
}
}
}
ファクトリーデリゲート
builder.Services.AddScoped<IOrderService>(sp =>
{
var repo = sp.GetRequiredService<IOrderRepository>();
var logger = sp.GetRequiredService<ILogger<OrderService>>();
var options = sp.GetRequiredService<IOptions<OrderOptions>>();
return new OrderService(repo, logger, options.Value.MaxRetries);
});
ライブラリ登録での TryAdd
ライブラリでは、アプリケーション側で登録を上書きできるように TryAdd を使用してください。
// Library code -- won't overwrite app registrations
builder.Services.TryAddScoped<IOrderRepository, DefaultOrderRepository>();
// Application code -- takes precedence if registered first
builder.Services.AddScoped<IOrderRepository, CustomOrderRepository>();
キー付きサービス(net8.0 以降)
キーを使ってサービスを登録・解決することで、名前付きサービスのパターンが不要になります。
// Registration
builder.Services.AddKeyedScoped<ICache, RedisCache>("distributed");
builder.Services.AddKeyedScoped<ICache, MemoryCache>("local");
// Injection via attribute
public sealed class OrderService(
[FromKeyedServices("distributed")] ICache distributedCache,
[FromKeyedServices("local")] ICache localCache)
{
public async Task<Order?> GetAsync(int id, CancellationToken ct = default)
{
// Check local cache first, then distributed
return await localCache.GetAsync<Order>(id.ToString(), ct)
?? await distributedCache.GetAsync<Order>(id.ToString(), ct);
}
}
// Manual resolution
var cache = sp.GetRequiredKeyedService<ICache>("distributed");
net8.0 以降のみです。 それ以前の TFM では、ファクトリーパターンまたはディクショナリベースの方法を使用してください。
デコレーションパターン
組み込みのコンテナーは、ネイティブではデコレーションをサポートしていません。次のいずれかの方法を使用してください。
手動デコレーション
builder.Services.AddScoped<SqlOrderRepository>();
builder.Services.AddScoped<IOrderRepository>(sp =>
{
var inner = sp.GetRequiredService<SqlOrderRepository>();
var logger = sp.GetRequiredService<ILogger<LoggingOrderRepository>>();
return new LoggingOrderRepository(inner, logger);
});
public sealed class LoggingOrderRepository(
IOrderRepository inner,
ILogger<LoggingOrderRepository> logger) : IOrderRepository
{
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct = default)
{
logger.LogInformation("Getting order {OrderId}", id);
return await inner.GetByIdAsync(id, ct);
}
}
Scrutor ライブラリ(広く使われている代替手段)
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.Decorate<IOrderRepository, LoggingOrderRepository>();
builder.Services.Decorate<IOrderRepository, CachingOrderRepository>();
// Outer -> CachingOrderRepository -> LoggingOrderRepository -> SqlOrderRepository
ホステッドサービスとバックグラウンドワーカー
BackgroundService(推奨)
public sealed class QueueProcessorWorker(
IServiceScopeFactory scopeFactory,
ILogger<QueueProcessorWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Queue processor starting");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = scopeFactory.CreateScope();
var processor = scope.ServiceProvider
.GetRequiredService<IQueueProcessor>();
await processor.ProcessNextBatchAsync(stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Error processing queue batch");
}
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
// Registration
builder.Services.AddHostedService<QueueProcessorWorker>();
IHostedService(起動/シャットダウンのフック)
public sealed class DatabaseMigrationService(
IServiceScopeFactory scopeFactory,
ILogger<DatabaseMigrationService> logger) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync(cancellationToken);
logger.LogInformation("Database migration completed");
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
builder.Services.AddHostedService<DatabaseMigrationService>();
ホステッドサービスの主なルール
- スコープを作成するには、必ず
IServiceScopeFactoryを使用してください。ホステッドサービスはシングルトンです - スコープ付きサービスをホステッドサービスのコンストラクターに直接注入しないでください
ExecuteAsync内で例外を処理してください。未処理の例外があるとホストが停止します(net8.0 以降)- バックグラウンドワーカーの非同期パターンについては、[skill:dotnet-csharp-async-patterns] を参照してください
登録の整理
関連する登録を拡張メソッドにまとめると、Program.cs をすっきりと保てます。
// ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddOrderServices(this IServiceCollection services)
{
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddScoped<IOrderService, OrderService>();
services.AddHostedService<OrderProcessorWorker>();
return services;
}
public static IServiceCollection AddNotificationServices(this IServiceCollection services)
{
services.AddScoped<INotifier, EmailNotifier>();
services.AddScoped<INotifier, SmsNotifier>();
return services;
}
}
// Program.cs
builder.Services.AddOrderServices();
builder.Services.AddNotificationServices();
DI を使ったテスト
[Fact]
public async Task OrderService_UsesRepository()
{
// Arrange -- build a service provider for integration tests
var services = new ServiceCollection();
services.AddScoped<IOrderRepository, InMemoryOrderRepository>();
services.AddScoped<IOrderService, OrderService>();
services.AddLogging();
using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IOrderService>();
// Act
var order = await service.GetByIdAsync(1);
// Assert
Assert.NotNull(order);
}
単体テストでは、完全なコンテナーを構築するよりも、モックを使ったコンストラクターへの直接注入を推奨します。