C#のDIとは?依存性注入の基本から使い方・メリットまで初心者向けに解説
はじめに
C#でアプリケーション開発をしていると、「DI」「依存性注入」「Dependency Injection」という言葉を目にすることがあります。特にASP.NET Coreを使ったWebアプリ開発では、ControllerやServiceに必要なクラスをコンストラクタで受け取る書き方がよく登場します。
DIは最初こそ難しく見えますが、考え方はシンプルです。ひと言でいえば、「クラスの中で必要なものを自分で作るのではなく、外から渡してもらう設計方法」です。
この記事では、C#のDIについて、依存性注入の基本、メリット、具体的なコード例、ASP.NET Coreでの使い方、ライフタイム、テストとの関係まで初心者向けに解説します。
1. C#のDIとは?依存性注入の意味を初心者向けに解説
1-1. DI(Dependency Injection/依存性注入)とは何か
DIとは、Dependency Injectionの略で、日本語では「依存性注入」と呼ばれます。
あるクラスが別のクラスを必要とするとき、その必要なオブジェクトをクラス内部で直接生成するのではなく、外部から渡してもらう設計パターンです。
たとえば、注文処理を行うOrderServiceがメール送信機能を必要とする場合を考えます。DIを使わない場合、OrderServiceの中でEmailSenderをnewします。一方、DIを使う場合は、OrderServiceの外側からEmailSenderを渡します。
C#public class OrderService
{
private readonly EmailSender _emailSender;
public OrderService(EmailSender emailSender)
{
_emailSender = emailSender;
}
}
このように、必要な依存先を外部から受け取るのがDIです。.NETではDIはフレームワークに組み込まれた設計パターンとして扱われ、クラスと依存関係の間でIoCを実現する手法とされています。
1-2. 「依存」とは何を指すのか
プログラミングにおける「依存」とは、あるクラスが別のクラスや機能を使わないと動作できない状態を指します。
たとえば、次のような関係です。
C#public class OrderService
{
private readonly EmailSender _emailSender = new EmailSender();
public void CompleteOrder()
{
_emailSender.Send("注文が完了しました");
}
}
この場合、OrderServiceはEmailSenderがないと注文完了メールを送れません。つまり、OrderServiceはEmailSenderに依存しています。
依存そのものが悪いわけではありません。問題になるのは、依存先をクラス内部で固定してしまい、差し替えやテストが難しくなることです。
1-3. なぜC#開発でDIが重要視されるのか
C#では、Webアプリ、API、バッチ処理、デスクトップアプリなど、さまざまな種類のアプリケーションを開発できます。アプリが大きくなるほど、クラス同士の関係は複雑になります。
DIを使うと、クラス同士の結びつきを弱くできます。その結果、次のような利点があります。
コードの変更に強くなる、単体テストがしやすくなる、クラスの責務が明確になる、機能の差し替えがしやすくなる、チーム開発で設計を共有しやすくなる、といったメリットがあります。
ASP.NET CoreではDIコンテナが標準で用意されており、サービスを登録しておくことで、ControllerやServiceに必要な依存関係を自動的に渡せます。
1-4. DIを使わないコードで起こりやすい問題
DIを使わないコードでは、クラスの中で依存先を直接newすることが多くなります。
C#public class OrderService
{
private readonly EmailSender _emailSender = new EmailSender();
public void CompleteOrder()
{
_emailSender.Send("注文が完了しました");
}
}
このコードは一見シンプルですが、次のような問題があります。
EmailSenderをSmsSenderに変更したい場合、OrderServiceを修正する必要があります。また、単体テストでメールを実際に送らないようにしたい場合でも、EmailSenderが内部で固定されているため差し替えが難しくなります。
さらに、依存先のクラスが増えるほど、OrderServiceの中に生成処理が増え、本来の注文処理とは関係ないコードが混ざってしまいます。
MicrosoftのDIドキュメントでも、ハードコードされた依存関係は、実装の差し替え、設定の分散、単体テストの難しさにつながる問題として説明されています。
2. DIを理解する前に押さえたい基本概念
2-1. 依存関係とは
依存関係とは、「ある処理を実行するために、別のクラスや機能を必要としている関係」のことです。
たとえば、注文処理では次のような依存関係が考えられます。
OrderServiceは注文情報を保存するためにOrderRepositoryを必要とします。注文完了メールを送るためにEmailSenderを必要とします。ログを出力するためにILoggerを必要とします。
このように、実際のアプリケーションでは多くのクラスが互いに依存しています。DIは、この依存関係を整理して管理しやすくするための方法です。
2-2. 疎結合と密結合の違い
DIを理解するうえで重要なのが、「疎結合」と「密結合」です。
密結合とは、クラス同士が強く結びついている状態です。あるクラスが特定の実装クラスを直接newしている場合、その実装に強く依存しています。
C#public class ReportService
{
private readonly CsvExporter _exporter = new CsvExporter();
}
このコードでは、ReportServiceはCsvExporterに強く依存しています。将来、PDF出力に変えたい場合はReportServiceを修正する必要があります。
一方、疎結合とは、クラス同士の結びつきが弱い状態です。
C#public class ReportService
{
private readonly IExporter _exporter;
public ReportService(IExporter exporter)
{
_exporter = exporter;
}
}
このコードでは、ReportServiceは具体的なCsvExporterではなく、IExporterという抽象に依存しています。そのため、CSV出力でもPDF出力でも差し替えやすくなります。
2-3. インターフェースを使う理由
DIでは、インターフェースがよく使われます。インターフェースを使う理由は、実装クラスを直接指定せず、役割だけを定義できるからです。
C#public interface INotificationSender
{
void Send(string message);
}
このインターフェースを使えば、メール送信、SMS送信、Slack通知など、さまざまな実装を同じ形で扱えます。
C#public class EmailNotificationSender : INotificationSender
{
public void Send(string message)
{
Console.WriteLine($"メール送信: {message}");
}
}
public class SmsNotificationSender : INotificationSender
{
public void Send(string message)
{
Console.WriteLine($"SMS送信: {message}");
}
}
利用側のクラスは、具体的な通知方法を知る必要がありません。
C#public class OrderService
{
private readonly INotificationSender _notificationSender;
public OrderService(INotificationSender notificationSender)
{
_notificationSender = notificationSender;
}
public void CompleteOrder()
{
_notificationSender.Send("注文が完了しました");
}
}
これにより、通知方法を変更してもOrderServiceのコードを変更せずに済みます。
2-4. IoC(制御の反転)とDIの関係
IoCはInversion of Controlの略で、日本語では「制御の反転」と呼ばれます。
通常、クラスが必要なオブジェクトを自分で作る場合、制御はそのクラス自身にあります。しかしDIでは、必要なオブジェクトを外部から渡してもらいます。つまり、「依存先を作る責任」がクラスの外側に移動します。
これが制御の反転です。
DIは、IoCを実現する代表的な手段のひとつです。IoCは考え方であり、DIはその具体的な実装方法と考えると理解しやすいです。
2-5. DIコンテナとは何か
DIコンテナとは、依存関係を登録し、必要なタイミングでオブジェクトを生成して渡してくれる仕組みです。
たとえば、次のように登録しておくとします。
C#builder.Services.AddScoped<INotificationSender, EmailNotificationSender>();
この登録により、「INotificationSenderが必要な場所にはEmailNotificationSenderを渡す」というルールをDIコンテナに教えられます。
.NETでは、IServiceCollectionにサービスを登録し、IServiceProviderが登録されたサービスを解決する仕組みが提供されています。
3. C#でDIを使うメリット
3-1. コードの変更に強くなる
DIを使うと、実装を差し替えやすくなります。
たとえば、メール通知からSMS通知へ変更したい場合でも、利用側のOrderServiceは変更せず、DIコンテナへの登録だけを変更できます。
C#builder.Services.AddScoped<INotificationSender, EmailNotificationSender>();
これを次のように変更します。
C#builder.Services.AddScoped<INotificationSender, SmsNotificationSender>();
OrderServiceはINotificationSenderに依存しているため、具体的な通知方法が変わっても影響を受けにくくなります。
3-2. テストしやすい設計になる
DIを使うと、テスト時に本物の依存先ではなく、テスト用の依存先を渡せます。
たとえば、本番ではメールを送信し、テストではメールを送信せずに「呼び出されたかどうか」だけを確認できます。
C#public class FakeNotificationSender : INotificationSender
{
public bool WasCalled { get; private set; }
public void Send(string message)
{
WasCalled = true;
}
}
このように依存先を差し替えられるため、外部サービスやデータベースに依存しない単体テストを書きやすくなります。
3-3. クラスの責務が分かりやすくなる
DIを使うと、クラスの役割が明確になります。
たとえば、OrderServiceは注文処理に集中し、通知処理はINotificationSenderに任せます。注文処理のクラスがメールの作り方や送信方法まで知っていると、責務が増えすぎてしまいます。
DIを使うことで、「このクラスは何をするクラスなのか」が分かりやすくなり、コードの見通しがよくなります。
3-4. 再利用性・保守性が高まる
DIを使ってクラスの依存関係を外部から渡すようにしておくと、同じクラスを別の場面でも使いやすくなります。
たとえば、ReportServiceがIExporterに依存していれば、CSV出力、PDF出力、Excel出力などを切り替えて再利用できます。
保守の面でも、修正範囲を小さくしやすくなります。具体的な実装が変わっても、インターフェースが変わらなければ利用側のコードを大きく変更する必要がありません。
3-5. 大規模開発やチーム開発で管理しやすくなる
大規模開発では、クラス数が増え、依存関係も複雑になります。DIを使うと、依存関係の登録場所を集約できるため、どのインターフェースにどの実装が使われているかを管理しやすくなります。
また、チーム開発では、インターフェースを先に決めておくことで、実装担当と利用側の担当が並行して作業しやすくなります。
DIは、単なる便利機能ではなく、設計を整理するための考え方でもあります。
4. C#におけるDIの基本的な使い方
4-1. DIを使わない実装例
まずはDIを使わない例を見てみます。
C#public class EmailSender
{
public void Send(string message)
{
Console.WriteLine($"メール送信: {message}");
}
}
public class OrderService
{
private readonly EmailSender _emailSender = new EmailSender();
public void CompleteOrder()
{
Console.WriteLine("注文処理を完了しました");
_emailSender.Send("注文が完了しました");
}
}
このコードでは、OrderServiceがEmailSenderを直接生成しています。小さなコードでは問題に見えませんが、通知方法を変更したい場合やテストしたい場合に困ります。
4-2. コンストラクタインジェクションの実装例
DIでよく使われるのが、コンストラクタインジェクションです。これは、必要な依存先をコンストラクタの引数として受け取る方法です。
C#public class OrderService
{
private readonly EmailSender _emailSender;
public OrderService(EmailSender emailSender)
{
_emailSender = emailSender;
}
public void CompleteOrder()
{
Console.WriteLine("注文処理を完了しました");
_emailSender.Send("注文が完了しました");
}
}
このようにすると、OrderServiceはEmailSenderを自分で作らず、外部から受け取る形になります。
4-3. インターフェースを使った依存性注入
より実践的には、具体的なクラスではなくインターフェースに依存させます。
C#public interface INotificationSender
{
void Send(string message);
}
public class EmailNotificationSender : INotificationSender
{
public void Send(string message)
{
Console.WriteLine($"メール送信: {message}");
}
}
public class OrderService
{
private readonly INotificationSender _notificationSender;
public OrderService(INotificationSender notificationSender)
{
_notificationSender = notificationSender;
}
public void CompleteOrder()
{
Console.WriteLine("注文処理を完了しました");
_notificationSender.Send("注文が完了しました");
}
}
この形にすると、OrderServiceは通知方法の詳細を知りません。INotificationSenderという役割だけを知っています。
4-4. サービスクラスを差し替える方法
通知方法をメールからSMSに変更したい場合は、新しい実装クラスを作ります。
C#public class SmsNotificationSender : INotificationSender
{
public void Send(string message)
{
Console.WriteLine($"SMS送信: {message}");
}
}
ASP.NET Coreでは、Program.csで登録する実装を変更します。
C#builder.Services.AddScoped<INotificationSender, SmsNotificationSender>();
これで、INotificationSenderが必要な場所にはSmsNotificationSenderが渡されます。利用側のOrderServiceは変更不要です。
4-5. 初心者が理解しやすいDIのコード例
DIの考え方をシンプルに表すと、次のようになります。
C#public interface IMessageWriter
{
void Write(string message);
}
public class ConsoleMessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine(message);
}
}
public class GreetingService
{
private readonly IMessageWriter _messageWriter;
public GreetingService(IMessageWriter messageWriter)
{
_messageWriter = messageWriter;
}
public void SayHello()
{
_messageWriter.Write("こんにちは");
}
}
GreetingServiceは、メッセージをどこに出力するかを知りません。コンソールに出すのか、ファイルに書くのか、ログに出すのかは、外部から渡されるIMessageWriterの実装によって決まります。
これがDIの基本です。
5. DIの主な種類
5-1. コンストラクタインジェクション
コンストラクタインジェクションは、依存関係をコンストラクタで受け取る方法です。
C#public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
}
C#やASP.NET Coreでは、この方法が最もよく使われます。必要な依存関係がコンストラクタに明示されるため、クラスを使うために何が必要なのかが分かりやすくなります。
5-2. プロパティインジェクション
プロパティインジェクションは、依存関係をプロパティ経由で渡す方法です。
C#public class UserService
{
public IUserRepository? UserRepository { get; set; }
}
必須ではない依存関係を後から設定したい場合に使われることがあります。ただし、プロパティが設定されていない状態でもオブジェクトを作れてしまうため、実行時にnullによるエラーが起きる可能性があります。
5-3. メソッドインジェクション
メソッドインジェクションは、特定のメソッドを実行するときだけ必要な依存関係を引数で渡す方法です。
C#public class ReportService
{
public void Export(IExporter exporter)
{
exporter.Export();
}
}
クラス全体ではなく、特定の処理だけで依存先が必要な場合に使えます。
5-4. C#でよく使われるDIの種類
C#で最もよく使われるのは、コンストラクタインジェクションです。
理由は、依存関係が明示的で、必須の依存先を確実に渡せるからです。ASP.NET CoreのControllerやServiceでも、コンストラクタを使って依存関係を受け取る書き方が一般的です。
C#public class ProductsController : Controller
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
}
5-5. 初心者にはコンストラクタインジェクションがおすすめな理由
初心者には、まずコンストラクタインジェクションを覚えるのがおすすめです。
コンストラクタを見れば、そのクラスが何に依存しているかすぐに分かります。また、必要な依存関係が渡されていない状態でインスタンスを作りにくいため、設計ミスに気づきやすくなります。
プロパティインジェクションやメソッドインジェクションもありますが、最初は「DIはコンストラクタで受け取るもの」と理解しておくと実務でも困りにくいです。
6. ASP.NET CoreでのDIの使い方
6-1. ASP.NET Coreに標準搭載されているDIコンテナ
ASP.NET CoreにはDIコンテナが標準で搭載されています。そのため、外部ライブラリを追加しなくてもDIを使えます。
DIコンテナは、登録されたサービスを管理し、必要なクラスに自動的に依存関係を渡します。
たとえば、ControllerがIProductServiceを必要としている場合、DIコンテナが登録内容をもとにProductServiceを生成して渡してくれます。
6-2. Program.csでサービスを登録する方法
ASP.NET Coreでは、主にProgram.csでサービスを登録します。
C#var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
var app = builder.Build();
app.MapControllers();
app.Run();
この例では、IProductServiceが必要な場所にはProductServiceを、IProductRepositoryが必要な場所にはProductRepositoryを渡すように登録しています。
.NET 6以降のASP.NET Coreでは、通常Program.csでサービスを登録します。Microsoftのドキュメントでも、.NET 6以降ではProgramファイル、.NET 5以前ではStartup.ConfigureServicesでサービスを登録する例が説明されています。
6-3. AddTransient・AddScoped・AddSingletonの違い
ASP.NET Coreでサービスを登録するときによく使うのが、AddTransient、AddScoped、AddSingletonです。
C#builder.Services.AddTransient<IExampleService, ExampleService>();
builder.Services.AddScoped<IExampleService, ExampleService>();
builder.Services.AddSingleton<IExampleService, ExampleService>();
AddTransientは、必要になるたびに新しいインスタンスを作ります。軽量で状態を持たないサービスに向いています。
AddScopedは、スコープごとに同じインスタンスを使います。Webアプリでは、通常1回のHTTPリクエスト内で同じインスタンスが使われます。
AddSingletonは、アプリケーション全体で1つのインスタンスを使い回します。設定情報や共有して問題ないサービスに向いています。
ASP.NET Coreの公式ドキュメントでも、AddTransient、AddScoped、AddSingletonを使ってサービスのライフタイムを登録する例が示されています。
6-4. ControllerやServiceで依存関係を受け取る方法
Controllerでは、コンストラクタで依存関係を受け取ります。
C#public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
public IEnumerable<Product> Get()
{
return _productService.GetProducts();
}
}
Serviceでも同じように、必要なRepositoryなどをコンストラクタで受け取ります。
C#public class ProductService : IProductService
{
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public IEnumerable<Product> GetProducts()
{
return _productRepository.FindAll();
}
}
これにより、Controllerはビジネスロジックを直接持たず、Serviceに処理を任せられます。Serviceはデータアクセスの詳細をRepositoryに任せられます。
6-5. Webアプリ開発でよくあるDIの利用例
ASP.NET CoreのWebアプリ開発では、次のような場面でDIがよく使われます。
データベースアクセスを行うRepositoryをServiceに注入する、ビジネスロジックを行うServiceをControllerに注入する、ログ出力用のILogger<T>を注入する、設定情報を扱うOptionsを注入する、外部APIクライアントを注入する、といった使い方です。
たとえば、ログを出力したい場合は次のように書けます。
C#public class ProductService
{
private readonly ILogger<ProductService> _logger;
public ProductService(ILogger<ProductService> logger)
{
_logger = logger;
}
public void Execute()
{
_logger.LogInformation("商品処理を開始しました");
}
}
ASP.NET Coreではログ機能などもDIと組み合わせて使いやすくなっています。
7. DIコンテナのライフタイムを理解する
7-1. Transientとは
Transientは、依存関係が要求されるたびに新しいインスタンスを作成するライフタイムです。
C#builder.Services.AddTransient<IMessageService, MessageService>();
状態を持たない軽量なサービスに向いています。たとえば、単純な文字列加工、計算処理、変換処理などのサービスです。
ただし、生成コストが高いクラスをTransientにすると、何度もインスタンスが作られてパフォーマンスに影響する場合があります。
7-2. Scopedとは
Scopedは、1つのスコープ内で同じインスタンスを使うライフタイムです。ASP.NET CoreのMVCやRazor Pagesでは、通常1回のHTTPリクエストが1つのスコープになります。
C#builder.Services.AddScoped<IOrderService, OrderService>();
Webアプリでは、ServiceやRepository、Entity Framework CoreのDbContextなどでよく使われます。
同じリクエスト内では同じインスタンスを使いたいが、リクエストが変われば別のインスタンスにしたい場合に適しています。
7-3. Singletonとは
Singletonは、アプリケーション全体で1つのインスタンスだけを作り、それを使い回すライフタイムです。
C#builder.Services.AddSingleton<IClock, SystemClock>();
設定値を参照するサービス、アプリ全体で共有してよいキャッシュ、状態を持たないユーティリティ的なサービスなどに使われます。
ただし、Singletonはアプリ全体で共有されるため、変更可能な状態を持たせる場合は注意が必要です。複数リクエストから同時にアクセスされる可能性があるため、スレッドセーフな設計が必要になります。
7-4. ライフタイムの使い分け方
初心者は、まず次の基準で考えると分かりやすいです。
Webリクエストごとに状態を分けたいサービスはScopedにします。毎回新しく作って問題ない軽量なサービスはTransientにします。アプリ全体で共有してよいサービスはSingletonにします。
ASP.NET Coreの一般的な業務アプリでは、ServiceやRepositoryはScopedで登録されることが多いです。まずはScopedを基本にし、明確な理由がある場合にTransientやSingletonを選ぶとよいでしょう。
7-5. ライフタイム設定で起こりやすいミス
よくあるミスは、SingletonのサービスからScopedのサービスを直接使ってしまうことです。
たとえば、Singletonとして登録されたサービスが、リクエストごとに作られるDbContextを保持してしまうと、意図しない共有やエラーの原因になります。
Microsoftのドキュメントでも、ScopedサービスをSingletonに注入すると問題になるため、開発環境ではスコープ検証によって検出されることが説明されています。
ライフタイムはDIの中でもつまずきやすい部分です。エラーが出た場合は、「どのサービスをどのライフタイムで登録したか」を確認しましょう。
8. DIを使うとテストがしやすくなる理由
8-1. モックとは何か
モックとは、テスト用に用意する偽物のオブジェクトのことです。
たとえば、メール送信処理をテストしたい場合、本当にメールを送ってしまうと困ります。そこで、実際にはメールを送らない偽物のクラスを使います。
C#public class MockNotificationSender : INotificationSender
{
public string? SentMessage { get; private set; }
public void Send(string message)
{
SentMessage = message;
}
}
このようなモックを使うと、「メール送信メソッドが呼ばれたか」「どのメッセージが渡されたか」を確認できます。
8-2. DIによって依存先を差し替えられる仕組み
DIを使うと、クラスの依存先を外から渡せるため、本番用の実装とテスト用の実装を簡単に差し替えられます。
本番では次のようにメール送信用の実装を使います。
C#var service = new OrderService(new EmailNotificationSender());
テストでは次のようにモックを渡します。
C#var mockSender = new MockNotificationSender();
var service = new OrderService(mockSender);
このように、OrderServiceのコードを変更せずに依存先だけを変えられる点がDIの大きな強みです。
8-3. 単体テストでDIを活用する例
次のようなOrderServiceがあるとします。
C#public class OrderService
{
private readonly INotificationSender _notificationSender;
public OrderService(INotificationSender notificationSender)
{
_notificationSender = notificationSender;
}
public void CompleteOrder()
{
_notificationSender.Send("注文が完了しました");
}
}
テスト用のクラスを用意します。
C#public class FakeNotificationSender : INotificationSender
{
public bool WasSent { get; private set; }
public void Send(string message)
{
WasSent = true;
}
}
テストでは、次のように確認できます。
C#var fakeSender = new FakeNotificationSender();
var orderService = new OrderService(fakeSender);
orderService.CompleteOrder();
Console.WriteLine(fakeSender.WasSent); // True
本物のメール送信処理を使わなくても、注文完了時に通知処理が呼ばれたことを確認できます。
8-4. テストしにくいコードとテストしやすいコードの違い
テストしにくいコードは、依存先を内部で直接生成しています。
C#public class OrderService
{
private readonly EmailNotificationSender _sender = new EmailNotificationSender();
}
この場合、テスト時にEmailNotificationSenderを差し替えるのが難しくなります。
一方、テストしやすいコードは、依存先を外部から受け取ります。
C#public class OrderService
{
private readonly INotificationSender _sender;
public OrderService(INotificationSender sender)
{
_sender = sender;
}
}
この設計なら、本番では本物の実装を、テストでは偽物の実装を渡せます。DIは、単体テストしやすい設計を作るうえで非常に重要です。
9. C#のDIで初心者がつまずきやすいポイント
9-1. DIとnewの使い分けが分からない
DIを学び始めると、「すべてのクラスをDIで受け取るべきなのか」と悩むことがあります。
基本的には、アプリケーションのサービス、Repository、外部APIクライアント、ログ、設定、データベース関連など、差し替えや管理が必要なものはDIに向いています。
一方、一時的な値オブジェクトや単純なデータクラスは、普通にnewして問題ありません。
C#var user = new User("Taro", "taro@example.com");
DIは便利ですが、すべてのnewをなくすためのものではありません。
9-2. インターフェースをなぜ作るのか分からない
初心者がつまずきやすいのが、「なぜわざわざインターフェースを作るのか」という点です。
インターフェースは、実装を差し替えやすくするために使います。将来、メール送信をSMS送信に変えたい、テスト用の偽物に差し替えたい、外部APIの実装を変更したい、といった場合に役立ちます。
ただし、何でもインターフェースにすればよいわけではありません。実装が1つしかなく、差し替える予定もなく、テストでも困らない場合は、具体クラスをそのままDI登録する選択もあります。
9-3. サービス登録を忘れてエラーになる
ASP.NET Coreでよくあるのが、サービス登録を忘れるミスです。
たとえば、ControllerでIProductServiceを受け取っているのに、Program.csで登録していないとエラーになります。
C#public ProductsController(IProductService productService)
{
_productService = productService;
}
この場合は、Program.csに登録を追加します。
C#builder.Services.AddScoped<IProductService, ProductService>();
DIでエラーが出たら、まず「インターフェースと実装クラスを登録しているか」を確認しましょう。
9-4. ライフタイムの違いを理解せずに使ってしまう
AddTransient、AddScoped、AddSingletonの違いを理解せずに使うと、意図しない挙動になることがあります。
特に注意したいのはSingletonです。Singletonはアプリ全体で1つのインスタンスを共有するため、リクエストごとに変わる情報やユーザーごとの状態を持たせるのには向いていません。
迷った場合は、WebアプリのServiceやRepositoryにはまずScopedを検討すると理解しやすいです。
9-5. DIを使いすぎて設計が複雑になる
DIは便利ですが、使いすぎると逆に設計が複雑になることがあります。
たとえば、1つのクラスのコンストラクタに依存関係が10個もある場合、そのクラスは多くの責務を持ちすぎている可能性があります。
C#public class LargeService
{
public LargeService(
IServiceA serviceA,
IServiceB serviceB,
IServiceC serviceC,
IServiceD serviceD,
IServiceE serviceE)
{
}
}
MicrosoftのDIガイドラインでも、依存関係が多いクラスは責務が多すぎる可能性があり、単一責任の原則に反しているサインとして説明されています。
DIで複雑になったと感じたら、クラスを分割できないか見直しましょう。
10. DIを使うべきケース・使わなくてもよいケース
10-1. DIを使うべきケース
DIを使うべきなのは、依存先を差し替える可能性がある場合です。
たとえば、メール送信、SMS送信、外部API連携、データベースアクセス、ファイル操作、ログ出力、設定情報の参照などはDIに向いています。
また、単体テストで依存先をモックに差し替えたい場合もDIを使うべきです。
業務アプリケーションでは、Controller、Service、Repositoryのような構成にして、ServiceやRepositoryをDIで管理することが多いです。
10-2. DIを使わなくてもよいケース
DIを使わなくてもよいケースもあります。
たとえば、単純なデータを表すクラス、値オブジェクト、一時的に作るだけの小さなオブジェクトなどは、普通にnewして問題ありません。
C#var price = new Money(1000, "JPY");
var address = new Address("Tokyo", "Shibuya");
このようなクラスまで無理にDIコンテナに登録すると、かえってコードが分かりにくくなります。
10-3. 小規模アプリでDIを導入する判断基準
小規模アプリでも、ASP.NET Coreを使っているならDIは自然に使うことになります。ControllerにServiceを注入する程度であれば、設計が大きく複雑になることはありません。
一方、コンソールアプリや学習用の小さなプログラムでは、最初から本格的なDI設計にしなくてもよい場合があります。
判断基準は、「後から実装を差し替えたいか」「テストしやすくしたいか」「依存関係が増えそうか」です。これらに当てはまるなら、早めにDIを取り入れる価値があります。
10-4. DIを無理に使わないための考え方
DIは目的ではなく手段です。目的は、変更しやすく、テストしやすく、保守しやすいコードを書くことです。
そのため、DIを使うことでコードが分かりやすくなるなら使うべきです。逆に、DIを使うことで小さな処理が過度に複雑になるなら、無理に使う必要はありません。
大切なのは、「この依存関係は外から渡した方がよいか」「将来差し替える可能性があるか」「テストで困らないか」を考えることです。
11. C#のDIに関するよくある質問
11-1. DIとDIコンテナの違いは何ですか?
DIは、依存関係を外部から渡す設計パターンです。
DIコンテナは、そのDIを実現するために依存関係の生成や管理を行う仕組みです。
つまり、DIは考え方、DIコンテナはそれを助ける道具と考えると分かりやすいです。
DIコンテナを使わなくても、手動でコンストラクタに依存先を渡せばDIは実現できます。
C#var sender = new EmailNotificationSender();
var service = new OrderService(sender);
ASP.NET Coreでは、この生成と受け渡しをDIコンテナが自動で行ってくれます。
11-2. DIとIoCは同じ意味ですか?
DIとIoCは同じ意味ではありません。
IoCは「制御の反転」という設計上の考え方です。オブジェクトの生成や処理の流れを、自分自身ではなく外部の仕組みに任せる考え方です。
DIは、そのIoCを実現する具体的な方法のひとつです。
つまり、IoCの実現手段のひとつがDIです。
11-3. C#ではDIを必ず使うべきですか?
C#で必ずDIを使わなければならないわけではありません。
小さなプログラムや、差し替えの必要がない単純な処理では、普通にnewしても問題ありません。
ただし、ASP.NET CoreでWebアプリやAPIを作る場合は、DIが標準的に使われます。Service、Repository、Logger、DbContextなどをDIで受け取る設計に慣れておくと、実務で役立ちます。
11-4. staticクラスとDIはどちらを使うべきですか?
staticクラスは手軽に使えますが、テストや差し替えが難しくなる場合があります。
状態を持たない単純なユーティリティメソッドであれば、staticクラスでも問題ないことがあります。
C#public static class StringHelper
{
public static bool IsEmpty(string value)
{
return string.IsNullOrWhiteSpace(value);
}
}
一方、外部API、データベース、ログ、設定、日時取得など、テスト時に差し替えたい処理はDIの方が向いています。
MicrosoftのDIガイドラインでも、状態を持つstaticクラスやグローバル状態を避け、必要に応じてSingletonサービスを使う考え方が示されています。
11-5. ASP.NET Core以外でもDIは使えますか?
はい、ASP.NET Core以外でもDIは使えます。
C#のコンソールアプリ、Worker Service、Windows Forms、WPF、.NET MAUIなどでもDIを利用できます。.NETにはDIを扱うための仕組みが用意されており、Microsoft.Extensions.DependencyInjectionを使ってサービス登録や依存関係の解決ができます。
たとえば、コンソールアプリでも次のようにDIを使えます。
C#using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddScoped<INotificationSender, EmailNotificationSender>();
services.AddScoped<OrderService>();
var provider = services.BuildServiceProvider();
var orderService = provider.GetRequiredService<OrderService>();
orderService.CompleteOrder();
ASP.NET Coreだけでなく、さまざまなC#アプリでDIの考え方は活用できます。
まとめ
C#のDIとは、クラスが必要とする依存関係を内部で直接生成するのではなく、外部から渡す設計方法です。
DIを使うことで、クラス同士の結びつきを弱くでき、コードの変更に強くなります。また、依存先を差し替えやすくなるため、単体テストもしやすくなります。
初心者がまず理解すべきポイントは、依存関係、疎結合、インターフェース、コンストラクタインジェクション、DIコンテナ、ライフタイムです。
ASP.NET CoreではDIコンテナが標準で搭載されており、Program.csでサービスを登録し、ControllerやServiceのコンストラクタで依存関係を受け取る形が一般的です。
最初は難しく感じるかもしれませんが、DIの基本は「必要なものを自分で作らず、外から渡してもらう」ことです。この考え方を押さえるだけでも、C#の設計は大きく分かりやすくなります。

