依存性注入パターン は何をしますか?
このスキルを使用するタイミング
以下の場合にこのスキルを使用します:
- ASP.NET Coreアプリケーションでサービス登録を整理する場合
- 数百行もの登録がある巨大なProgram.cs/Startup.csファイルを避ける場合
- 本番環境とテスト間でサービス設定を再利用可能にする場合
- Microsoft.Extensions.DependencyInjection と統合するライブラリを設計する場合
参考ファイル
- advanced-patterns.md: DI拡張を使用したテスト、Akka.NETアクタースコープ管理、条件付き/ファクトリ/キー付き登録パターン
問題点
整理されていない場合、Program.csは管理不能になります:
// 悪い例: 200行以上の整理されていない登録
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行 ...
問題点: 関連する登録を見つけにくい、明確な境界がない、テストで再利用できない、マージコンフリクトが発生する。
解決策: 拡張メソッドによる構成
関連する登録を拡張メソッドにグループ化します:
// 良い例: クリーンで構成可能な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 # すべてのAdd*メソッドを構成
MyApp.Users/
Services/
UserService.cs
UserServiceCollectionExtensions.cs # AddUserServices()
MyApp.Orders/
OrderServiceCollectionExtensions.cs # AddOrderServices()
MyApp.Email/
EmailServiceCollectionExtensions.cs # AddEmailServices()
命名規則: {機能名}ServiceCollectionExtensions.cs をその機能のサービスの隣に配置します。
命名規則
| パターン | 使用目的 |
|---|---|
Add{機能名}Services() | 一般的な機能登録 |
Add{機能名}() | 曖昧さがない場合の短縮形 |
Configure{機能名}() | 主にオプション設定時 |
Use{機能名}() | ミドルウェア(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);
});
}
}
// Program.cs での使用例
builder.Services.AddAkka("MySystem", (builder, sp) =>
{
builder
.AddOrderActors()
.AddInventoryActors()
.AddNotificationActors();
});
完全なAkka.Hostingパターンについては akka-hosting-actor-patterns スキルを参照してください。
アンチパターン
避けるべき: Program.cs にすべてを登録する
// 悪い例: 200行以上の登録がある巨大なProgram.cs
避けるべき: 過度に汎用的な拡張メソッドを作成する
// 悪い例: 曖昧すぎて何が登録されるかわからない
public static IServiceCollection AddServices(this IServiceCollection services) { ... }
避けるべき: 重要な設定を隠す
// 悪い例: 設定が埋もれている
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer("ハードコードされた接続文字列")); // 隠れている!
}
// 良い例: 設定を明示的に受け取る
public static IServiceCollection AddDatabase(
this IServiceCollection services,
string connectionString)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
}
ベストプラクティスまとめ
| 実践 | 利点 |
|---|---|
関連するサービスを Add* メソッドにグループ化 | クリーンなProgram.cs、明確な境界 |
| 拡張メソッドを登録するサービスの近くに配置 | 見つけやすく、保守しやすい |
メソッドチェーンのために IServiceCollection を返す | 流暢なAPI |
| 設定パラメータを受け取る | 柔軟性 |
一貫した命名(Add{機能名}Services)を使用 | 発見可能性 |
| 本番拡張メソッドを再利用してテスト | 信頼性、重複削減 |
ライフタイム管理
| ライフタイム | 使用するタイミング | 例 |
|---|---|---|
| シングルトン | ステートレス、スレッドセーフ、生成コストが高い | 設定、HttpClientファクトリ、キャッシュ |
| スコープ | リクエストごとにステートフル、データベースコンテキスト | DbContext、リポジトリ、ユーザーコンテキスト |
| トランジェント | 軽量、ステートフルだが生成コストが低い | バリデーター、短命のヘルパー |
// シングルトン: ステートレスなサービス、安全に共有可能
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
// スコープ: データベースアクセス、リクエストごとの状態
services.AddScoped<IUserRepository, UserRepository>();
// トランジェント: 軽量、短命
services.AddTransient<CreateUserRequestValidator>();
スコープサービスにはスコープが必要です。 ASP.NET CoreはHTTPリクエストごとにスコープを作成します。バックグラウンドサービスやアクターでは、手動でスコープを作成します。
アクタースコープ管理パターンについては advanced-patterns.md を参照してください。
よくある間違い
スコープサービスをシングルトンに注入する
// 悪い例: シングルトンがスコープサービスをキャプチャ - 古いDbContextに!
public class CacheService // シングルトンとして登録
{
private readonly IUserRepository _repo; // スコープ - 起動時にキャプチャ!
}
// 良い例: IServiceProviderを注入し、操作ごとにスコープを作成
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);
}
}
バックグラウンド処理でスコープを作成しない
// 悪い例: スコープサービスのためのスコープがない
public class BadBackgroundService : BackgroundService
{
private readonly IOrderService _orderService; // スコープ - 例外が発生する!
}
// 良い例: 処理単位ごとにスコープを作成
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>();
// ...
}
}
リソース
- Microsoft.Extensions.DependencyInjection: https://learn.microsoft.com/ja-jp/dotnet/core/extensions/dependency-injection
- Akka.Hosting: https://github.com/akkadotnet/Akka.Hosting
- Akka.DependencyInjection: https://getakka.net/articles/actors/dependency-injection.html
- Options Pattern:
microsoft-extensions-configurationスキルを参照