mcpField
M
microsoft-extensions-dependency-injection

microsoft-extensions-dependency-injection

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

microsoft-extensions-dependency-injection について

Microsoft.Extensions.DependencyInjection の登録を、組み合わせ可能な拡張メソッドに整理する .NET 開発者向けスキルです。設定の再利用、サービスのライフタイム、テスト、Akka.NET アクターのスコープ管理を扱います。

主な機能

  1. 01拡張メソッドで登録を整理
  2. 02拡張メソッドを階層的に構成
  3. 03テストで登録設定を再利用
  4. 04サービスのライフタイムを選択
  5. 05Akka.NET アクターのスコープを管理

活用シーン

  1. 01大規模な Program.cs を分割
  2. 02本番環境の登録をテストで再利用
  3. 03機能ごとのサービスを構成
  4. 04アクターでスコープ付き依存関係を利用

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill microsoft-extensions-dependency-injection

インストール後、Claude Code で/microsoft-extensions-dependency-injectionを入力して読み込まれたか確認します。

詳細

依存性注入のパターン

このスキルを使用する場面

次の場合にこのスキルを使用します。

  • ASP.NET Core アプリケーションでサービス登録を整理するとき
  • 数百もの登録がある巨大な Program.cs/Startup.cs ファイルを避けるとき
  • 本番環境とテストの間でサービス構成を再利用するとき
  • Microsoft.Extensions.DependencyInjection と連携するライブラリを設計するとき

問題点

整理されていないと、Program.cs は管理しにくくなります。

// BAD: 200+ lines of unorganized registrations
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IEmailComposer, MjmlEmailComposer>();
builder.Services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
builder.Services.AddScoped<IPaymentProcessor, StripePaymentProcessor>();
builder.Services.AddScoped<IInvoiceGenerator, InvoiceGenerator>();
// ... 150 more lines ...

問題点:

  • 関連する登録を見つけにくい
  • サブシステム間の境界が明確でない
  • テストで構成を再利用できない
  • チームで作業する際にマージ競合が発生する
  • 内部依存関係をカプセル化できない

解決策:拡張メソッドによる構成

関連する登録を拡張メソッドにまとめます。

// GOOD: Clean, composable Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddUserServices()
    .AddOrderServices()
    .AddEmailServices()
    .AddPaymentServices()
    .AddValidators();

var app = builder.Build();

各 Add* メソッドは、まとまりのある登録一式をカプセル化します。


拡張メソッドのパターン

基本構造
namespace MyApp.Users;

public static class UserServiceCollectionExtensions
{
    public static IServiceCollection AddUserServices(this IServiceCollection services)
    {
        // Repositories
        services.AddScoped<IUserRepository, UserRepository>();
        services.AddScoped<IUserReadStore, UserReadStore>();
        services.AddScoped<IUserWriteStore, UserWriteStore>();

        // Services
        services.AddScoped<IUserService, UserService>();
        services.AddScoped<IUserValidationService, UserValidationService>();

        // Return for chaining
        return services;
    }
}
構成を指定する場合
namespace MyApp.Email;

public static class EmailServiceCollectionExtensions
{
    public static IServiceCollection AddEmailServices(
        this IServiceCollection services,
        string configSectionName = "EmailSettings")
    {
        // Bind configuration
        services.AddOptions<EmailOptions>()
            .BindConfiguration(configSectionName)
            .ValidateDataAnnotations()
            .ValidateOnStart();

        // Register services
        services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
        services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
        services.AddScoped<IUserEmailComposer, UserEmailComposer>();
        services.AddScoped<IOrderEmailComposer, OrderEmailComposer>();

        // SMTP client depends on environment
        services.AddScoped<IEmailSender, SmtpEmailSender>();

        return services;
    }
}
他の拡張メソッドへの依存関係がある場合
namespace MyApp.Orders;

public static class OrderServiceCollectionExtensions
{
    public static IServiceCollection AddOrderServices(this IServiceCollection services)
    {
        // This subsystem depends on email services
        // Caller is responsible for calling AddEmailServices() first
        // Or we can call it here if it's idempotent

        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<IOrderService, OrderService>();
        services.AddScoped<IOrderEmailNotifier, OrderEmailNotifier>();

        return services;
    }
}

ファイル構成

拡張メソッドは、登録するサービスの近くに配置します。

src/
  MyApp.Api/
    Program.cs                    # Composes all Add* methods
  MyApp.Users/
    Services/
      UserService.cs
      IUserService.cs
    Repositories/
      UserRepository.cs
    UserServiceCollectionExtensions.cs   # AddUserServices()
  MyApp.Orders/
    Services/
      OrderService.cs
    OrderServiceCollectionExtensions.cs  # AddOrderServices()
  MyApp.Email/
    Composers/
      UserEmailComposer.cs
    EmailServiceCollectionExtensions.cs  # AddEmailServices()

規約:{Feature}ServiceCollectionExtensions.cs を、その機能のサービスと同じ場所に配置します。


命名規則

パターン用途
Add{Feature}Services()一般的な機能の登録
Add{Feature}()曖昧さがない場合の短縮形
Configure{Feature}()主にオプションを設定する場合
Use{Feature}()ミドルウェア(IApplicationBuilder 用)
// Feature services
services.AddUserServices();
services.AddEmailServices();
services.AddPaymentServices();

// Third-party integrations
services.AddStripePayments();
services.AddSendGridEmail();

// Configuration-heavy
services.ConfigureAuthentication();
services.ConfigureAuthorization();

テストでの利点

主な利点は、本番環境の構成をテストで再利用できることです。

WebApplicationFactory
public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public ApiTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Production services already registered via Add* methods
                // Only override what's different for testing

                // Replace email sender with test double
                services.RemoveAll<IEmailSender>();
                services.AddSingleton<IEmailSender, TestEmailSender>();

                // Replace external payment processor
                services.RemoveAll<IPaymentProcessor>();
                services.AddSingleton<IPaymentProcessor, FakePaymentProcessor>();
            });
        });
    }

    [Fact]
    public async Task CreateOrder_SendsConfirmationEmail()
    {
        var client = _factory.CreateClient();
        var emailSender = _factory.Services.GetRequiredService<IEmailSender>() as TestEmailSender;

        await client.PostAsJsonAsync("/api/orders", new CreateOrderRequest(...));

        Assert.Single(emailSender!.SentEmails);
    }
}
Akka.Hosting.TestKit
public class OrderActorSpecs : Akka.Hosting.TestKit.TestKit
{
    protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
    {
        // Reuse production Akka configuration
        builder.AddOrderActors();
    }

    protected override void ConfigureServices(IServiceCollection services)
    {
        // Reuse production service configuration
        services.AddOrderServices();

        // Override only external dependencies
        services.RemoveAll<IPaymentProcessor>();
        services.AddSingleton<IPaymentProcessor, FakePaymentProcessor>();
    }

    [Fact]
    public async Task OrderActor_ProcessesPayment()
    {
        var orderActor = ActorRegistry.Get<OrderActor>();
        orderActor.Tell(new ProcessOrder(orderId));

        ExpectMsg<OrderProcessed>();
    }
}
単独の単体テスト
public class UserServiceTests
{
    private readonly ServiceProvider _provider;

    public UserServiceTests()
    {
        var services = new ServiceCollection();

        // Reuse production registrations
        services.AddUserServices();

        // Add test infrastructure
        services.AddSingleton<IUserRepository, InMemoryUserRepository>();

        _provider = services.BuildServiceProvider();
    }

    [Fact]
    public async Task CreateUser_ValidData_Succeeds()
    {
        var service = _provider.GetRequiredService<IUserService>();
        var result = await service.CreateUserAsync(new CreateUserRequest(...));

        Assert.True(result.IsSuccess);
    }
}

階層化された拡張メソッド

大規模なアプリケーションでは、拡張メソッドを階層的に構成します。

// Top-level: Everything the app needs
public static class AppServiceCollectionExtensions
{
    public static IServiceCollection AddAppServices(this IServiceCollection services)
    {
        return services
            .AddDomainServices()
            .AddInfrastructureServices()
            .AddApiServices();
    }
}

// Domain layer
public static class DomainServiceCollectionExtensions
{
    public static IServiceCollection AddDomainServices(this IServiceCollection services)
    {
        return services
            .AddUserServices()
            .AddOrderServices()
            .AddProductServices();
    }
}

// Infrastructure layer
public static class InfrastructureServiceCollectionExtensions
{
    public static IServiceCollection AddInfrastructureServices(this IServiceCollection services)
    {
        return services
            .AddEmailServices()
            .AddPaymentServices()
            .AddStorageServices();
    }
}

Akka.Hosting との統合

同じパターンを Akka.NET アクターの構成にも使用できます。

public static class OrderActorExtensions
{
    public static AkkaConfigurationBuilder AddOrderActors(
        this AkkaConfigurationBuilder builder)
    {
        return builder
            .WithActors((system, registry, resolver) =>
            {
                var orderProps = resolver.Props<OrderActor>();
                var orderRef = system.ActorOf(orderProps, "orders");
                registry.Register<OrderActor>(orderRef);
            })
            .WithShardRegion<OrderShardActor>(
                typeName: "order-shard",
                (system, registry, resolver) =>
                    entityId => resolver.Props<OrderShardActor>(entityId),
                new OrderMessageExtractor(),
                ShardOptions.Create());
    }
}

// Usage in Program.cs
builder.Services.AddAkka("MySystem", (builder, sp) =>
{
    builder
        .AddOrderActors()
        .AddInventoryActors()
        .AddNotificationActors();
});

Akka.Hosting のパターン全般については、akka/hosting-actor-patterns スキルを参照してください。


一般的なパターン

条件付き登録
public static IServiceCollection AddEmailServices(
    this IServiceCollection services,
    IHostEnvironment environment)
{
    services.AddSingleton<IEmailComposer, MjmlEmailComposer>();

    if (environment.IsDevelopment())
    {
        // Use Mailpit in development
        services.AddSingleton<IEmailSender, MailpitEmailSender>();
    }
    else
    {
        // Use real SMTP in production
        services.AddSingleton<IEmailSender, SmtpEmailSender>();
    }

    return services;
}
ファクトリーベースの登録
public static IServiceCollection AddPaymentServices(
    this IServiceCollection services,
    string configSection = "Stripe")
{
    services.AddOptions<StripeOptions>()
        .BindConfiguration(configSection)
        .ValidateOnStart();

    // Factory for complex initialization
    services.AddSingleton<IPaymentProcessor>(sp =>
    {
        var options = sp.GetRequiredService<IOptions<StripeOptions>>().Value;
        var logger = sp.GetRequiredService<ILogger<StripePaymentProcessor>>();

        return new StripePaymentProcessor(options.ApiKey, options.WebhookSecret, logger);
    });

    return services;
}
キー付きサービス(.NET 8+)
public static IServiceCollection AddNotificationServices(this IServiceCollection services)
{
    // Register multiple implementations with keys
    services.AddKeyedSingleton<INotificationSender, EmailNotificationSender>("email");
    services.AddKeyedSingleton<INotificationSender, SmsNotificationSender>("sms");
    services.AddKeyedSingleton<INotificationSender, PushNotificationSender>("push");

    // Resolver that picks the right one
    services.AddScoped<INotificationDispatcher, NotificationDispatcher>();

    return services;
}

アンチパターン

避ける:すべてを Program.cs に登録する
// BAD: Massive Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
// ... 200 more lines ...
避ける:汎用的すぎる拡張メソッドを作成する
// BAD: Too vague, doesn't communicate what's registered
public static IServiceCollection AddServices(this IServiceCollection services)
{
    // Registers 50 random things
}
避ける:重要な構成を隠す
// BAD: Buried important settings
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options =>
        options.UseSqlServer("hardcoded-connection-string"));  // Hidden!
}

// GOOD: Accept configuration explicitly
public static IServiceCollection AddDatabase(
    this IServiceCollection services,
    string connectionString)
{
    services.AddDbContext<AppDbContext>(options =>
        options.UseSqlServer(connectionString));
}

ベストプラクティスのまとめ

実践項目利点
関連するサービスを Add* メソッドにまとめるProgram.cs がすっきりし、境界が明確になる
拡張メソッドを登録対象のサービスの近くに配置する見つけやすく、保守しやすい
チェーン呼び出しに対応するため IServiceCollection を返す流れるような API
構成パラメーターを受け取る柔軟性
一貫した命名規則(Add{Feature}Services)を使用する見つけやすさ
本番環境の拡張メソッドを再利用してテストする信頼性の向上、重複の削減

ライフタイムの管理

状態に応じて適切なライフタイムを選択します。

ライフタイム使用する場面例
Singletonステートレスでスレッドセーフ、生成コストが高い場合構成、HttpClient ファクトリー、キャッシュ
Scopedリクエストごとに状態を持つ場合、データベースコンテキストDbContext、リポジトリ、ユーザーコンテキスト
Transient軽量で状態を持ち、生成コストが低い場合バリデーター、短期間使用するヘルパー
経験則
// SINGLETON: Stateless services, shared safely
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();

// SCOPED: Database access, per-request state
services.AddScoped<IUserRepository, UserRepository>();  // DbContext dependency
services.AddScoped<IOrderService, OrderService>();       // Uses scoped repos

// TRANSIENT: Cheap, short-lived
services.AddTransient<CreateUserRequestValidator>();
スコープの要件

Scoped サービスにはスコープが必要です。 ASP.NET Core では、各 HTTP リクエストによってスコープが自動的に作成されます。ただし、他のコンテキスト(バックグラウンドサービス、アクター)では、手動でスコープを作成する必要があります。

// ASP.NET Controller - scope exists automatically
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;  // Scoped - works!

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }
}

// Background Service - no automatic scope!
public class OrderProcessingService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public OrderProcessingService(IServiceProvider serviceProvider)
    {
        // Inject IServiceProvider, NOT scoped services directly
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            // Create scope manually for each unit of work
            using var scope = _serviceProvider.CreateScope();
            var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();

            await orderService.ProcessPendingOrdersAsync(ct);
            await Task.Delay(TimeSpan.FromMinutes(1), ct);
        }
    }
}

Akka.NET アクターのスコープ管理

アクターには DI スコープが自動的に作成されません。 アクター内で Scoped サービスが必要な場合は、IServiceProvider を注入し、手動でスコープを作成します。

パターン:メッセージごとのスコープ
public sealed class AccountProvisionActor : ReceiveActor
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IActorRef _mailingActor;

    public AccountProvisionActor(
        IServiceProvider serviceProvider,
        IRequiredActor<MailingActor> mailingActor)
    {
        _serviceProvider = serviceProvider;
        _mailingActor = mailingActor.ActorRef;

        ReceiveAsync<ProvisionAccount>(HandleProvisionAccount);
    }

    private async Task HandleProvisionAccount(ProvisionAccount msg)
    {
        // Create scope for this message processing
        using var scope = _serviceProvider.CreateScope();

        // Resolve scoped services
        var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
        var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
        var emailComposer = scope.ServiceProvider.GetRequiredService<IPaymentEmailComposer>();

        // Do work with scoped services
        var user = await userManager.FindByIdAsync(msg.UserId);
        var order = await orderRepository.CreateAsync(msg.Order);

        // DbContext commits when scope disposes
    }
}
このパターンが有効な理由
  1. 各メッセージで新しい DbContext を取得します - 古いエンティティの追跡が発生しません
  2. 適切に破棄されます - 各メッセージの処理後に接続が解放されます
  3. 分離されています - あるメッセージのエラーがほかのメッセージに影響しません
  4. テスト可能です - モックの IServiceProvider を注入できます
アクター内のシングルトンサービス

ステートレスなサービスは、直接注入してください(スコープは不要です)。

public sealed class NotificationActor : ReceiveActor
{
    private readonly IEmailLinkGenerator _linkGenerator;  // Singleton - OK!
    private readonly IActorRef _mailingActor;

    public NotificationActor(
        IEmailLinkGenerator linkGenerator,  // Direct injection
        IRequiredActor<MailingActor> mailingActor)
    {
        _linkGenerator = linkGenerator;
        _mailingActor = mailingActor.ActorRef;

        Receive<SendWelcomeEmail>(Handle);
    }
}
Akka.DependencyInjection のリファレンス

Akka.NET の DI 統合については、以下に記載されています。


よくある間違い

シングルトンへのスコープ付きサービスの注入
// BAD: Singleton captures scoped service - stale DbContext!
public class CacheService  // Registered as Singleton
{
    private readonly IUserRepository _repo;  // Scoped!

    public CacheService(IUserRepository repo)  // Captured at startup!
    {
        _repo = repo;  // This DbContext lives forever - BAD
    }
}

// GOOD: Inject factory or IServiceProvider
public class CacheService
{
    private readonly IServiceProvider _serviceProvider;

    public CacheService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task<User> GetUserAsync(string id)
    {
        using var scope = _serviceProvider.CreateScope();
        var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
        return await repo.GetByIdAsync(id);
    }
}
バックグラウンド処理でのスコープの未作成
// BAD: No scope for scoped services
public class BadBackgroundService : BackgroundService
{
    private readonly IOrderService _orderService;  // Scoped!

    public BadBackgroundService(IOrderService orderService)
    {
        _orderService = orderService;  // Will throw or behave unexpectedly
    }
}

// GOOD: Create scope for each unit of work
public class GoodBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public GoodBackgroundService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        using var scope = _scopeFactory.CreateScope();
        var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
        // ...
    }
}

リソース

コメント