メインコンテンツまでスキップ

Dependency Injection(依存性注入)

Dependency Injection(依存性注入)とは

Dependency Injection(DI)は、クラスが必要とする依存関係を外部から注入するデザインパターンです。これにより、クラス間の結合度を下げ、テストしやすく、保守性の高いコードを書くことができます。

DIを使わない場合の問題点

public class OrderService
{
private readonly EmailService _emailService;

public OrderService()
{
// クラス内部で依存関係を直接生成
_emailService = new EmailService();
}

public void ProcessOrder(Order order)
{
// 注文処理
_emailService.SendConfirmation(order);
}
}

問題点:

  • OrderServiceEmailServiceに強く依存している
  • テスト時にモックに置き換えることが困難
  • EmailServiceの変更がOrderServiceに影響する
  • 柔軟性が低い

DIを使った場合

public interface IEmailService
{
void SendConfirmation(Order order);
}

public class EmailService : IEmailService
{
public void SendConfirmation(Order order)
{
// メール送信処理
}
}

public class OrderService
{
private readonly IEmailService _emailService;

// コンストラクタで依存関係を注入
public OrderService(IEmailService emailService)
{
_emailService = emailService;
}

public void ProcessOrder(Order order)
{
// 注文処理
_emailService.SendConfirmation(order);
}
}

メリット:

  • インターフェースを使用することで結合度が下がる
  • テスト時にモック実装を注入できる
  • 実装の切り替えが容易
  • 依存関係が明確になる

DIコンテナとは

DIコンテナ(Dependency Injection Container)は、依存関係の登録、解決、ライフタイム管理を自動的に行うフレームワークです。IoC(Inversion of Control:制御の反転)コンテナとも呼ばれます。

DIコンテナの役割

DIコンテナは主に以下の3つの役割を担います:

1. サービスの登録(Registration)

アプリケーション起動時に、インターフェースと実装クラスの対応関係をコンテナに登録します。

// DIコンテナにサービスを登録
var services = new ServiceCollection();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IOrderRepository, OrderRepository>();

2. 依存関係の解決(Resolution)

必要な時にコンテナからサービスのインスタンスを取得します。コンテナは依存関係を自動的に解決し、必要なオブジェクトをすべて生成・注入します。

// コンテナからサービスを取得
var serviceProvider = services.BuildServiceProvider();
var orderService = serviceProvider.GetRequiredService<OrderService>();
// OrderServiceが必要とするIEmailService、IOrderRepositoryも自動的に注入される

3. ライフタイム管理(Lifetime Management)

サービスの生成と破棄のタイミングを管理します(Transient、Scoped、Singleton)。

.NETのDIコンテナ

.NET Coreには標準でDIコンテナが組み込まれており、Microsoft.Extensions.DependencyInjection名前空間で提供されています。

// ASP.NET Coreアプリケーションの場合
var builder = WebApplication.CreateBuilder(args);

// builder.Services がIServiceCollection(DIコンテナ)
builder.Services.AddScoped<IEmailService, EmailService>();

var app = builder.Build();
// app.Services がIServiceProvider(サービスの取得に使用)

DIコンテナの動作原理

DIコンテナはコンストラクタインジェクションを使用して、依存関係を自動的に解決します。

// 1. サービスの登録
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IPaymentService, PaymentService>();
services.AddScoped<OrderService>();

// 2. OrderServiceの定義
public class OrderService
{
public OrderService(IEmailService emailService, IPaymentService paymentService)
{
// コンストラクタで依存関係を受け取る
}
}

// 3. DIコンテナによる解決プロセス
// OrderServiceが要求される
// ↓
// コンテナがOrderServiceのコンストラクタを確認
// ↓
// IEmailServiceとIPaymentServiceが必要だと認識
// ↓
// EmailServiceとPaymentServiceのインスタンスを作成
// ↓
// それらをOrderServiceのコンストラクタに渡す
// ↓
// OrderServiceのインスタンスが返される

DIコンテナを使わない手動DI

DIコンテナを使わずに手動で依存関係を注入することも可能ですが、複雑なアプリケーションでは管理が困難になります。

// 手動でのDI(DIコンテナなし)
var dbContext = new ApplicationDbContext(options);
var emailService = new EmailService(emailSettings);
var paymentService = new PaymentService(paymentSettings);
var orderRepository = new OrderRepository(dbContext);
var orderService = new OrderService(orderRepository, emailService, paymentService);

// 問題点:
// - 依存関係が増えるとコードが複雑になる
// - ライフタイム管理を手動で行う必要がある
// - 依存関係の変更時に多くの箇所を修正する必要がある

DIコンテナのメリット

  1. 自動的な依存関係解決: コンストラクタを見て必要なサービスを自動的に注入
  2. ライフタイム管理の自動化: オブジェクトの生成と破棄を自動管理
  3. 設定の一元化: すべてのサービス登録を一箇所で管理
  4. 保守性の向上: 依存関係の変更が容易
  5. テスト容易性: モックやスタブへの置き換えが簡単

サービスロケーターパターンとの違い

DIコンテナと混同されやすいパターンにサービスロケーターがありますが、これはアンチパターンとされています。

// ❌ サービスロケーターパターン(アンチパターン)
public class OrderService
{
public void ProcessOrder(Order order)
{
// クラス内部でコンテナから直接取得
var emailService = ServiceLocator.GetService<IEmailService>();
emailService.SendConfirmation(order);
}
}

// ✅ DIコンテナを使った正しいパターン
public class OrderService
{
private readonly IEmailService _emailService;

// コンストラクタで注入
public OrderService(IEmailService emailService)
{
_emailService = emailService;
}

public void ProcessOrder(Order order)
{
_emailService.SendConfirmation(order);
}
}

サービスロケーターが避けられる理由:

  • 依存関係が隠蔽され、クラスの依存関係が不明確
  • テストが困難(依存関係がコンストラクタに現れない)
  • 実行時エラーが発生しやすい

Service Collectionとは

.NET Coreでは、IServiceCollectionを使用してDIコンテナにサービスを登録します。これにより、アプリケーション全体で依存関係を管理できます。

サービスの登録

ASP.NET Coreアプリケーションでは、通常Program.csまたはStartup.csでサービスを登録します。

var builder = WebApplication.CreateBuilder(args);

// サービスの登録
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();

サービスのライフタイム

DIコンテナでは、3種類のライフタイムを指定できます:

1. Transient(一時的)

builder.Services.AddTransient<IMyService, MyService>();
  • リクエストされるたびに新しいインスタンスを作成
  • 軽量でステートレスなサービスに適している
  • メモリ使用量が多くなる可能性がある

使用例: ユーティリティクラス、軽量なヘルパークラス

2. Scoped(スコープ付き)

builder.Services.AddScoped<IMyService, MyService>();
  • HTTPリクエストごとに1つのインスタンスを作成
  • 同じリクエスト内では同じインスタンスが使用される
  • 最も一般的に使用される

使用例: データベースコンテキスト(Entity Framework Core)、リポジトリ、ビジネスロジック

3. Singleton(シングルトン)

builder.Services.AddSingleton<IMyService, MyService>();
  • アプリケーション起動時に1つのインスタンスを作成
  • アプリケーション全体で同じインスタンスが使用される
  • スレッドセーフである必要がある

使用例: 設定オブジェクト、キャッシュ、ロガー

ライフタイムの選択ガイド

ライフタイムいつ使うか注意点
Transient軽量でステートレスなサービス多くのインスタンスが作成されるため、重いオブジェクトには不向き
Scopedリクエストごとに状態を保持するサービスWeb APIやMVCアプリケーションで最も一般的
Singletonアプリケーション全体で共有するサービススレッドセーフでなければならない

実践例

1. 基本的なサービス登録と使用

// インターフェース定義
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
}

// 実装
public class ProductRepository : IProductRepository
{
private readonly ApplicationDbContext _context;

public ProductRepository(ApplicationDbContext context)
{
_context = context;
}

public async Task<Product> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id);
}

public async Task<IEnumerable<Product>> GetAllAsync()
{
return await _context.Products.ToListAsync();
}
}

// サービス登録(Program.cs)
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IProductRepository, ProductRepository>();

// コントローラーでの使用
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;

public ProductsController(IProductRepository repository)
{
_repository = repository;
}

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
return NotFound();

return Ok(product);
}
}

2. 複数の依存関係の注入

public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IEmailService _emailService;
private readonly IPaymentService _paymentService;
private readonly ILogger<OrderService> _logger;

// 複数の依存関係をコンストラクタで注入
public OrderService(
IOrderRepository orderRepository,
IEmailService emailService,
IPaymentService paymentService,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_emailService = emailService;
_paymentService = paymentService;
_logger = logger;
}

public async Task<bool> ProcessOrderAsync(Order order)
{
try
{
_logger.LogInformation($"Processing order {order.Id}");

var paymentSuccess = await _paymentService.ProcessPaymentAsync(order);
if (!paymentSuccess)
{
_logger.LogWarning($"Payment failed for order {order.Id}");
return false;
}

await _orderRepository.SaveAsync(order);
await _emailService.SendConfirmationAsync(order);

_logger.LogInformation($"Order {order.Id} processed successfully");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing order {order.Id}");
throw;
}
}
}

// サービス登録
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<IPaymentService, PaymentService>();
builder.Services.AddScoped<OrderService>();

3. 設定オブジェクトの注入

// appsettings.json
{
"EmailSettings": {
"SmtpServer": "smtp.example.com",
"Port": 587,
"SenderEmail": "noreply@example.com"
}
}

// 設定クラス
public class EmailSettings
{
public string SmtpServer { get; set; }
public int Port { get; set; }
public string SenderEmail { get; set; }
}

// サービス登録
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection("EmailSettings"));

// 使用例
public class EmailService : IEmailService
{
private readonly EmailSettings _settings;

public EmailService(IOptions<EmailSettings> options)
{
_settings = options.Value;
}

public async Task SendEmailAsync(string to, string subject, string body)
{
// _settings.SmtpServer などを使用してメール送信
}
}

C# 12 Primary Constructorsによる簡略化

C# 12で導入された**Primary Constructors(プライマリコンストラクタ)**を使用すると、DIの記述を大幅に簡略化できます。

従来の書き方

これまでのC#では、依存関係を受け取るためにプライベートフィールドの宣言とコンストラクタでの代入が必要でした。

public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IEmailService _emailService;

public OrderService(IOrderRepository orderRepository, IEmailService emailService)
{
_orderRepository = orderRepository;
_emailService = emailService;
}

public void Process()
{
_orderRepository.Save();
_emailService.Send();
}
}

Primary Constructorsを使った書き方

クラス名の直後に引数を定義することで、フィールドの宣言とコンストラクタの定義を省略できます。

// クラス定義で直接依存関係を受け取る
public class OrderService(IOrderRepository orderRepository, IEmailService emailService)
{
public void Process()
{
// 引数をそのままフィールドのように使用可能
orderRepository.Save();
emailService.Send();
}
}

メリット:

  • ボイラープレートコード(フィールド宣言、コンストラクタ、代入処理)を削減できる
  • コードが読みやすくなる
  • 依存関係がクラス定義のトップに明示される

注意点:

  • Primary Constructorの引数は、クラス内のどこからでもアクセス可能ですが、明示的にreadonlyフィールドとして定義されるわけではありません(キャプチャされた場合は実質的にフィールドのように振る舞います)。
  • 従来の_プレフィックス付きフィールドを使用したい場合は、明示的にフィールドを定義して初期化することも可能です。
public class OrderService(IOrderRepository orderRepository)
{
// プライベートフィールドに代入することも可能
private readonly IOrderRepository _orderRepository = orderRepository;
}

テストでのDI活用

DIを使用することで、ユニットテストが容易になります。

public class OrderServiceTests
{
[Fact]
public async Task ProcessOrder_Success_ReturnsTrue()
{
// Arrange - モックの準備
var mockOrderRepo = new Mock<IOrderRepository>();
var mockEmailService = new Mock<IEmailService>();
var mockPaymentService = new Mock<IPaymentService>();
var mockLogger = new Mock<ILogger<OrderService>>();

mockPaymentService
.Setup(x => x.ProcessPaymentAsync(It.IsAny<Order>()))
.ReturnsAsync(true);

var service = new OrderService(
mockOrderRepo.Object,
mockEmailService.Object,
mockPaymentService.Object,
mockLogger.Object);

var order = new Order { Id = 1, Total = 100 };

// Act
var result = await service.ProcessOrderAsync(order);

// Assert
Assert.True(result);
mockPaymentService.Verify(x => x.ProcessPaymentAsync(order), Times.Once);
mockEmailService.Verify(x => x.SendConfirmationAsync(order), Times.Once);
}
}

ベストプラクティス

1. インターフェースを使用する

具象クラスではなくインターフェースに依存することで、柔軟性とテスト容易性が向上します。

// ✅ 推奨
builder.Services.AddScoped<IEmailService, EmailService>();

// ❌ 非推奨
builder.Services.AddScoped<EmailService>();

2. 適切なライフタイムを選択する

サービスの性質に応じて適切なライフタイムを選択してください。

// データベースコンテキスト - Scoped
builder.Services.AddDbContext<AppDbContext>();

// ロガー - Singleton
builder.Services.AddSingleton<ILogger, FileLogger>();

// ユーティリティクラス - Transient
builder.Services.AddTransient<IDateTimeProvider, DateTimeProvider>();

3. コンストラクタ注入を優先する

フィールド注入やプロパティ注入ではなく、コンストラクタ注入を使用してください。

// ✅ 推奨 - コンストラクタ注入
public class MyService
{
private readonly IDependency _dependency;

public MyService(IDependency dependency)
{
_dependency = dependency ?? throw new ArgumentNullException(nameof(dependency));
}
}

4. Singleton内でScopedサービスを使用しない

ライフタイムの長いサービスは、短いサービスを直接注入してはいけません。

// ❌ 避けるべき
public class MySingletonService
{
private readonly IScopedService _scopedService; // 問題!

public MySingletonService(IScopedService scopedService)
{
_scopedService = scopedService;
}
}

// ✅ 代替案 - IServiceScopeFactoryを使用
public class MySingletonService
{
private readonly IServiceScopeFactory _scopeFactory;

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

public async Task DoWorkAsync()
{
using var scope = _scopeFactory.CreateScope();
var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
await scopedService.DoSomethingAsync();
}
}

まとめ

  • **Dependency Injection(DI)**は、クラスの依存関係を外部から注入するデザインパターン
  • DIコンテナは、サービスの登録・解決・ライフタイム管理を自動的に行うフレームワーク
  • .NETには標準でDIコンテナが組み込まれている(Microsoft.Extensions.DependencyInjection)
  • Service Collectionを使用してDIコンテナにサービスを登録
  • 3つのライフタイム: Transient(毎回新規)、Scoped(リクエスト毎)、Singleton(1つのみ)
  • インターフェースを使用することでテスト容易性と柔軟性が向上
  • 適切なライフタイムの選択とコンストラクタ注入の使用が重要

DIとDIコンテナを正しく使用することで、保守性が高く、テストしやすい、拡張可能なアプリケーションを構築できます。