mcpField
D
dependency-injection-patterns

dependency-injection-patterns

github.com/aaronontheweb開発1.2k681 回インストール今日更新
リポジトリ

dependency-injection-patterns について

ASP.NET Core 開発者向けの依存性注入パターンをまとめたスキルです。拡張メソッドによる登録整理、本番設定のテストでの再利用、サービスのライフタイムとスコープ管理を扱います。

主な機能

  1. 01機能ごとのサービス登録整理
  2. 02連鎖可能な拡張メソッド
  3. 03階層的な登録構成
  4. 04本番設定のテストでの再利用
  5. 05サービスのライフタイムとスコープ管理

活用シーン

  1. 01大規模な Program.cs の登録整理
  2. 02テストでのサービス設定再利用
  3. 03設定可能なサービスライブラリの構築
  4. 04バックグラウンド処理のスコープ管理

インストール

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

$npx skills add https://github.com/aaronontheweb/dotnet-skills --skill dependency-injection-patterns

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

同梱ファイルadvanced-patterns.md

詳細

依存性注入のパターン

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

以下の場合にこのスキルを使用してください。

  • ASP.NET Core アプリケーションでサービス登録を整理するとき
  • 数百件の登録で Program.cs/Startup.cs が肥大化するのを防ぐとき
  • 本番環境とテストでサービス構成を再利用できるようにするとき
  • Microsoft.Extensions.DependencyInjection と連携するライブラリを設計するとき

参照ファイル

  • advanced-patterns.md:DI 拡張機能を使ったテスト、Akka.NET アクターのスコープ管理、条件付き/ファクトリー/キー付き登録のパターン

問題点

整理しないと、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>();
// ... 150 more lines ...

問題点:関連する登録を見つけにくく、境界が明確でなく、テストで再利用できず、マージ時の競合が発生します。


解決策:拡張メソッドの構成

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

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

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

var app = builder.Build();

拡張メソッドのパターン

基本構造
namespace MyApp.Users;

public static class UserServiceCollectionExtensions
{
    public static IServiceCollection AddUserServices(this IServiceCollection services)
    {
        services.AddScoped<IUserRepository, UserRepository>();
        services.AddScoped<IUserReadStore, UserReadStore>();
        services.AddScoped<IUserWriteStore, UserWriteStore>();
        services.AddScoped<IUserService, UserService>();
        services.AddScoped<IUserValidationService, UserValidationService>();

        return services;
    }
}
構成を伴う場合
namespace MyApp.Email;

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

        services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
        services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
        services.AddScoped<IUserEmailComposer, UserEmailComposer>();
        services.AddScoped<IEmailSender, SmtpEmailSender>();

        return services;
    }
}

ファイル構成

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

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

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


命名規則

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

テストでのメリット

Add* パターンを使うと、本番環境の構成をテストで再利用し、異なる部分だけをオーバーライドできます。WebApplicationFactory、Akka.Hosting.TestKit、スタンドアロンの ServiceCollection で使用できます。

テストの完全な例については、advanced-patterns.md を参照してください。


階層化された拡張機能

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

public static class AppServiceCollectionExtensions
{
    public static IServiceCollection AddAppServices(this IServiceCollection services)
    {
        return services
            .AddDomainServices()
            .AddInfrastructureServices()
            .AddApiServices();
    }
}

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

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);
            });
    }
}

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

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


アンチパターン

避ける:すべてを Program.cs に登録する
// BAD: Massive Program.cs with 200+ lines of registrations
避ける:汎用的すぎる拡張機能を作成する
// BAD: Too vague, doesn't communicate what's registered
public static IServiceCollection AddServices(this IServiceCollection services) { ... }
避ける:重要な構成を隠す
// BAD: Buried 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>();

// SCOPED: Database access, per-request state
services.AddScoped<IUserRepository, UserRepository>();

// TRANSIENT: Cheap, short-lived
services.AddTransient<CreateUserRequestValidator>();

Scoped サービスにはスコープが必要です。 ASP.NET Core は HTTP リクエストごとにスコープを作成します。バックグラウンドサービスやアクターでは、スコープを手動で作成します。

アクターのスコープ管理パターンについては、advanced-patterns.md を参照してください。


よくある間違い

Scoped を Singleton に注入する
// BAD: Singleton captures scoped service - stale DbContext!
public class CacheService  // Registered as Singleton
{
    private readonly IUserRepository _repo;  // Scoped - captured at startup!
}

// GOOD: Inject IServiceProvider, create scope per operation
public class CacheService
{
    private readonly IServiceProvider _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 - will throw!
}

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

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

リソース

コメント