コンテンツにスキップ

第6章:依存性注入 (DI)

  1. DI の基本概念(Interface と具象クラス)
  2. サービスの登録スコープ(Transient / Scoped / Singleton)
  3. ファクトリ登録、条件付き登録、手動スコープ
  4. サービス層設計パターン
  5. 参考ドキュメント

1. DI の基本概念(Interface と具象クラス)

Section titled “1. DI の基本概念(Interface と具象クラス)”

依存性注入 (Dependency Injection: DI) は、クラスとその依存関係の間で 制御の反転 (Inversion of Control: IoC) を実現するためのソフトウェア設計パターンです。
ASP.NET Core には DI が フレームワークの中核機能 として組み込まれており、アプリケーション全体で一貫した方法でサービスの管理・注入を行えます。

DI を使わない場合、あるクラスが別のクラスに依存するコードは次のようになります。

public class OrderController : Controller
{
// ❌ 具象クラスを直接 new している(密結合)
private readonly OrderService _orderService = new OrderService();
public IActionResult Index()
{
var orders = _orderService.GetAll();
return View(orders);
}
}

この実装は、以下の問題を抱えています。

  • OrderService を別の実装に差し替えるには、OrderController のソースコードを変更する必要がある
  • OrderService 自体がさらに別の依存関係を持っている場合、それらもすべて手動で構成しなければならない
  • 単体テスト時にモックへ差し替えることが難しい

DI はこれらの問題を、以下の 3 つのステップで解決します。

  1. インターフェイスによる抽象化 — 依存関係をインターフェイス(契約)で定義する
  2. サービスコンテナーへの登録 — インターフェイスと具象クラスの対応を DI コンテナーに登録する
  3. コンストラクターインジェクション — フレームワークが依存関係のインスタンスを自動で生成し、コンストラクター経由で注入する
flowchart LR
    subgraph registration ["① サービス登録(Program.cs)"]
        reg["builder.Services.AddScoped<IOrderService, OrderService>()"]
    end
    subgraph container ["② DI コンテナー"]
        dic["IServiceProvider"]
    end
    subgraph injection ["③ 注入"]
        ctrl["OrderController(IOrderService orderService)"]
    end
    registration --> container --> injection

インターフェイスと具象クラスによる抽象化

Section titled “インターフェイスと具象クラスによる抽象化”

DI の第一歩は、依存関係を インターフェイス(抽象) で表現することです。
インターフェイスは「何ができるか」を定義し、具象クラスは「どのように実現するか」を定義します。

// インターフェイス — 「注文を取得する」という契約を定義
public interface IOrderService
{
IReadOnlyList<Order> GetAll();
Order? GetById(int id);
void Create(Order order);
}
// 具象クラス — インターフェイスの実装
public class OrderService : IOrderService
{
private readonly AppDbContext _dbContext;
public OrderService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public IReadOnlyList<Order> GetAll()
=> _dbContext.Orders.ToList();
public Order? GetById(int id)
=> _dbContext.Orders.Find(id);
public void Create(Order order)
{
_dbContext.Orders.Add(order);
_dbContext.SaveChanges();
}
}

このようにインターフェイスを介して依存関係を定義することで、次のような利点が得られます。

  • 疎結合: 利用側は具象クラスの内部実装を知る必要がない
  • テスト容易性: テスト時にモック実装を注入できる
  • 差し替え容易性: 例えばデータベースアクセスからインメモリキャッシュに切り替える場合も、登録を変更するだけで対応可能

ASP.NET Core 組み込みの DI コンテナー

Section titled “ASP.NET Core 組み込みの DI コンテナー”

ASP.NET Core は IServiceProvider を基盤とした組み込みの DI コンテナーを提供しています。
サービスの登録は Program.csbuilder.Services (IServiceCollection) に対して行います。

var builder = WebApplication.CreateBuilder(args);
// サービスを DI コンテナーに登録
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();

builder.ServicesIServiceCollection 型であり、サービスの登録情報を保持するコレクションです。
すべてのサービスが追加された後、builder.Build() が呼ばれると内部で BuildServiceProvider() が実行され、サービスコンテナー(IServiceProvider)が構築されます。

コンストラクターインジェクション

Section titled “コンストラクターインジェクション”

ASP.NET Core で最も一般的な注入方法が コンストラクターインジェクション です。
コントローラーやサービスのコンストラクターにインターフェイス型のパラメーターを宣言すると、DI コンテナーが対応するインスタンスを自動的に解決し、注入します。

public class OrderController : Controller
{
private readonly IOrderService _orderService;
// DI コンテナーが IOrderService の実装を自動で注入する
public OrderController(IOrderService orderService)
{
_orderService = orderService;
}
public IActionResult Index()
{
var orders = _orderService.GetAll();
return View(orders);
}
}

コンストラクターインジェクションには public コンストラクター が必要です。
複数のコンストラクターを定義した場合の挙動は、そのクラスを誰が生成するか によって異なります。

生成経路主な対象複数コンストラクター時の挙動
IServiceProviderDI コンテナーに登録した通常のサービスDI で解決可能なパラメーターが最も多いコンストラクターが選択される。優劣を判定できない組み合わせがある場合は例外
ActivatorUtilitiesコントローラーなど、フレームワークが生成するクラス適用可能なコンストラクターが 1 つだけ である必要がある。複数該当すると例外

アクションインジェクション([FromServices]

Section titled “アクションインジェクション([FromServices])”

特定のアクションメソッドでのみ必要なサービスは、コンストラクターではなくメソッドパラメーターに [FromServices] 属性を付与して注入することもできます。

public class ReportController : Controller
{
public IActionResult Generate([FromServices] IReportGenerator generator)
{
var report = generator.CreateMonthlyReport();
return View(report);
}
}

プライマリコンストラクターによる注入(C# 12)

Section titled “プライマリコンストラクターによる注入(C# 12)”

C# 12(.NET 8 以降)で導入された プライマリコンストラクター を使うと、フィールド宣言とコンストラクターの記述を簡潔にできます。

// C# 12 のプライマリコンストラクターを使用した例
public class OrderController(IOrderService orderService) : Controller
{
public IActionResult Index()
{
var orders = orderService.GetAll();
return View(orders);
}
public IActionResult Details(int id)
{
var order = orderService.GetById(id);
if (order is null) return NotFound();
return View(order);
}
}

プライマリコンストラクターのパラメーターはクラス全体のスコープで参照できるため、private readonly フィールドへの代入が不要になります。

Minimal API ではエンドポイントハンドラーのパラメーターに直接サービスを記述することで注入されます。
ASP.NET Core は DI コンテナーに登録済みの型をパラメーターから自動認識します。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();
// IOrderService が自動注入される
app.MapGet("/orders", (IOrderService orderService) =>
{
return Results.Ok(orderService.GetAll());
});
app.MapGet("/orders/{id}", (int id, IOrderService orderService) =>
{
var order = orderService.GetById(id);
return order is not null ? Results.Ok(order) : Results.NotFound();
});
app.Run();

2. サービスの登録スコープ(Transient / Scoped / Singleton)

Section titled “2. サービスの登録スコープ(Transient / Scoped / Singleton)”

サービスを DI コンテナーに登録する際は、そのサービスの ライフタイム(有効期間) を指定します。
ライフタイムは、サービスインスタンスの生成タイミングと破棄タイミングを制御する重要な概念です。

flowchart TD
    subgraph Transient ["Transient(一時的)"]
        T1["要求 ①"] --> TI1["インスタンス A"]
        T2["要求 ②"] --> TI2["インスタンス B"]
        T3["要求 ③"] --> TI3["インスタンス C"]
    end
    subgraph Scoped ["Scoped(スコープ付き)"]
        S1["リクエスト 1 内の要求 ①"] --> SI1["インスタンス X"]
        S2["リクエスト 1 内の要求 ②"] --> SI1
        S3["リクエスト 2 内の要求 ①"] --> SI2["インスタンス Y"]
    end
    subgraph Singleton ["Singleton(シングルトン)"]
        G1["要求 ①"] --> GI["インスタンス Z(唯一)"]
        G2["要求 ②"] --> GI
        G3["要求 ③"] --> GI
    end
ライフタイム登録メソッドインスタンス生成タイミング破棄タイミング
TransientAddTransient<TService, TImpl>()サービスが要求されるたびに毎回解決されたスコープの終了時(Web アプリでは通常リクエスト終了時)
ScopedAddScoped<TService, TImpl>()同一スコープ(HTTP リクエスト)内で最初の要求時スコープの終了時(Web アプリでは通常リクエスト終了時)
SingletonAddSingleton<TService, TImpl>()アプリケーション内で最初の要求時アプリケーション終了時

Transient で登録されたサービスは、DI コンテナーから要求されるたびに 新しいインスタンス が生成されます。

builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
  • 同一リクエスト内であっても、複数箇所で注入されればそれぞれ別のインスタンスが生成される
  • 状態を持たない軽量なサービスに適している
  • IDisposable を実装している場合、解決されたスコープ の終了時にコンテナーによって破棄される

適切な用途: 軽量かつステートレスなサービス(バリデータ、フォーマッタ、一時的な計算ロジックなど)

Scoped で登録されたサービスは、同一の スコープ(通常は HTTP リクエスト) 内で同一のインスタンスが共有されます。異なるリクエスト間では別のインスタンスが生成されます。

builder.Services.AddScoped<IOrderService, OrderService>();
  • ASP.NET Core では HTTP リクエストごとに自動的にスコープが作成される
  • 同一リクエスト内のコントローラー・サービス・リポジトリ間で同じインスタンスを共有できる
  • Entity Framework Core の DbContext はデフォルトで Scoped として登録される

適切な用途: リクエスト単位で状態を共有するサービス(データベースコンテキスト、リクエストごとのユニットオブワーク、ユーザーコンテキストなど)

Singleton で登録されたサービスは、アプリケーション全体で 1 つのインスタンスのみ が生成されます。

builder.Services.AddSingleton<ICacheService, InMemoryCacheService>();
  • 最初の要求時にインスタンスが生成され、以降はすべてのリクエスト・すべてのスレッドで同じインスタンスが使われる
  • アプリケーション終了時に破棄される
  • スレッドセーフ な実装が必須

適切な用途: アプリケーション全体で共有する設定やキャッシュ、HttpClient ファクトリ、ロガーなど

ライフタイムの動作を確認するサンプル

Section titled “ライフタイムの動作を確認するサンプル”

以下のコードは、各ライフタイムの違いを確認するためのサンプルです。

// 各ライフタイムの動作確認用インターフェイス
public interface IOperationTransient
{
Guid OperationId { get; }
}
public interface IOperationScoped
{
Guid OperationId { get; }
}
public interface IOperationSingleton
{
Guid OperationId { get; }
}
// 実装クラス — コンストラクターで一意の ID を生成
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public Guid OperationId { get; } = Guid.NewGuid();
}
// Program.cs でのサービス登録
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
// ライフタイムの違いを確認するコントローラー
public class LifetimeController(
IOperationTransient transient1,
IOperationTransient transient2,
IOperationScoped scoped1,
IOperationScoped scoped2,
IOperationSingleton singleton1,
IOperationSingleton singleton2) : Controller
{
public IActionResult Index()
{
// Transient: 毎回異なるインスタンス → ID が異なる
ViewData["Transient1"] = transient1.OperationId;
ViewData["Transient2"] = transient2.OperationId;
// Scoped: 同一リクエスト内は同じインスタンス → ID が同じ
ViewData["Scoped1"] = scoped1.OperationId;
ViewData["Scoped2"] = scoped2.OperationId;
// Singleton: アプリケーション全体で同じインスタンス → ID が常に同じ
ViewData["Singleton1"] = singleton1.OperationId;
ViewData["Singleton2"] = singleton2.OperationId;
return View();
}
}

このコントローラーにアクセスすると、以下のような結果が得られます。

サービスID 1ID 2同一?
Transienta1b2c3d4-...e5f6a7b8-...❌ 異なる
Scoped11223344-...11223344-...✅ 同じ
Singletonaabbccdd-...aabbccdd-...✅ 同じ(リクエストをまたいでも同じ)

ASP.NET Core は Development 環境 でスコープバリデーションを既定で有効化しています。
この機能は以下を検証し、違反時に例外をスローします。

  • Scoped サービスがルートサービスプロバイダーから解決されていないこと
  • Scoped サービスが Singleton に注入されていないこと
Program.cs
var builder = WebApplication.CreateBuilder(args);
// ❌ この組み合わせは Development 環境では起動時に失敗する
builder.Services.AddSingleton<MySingletonService>();
builder.Services.AddScoped<MyScopedService>();
var app = builder.Build(); // ← ここで例外がスローされる
app.Run();
public class MyScopedService { }
public class MySingletonService
{
// Scoped サービスを Singleton に注入している
public MySingletonService(MyScopedService scopedService) { }
}

上記を実行すると、builder.Build() の時点で次の例外が発生します。

System.AggregateException: Some services are not able to be constructed
(Error while validating the service descriptor 'ServiceType: MySingletonService
Lifetime: Singleton ImplementationType: MySingletonService':
Cannot consume scoped service 'MyScopedService' from singleton 'MySingletonService'.)

この検証により、ライフタイムの不整合を開発段階で早期発見できます。

なお、この挙動はあくまで既定値であり、UseDefaultServiceProvider で明示的に設定すれば Production 環境でも検証を有効化 できます。

var builder = WebApplication.CreateBuilder(args);
// 環境によらず常にスコープバリデーションを行う
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true; // 解決時にライフタイムの整合性を検証
options.ValidateOnBuild = true; // Build() 時にすべての登録を事前検証
});
オプション役割既定値
ValidateScopesScoped サービスがルートから解決されていないか、Singleton に注入されていないかを検証Development のみ true
ValidateOnBuildBuild() の時点ですべてのサービス登録を事前検証(起動時に失敗させる)Development のみ true

3. ファクトリ登録、条件付き登録、手動スコープ

Section titled “3. ファクトリ登録、条件付き登録、手動スコープ”

ファクトリデリゲートによる登録

Section titled “ファクトリデリゲートによる登録”

サービスのインスタンス生成に追加のロジックが必要な場合、ファクトリデリゲート(ラムダ式)を使用して登録できます。
ファクトリデリゲートは IServiceProvider を引数に取り、他の登録済みサービスを利用してインスタンスを生成できます。

// ファクトリデリゲートで登録(設定値を使う場合)
builder.Services.AddSingleton<INotificationService>(sp =>
{
var configuration = sp.GetRequiredService<IConfiguration>();
var apiKey = configuration["Notification:ApiKey"]
?? throw new InvalidOperationException("Notification:ApiKey is not configured.");
return new SlackNotificationService(apiKey);
});
// 他のサービスに依存するサービスをファクトリで生成
// (OrderService が AppDbContext と ILogger を受け取るコンストラクターを持つ場合)
builder.Services.AddScoped<IOrderService>(sp =>
{
var dbContext = sp.GetRequiredService<AppDbContext>();
var logger = sp.GetRequiredService<ILogger<OrderService>>();
return new OrderService(dbContext, logger);
});

既存インスタンスの登録(Singleton のみ)

Section titled “既存インスタンスの登録(Singleton のみ)”

AddSingleton には、事前に生成したインスタンスをそのまま DI コンテナーに登録するオーバーロードがあります。
このパターンはインターフェイスを介さず具象クラスのインスタンスを直接登録できる、Singleton 固有 の機能です(Scoped / Transient はリクエストや要求ごとに新しいインスタンスを生成するため、この概念が成立しません)。

// 具象クラスのインスタンスをそのまま登録
var settings = new AppSettings { MaxRetries = 3, TimeoutSeconds = 30 };
builder.Services.AddSingleton(settings);
// インターフェイス経由で既存インスタンスを登録
var cache = new InMemoryCacheService();
builder.Services.AddSingleton<ICacheService>(cache);

このパターンは、アプリケーション起動時に構成済みのオブジェクトをそのまま DI コンテナーに渡したい場合に使用します。

キー付きサービス(Keyed Services)

Section titled “キー付きサービス(Keyed Services)”

.NET 8 で導入された キー付きサービス を使うと、同じインターフェイスの複数実装を キー(名前)で区別 して登録・解決できます。

// キャッシュの抽象と 2 つの実装
public interface ICache
{
string? Get(string key);
void Set(string key, string value);
}
public class RedisCache : ICache { /* 分散キャッシュの実装 */ }
public class InMemoryCache : ICache { /* インメモリキャッシュの実装 */ }
// キー付きサービスの登録
builder.Services.AddKeyedSingleton<ICache, RedisCache>("distributed");
builder.Services.AddKeyedSingleton<ICache, InMemoryCache>("local");

キー付きサービスの解決には [FromKeyedServices] 属性を使用します。

// コントローラーのアクションメソッドで解決
[ApiController]
[Route("/cache")]
public class CacheController : ControllerBase
{
[HttpGet("distributed")]
public ActionResult<string> GetDistributed(
[FromKeyedServices("distributed")] ICache cache)
{
var value = cache.Get("key1");
return value is null ? NotFound() : Ok(value);
}
[HttpGet("local")]
public ActionResult<string> GetLocal(
[FromKeyedServices("local")] ICache cache)
{
var value = cache.Get("key1");
return value is null ? NotFound() : Ok(value);
}
}
// Minimal API で解決
app.MapGet("/cache/distributed",
([FromKeyedServices("distributed")] ICache cache) => cache.Get("key1"));
app.MapGet("/cache/local",
([FromKeyedServices("local")] ICache cache) => cache.Get("key1"));
// プライマリコンストラクターで解決
public class OrderProcessor(
[FromKeyedServices("distributed")] ICache distributedCache,
[FromKeyedServices("local")] ICache localCache)
{
public void Process(Order order)
{
// ローカルキャッシュを確認してから分散キャッシュにフォールバック
var cached = localCache.Get(order.Id.ToString())
?? distributedCache.Get(order.Id.ToString());
// ...
}
}

同じインターフェイスに対して複数の実装を登録し、IEnumerable<TService> で一括取得するパターンも利用可能です。

// 複数実装の登録
builder.Services.AddSingleton<INotificationChannel, EmailNotificationChannel>();
builder.Services.AddSingleton<INotificationChannel, SmsNotificationChannel>();
builder.Services.AddSingleton<INotificationChannel, PushNotificationChannel>();
// IEnumerable<T> で全実装を一括注入
public class NotificationService(
IEnumerable<INotificationChannel> channels,
ILogger<NotificationService> logger)
{
public async Task NotifyAllAsync(string message)
{
foreach (var channel in channels)
{
logger.LogInformation("Sending via {Channel}", channel.GetType().Name);
await channel.SendAsync(message);
}
}
}

IServiceScopeFactory による手動スコープ作成

Section titled “IServiceScopeFactory による手動スコープ作成”

BackgroundService(バックグラウンドタスク)のような Singleton ライフタイムで動作するクラスから Scoped サービスを利用する場合は、IServiceScopeFactory を使って明示的にスコープを作成します。

// 未処理の注文をまとめて処理する Scoped サービス
public interface IOrderBatchProcessor
{
Task ProcessPendingOrdersAsync();
}
public sealed class OrderProcessingWorker(
ILogger<OrderProcessingWorker> logger,
IServiceScopeFactory serviceScopeFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// スコープを明示的に作成して Scoped サービスを安全に利用
using (var scope = serviceScopeFactory.CreateScope())
{
var processor = scope.ServiceProvider
.GetRequiredService<IOrderBatchProcessor>();
await processor.ProcessPendingOrdersAsync();
logger.LogInformation("未処理の注文をバッチ処理しました。");
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}

サービス層 は、コントローラー(プレゼンテーション層)とデータアクセス層の間に位置し、ビジネスロジックを集約 する層です。
コントローラーはリクエストの受付とレスポンスの返却に専念し、具体的なビジネスルールの実行はサービス層に委譲します。

flowchart TD
    subgraph Presentation ["プレゼンテーション層"]
        C["Controller / Minimal API Endpoint"]
    end
    subgraph Service ["サービス層"]
        S["IOrderService → OrderService"]
    end
    subgraph DataAccess ["データアクセス層"]
        R["IOrderRepository → OrderRepository"]
        DB[("Database")]
    end
    C --> S --> R --> DB

以下は、インターフェイスと実装で構成される典型的なサービス層の実装例です。

インターフェイス定義

public interface IProductService
{
Task<IReadOnlyList<ProductDto>> GetAllAsync();
Task<ProductDto?> GetByIdAsync(int id);
Task<ProductDto> CreateAsync(CreateProductRequest request);
Task UpdateAsync(int id, UpdateProductRequest request);
Task DeleteAsync(int id);
}

サービス実装

public class ProductService(
AppDbContext dbContext,
ILogger<ProductService> logger) : IProductService
{
public async Task<IReadOnlyList<ProductDto>> GetAllAsync()
{
var products = await dbContext.Products
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.ToListAsync();
return products;
}
public async Task<ProductDto?> GetByIdAsync(int id)
{
var product = await dbContext.Products.FindAsync(id);
return product is null
? null
: new ProductDto(product.Id, product.Name, product.Price);
}
public async Task<ProductDto> CreateAsync(CreateProductRequest request)
{
var product = new Product
{
Name = request.Name,
Price = request.Price
};
dbContext.Products.Add(product);
await dbContext.SaveChangesAsync();
logger.LogInformation("Product created: {ProductId}", product.Id);
return new ProductDto(product.Id, product.Name, product.Price);
}
public async Task UpdateAsync(int id, UpdateProductRequest request)
{
var product = await dbContext.Products.FindAsync(id)
?? throw new KeyNotFoundException($"Product with ID {id} not found.");
product.Name = request.Name;
product.Price = request.Price;
await dbContext.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var product = await dbContext.Products.FindAsync(id)
?? throw new KeyNotFoundException($"Product with ID {id} not found.");
dbContext.Products.Remove(product);
await dbContext.SaveChangesAsync();
}
}

DI コンテナーへの登録と利用

Program.cs
builder.Services.AddScoped<IProductService, ProductService>();
// コントローラーでの利用
[ApiController]
[Route("api/[controller]")]
public class ProductsController(IProductService productService) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<IReadOnlyList<ProductDto>>> GetAll()
{
var products = await productService.GetAllAsync();
return Ok(products);
}
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetById(int id)
{
var product = await productService.GetByIdAsync(id);
return product is null ? NotFound() : Ok(product);
}
[HttpPost]
public async Task<ActionResult<ProductDto>> Create(CreateProductRequest request)
{
var product = await productService.CreateAsync(request);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}

リポジトリパターンとの組み合わせ

Section titled “リポジトリパターンとの組み合わせ”

規模が大きいアプリケーションでは、サービス層とデータアクセス層の間に リポジトリ層 を挟むことで、関心をさらに分離できます。

// リポジトリインターフェイス
public interface IProductRepository
{
Task<IReadOnlyList<Product>> GetAllAsync();
Task<Product?> GetByIdAsync(int id);
Task AddAsync(Product product);
Task UpdateAsync(Product product);
Task DeleteAsync(Product product);
}
// リポジトリ実装
public class ProductRepository(AppDbContext dbContext) : IProductRepository
{
public async Task<IReadOnlyList<Product>> GetAllAsync()
=> await dbContext.Products.ToListAsync();
public async Task<Product?> GetByIdAsync(int id)
=> await dbContext.Products.FindAsync(id);
public async Task AddAsync(Product product)
{
dbContext.Products.Add(product);
await dbContext.SaveChangesAsync();
}
public async Task UpdateAsync(Product product)
{
dbContext.Products.Update(product);
await dbContext.SaveChangesAsync();
}
public async Task DeleteAsync(Product product)
{
dbContext.Products.Remove(product);
await dbContext.SaveChangesAsync();
}
}
// サービスはリポジトリに依存する
public class ProductService(
IProductRepository repository,
ILogger<ProductService> logger) : IProductService
{
public async Task<IReadOnlyList<ProductDto>> GetAllAsync()
{
var products = await repository.GetAllAsync();
return products.Select(p => new ProductDto(p.Id, p.Name, p.Price)).ToList();
}
// ...(以下省略)
}
// Program.cs — リポジトリとサービスの両方を登録
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IProductService, ProductService>();

拡張メソッドによるサービス登録のグルーピング

Section titled “拡張メソッドによるサービス登録のグルーピング”

ASP.NET Core のフレームワーク規約として、関連するサービス群を Add{GroupName} という拡張メソッドにまとめて登録するパターンが推奨されています。
例えば AddControllers()AddDbContext() がこのパターンの実例です。
アプリケーション固有のサービスも同様にグルーピングできます。

ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(
this IServiceCollection services)
{
services.AddScoped<IProductService, ProductService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
return services;
}
public static IServiceCollection AddInfrastructureServices(
this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection")));
services.AddSingleton<ICacheService, RedisCacheService>();
return services;
}
}
// Program.cs — 拡張メソッドによる簡潔な登録
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationServices();
builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddControllers();
var app = builder.Build();

この方式により、Program.cs を簡潔に保ちつつ、各レイヤーのサービス登録を論理的にグルーピングできます。

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

Section titled “ライフタイム選択のガイドライン”

サービスの登録時にどのライフタイムを選択すべきかは、サービスの性質と利用パターンによって判断します。

flowchart TD
    Start(["サービスのライフタイムを選択"]) --> Q1{"状態を持つ?"}
    Q1 -->|"いいえ(ステートレス)"| Q2{"生成コストが高い?"}
    Q1 -->|"はい(ステートフル)"| Q3{"状態のスコープは?"}
    Q2 -->|"いいえ"| Transient["Transient\n(軽量・ステートレス)"]
    Q2 -->|"はい"| Singleton_Q["スレッドセーフ?"]
    Singleton_Q -->|"はい"| Singleton["Singleton\n(高コスト・スレッドセーフ)"]
    Singleton_Q -->|"いいえ"| Scoped2["Scoped\n(高コスト・非スレッドセーフ)"]
    Q3 -->|"リクエスト単位"| Scoped["Scoped\n(リクエスト単位の状態)"]
    Q3 -->|"アプリ全体"| Singleton2["Singleton\n(アプリ全体で共有)"]
分類ライフタイム代表的なサービス
データアクセスScopedDbContext、リポジトリ
ビジネスロジックScopedアプリケーションサービス
キャッシュSingletonインメモリキャッシュ、分散キャッシュクライアント
HTTP クライアントTransient(IHttpClientFactory 経由)外部 API クライアント
ロギングSingletonILogger<T>(フレームワーク登録済み)
バリデーションTransientバリデータ、フォーマッタ