mcpField
D
dotnet-csharp-dependency-injection

dotnet-csharp-dependency-injection

github.com/wshaddix開発86116 回インストール35 週間前に更新
リポジトリ

dotnet-csharp-dependency-injection について

.NET アプリケーションで Microsoft.Extensions.DependencyInjection を使う開発者向けのリファレンススキルです。サービスのライフタイム、登録と解決、ホステッドサービスに加え、キー付きサービス、デコレーション、スコープ検証を扱います。

主な機能

  1. 01サービスのライフタイムと依存関係
  2. 02ファクトリーデリゲートと複数登録
  3. 03net8.0+ のキー付きサービス
  4. 04サービスのデコレーションパターン
  5. 05ホステッドサービスとスコープ検証

活用シーン

  1. 01シングルトンへの短いライフタイムのサービス取り込み防止
  2. 02キーによる実装の登録と解決
  3. 03サービスへのログ記録やキャッシュの追加
  4. 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> のバインド)。


サービスのライフタイム

ライフタイム登録使用する場面
TransientAddTransient<T>()軽量でステートレスなサービスです。注入されるたびに新しいインスタンスが作成されます。
ScopedAddScoped<T>()リクエスト単位の状態(EF Core の DbContext、ユニット・オブ・ワーク)に使用します。
SingletonAddSingleton<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);
}

単体テストでは、完全なコンテナーを構築するよりも、モックを使ったコンストラクターへの直接注入を推奨します。


参考資料

コメント