Einleitung: Was ist Dependency Injection und warum wird es benötigt
Dependency Injection (DI) ist eines der wichtigsten Entwurfsmuster, das das Prinzip der Abhängigkeitsumkehr (Dependency Inversion Principle) aus SOLID umsetzt. In der Praxis bedeutet dies, dass Objekte ihre Abhängigkeiten nicht selbst erstellen, sondern von außen erhalten. Dies macht den Code flexibel, testbar und lose gekoppelt.
Im Ökosystem von C# und .NET ist DI zum De-facto-Standard geworden. Seit .NET Core verfügt die Plattform über einen eigenen Container für die Abhängigkeitsinjektion, der in ASP.NET Core, Hintergrunddiensten und sogar in Konsolenanwendungen verwendet wird. Ohne das Verständnis von DI ist moderne C#-Entwicklung kaum vorstellbar.
In diesem Artikel analysieren wir DI anhand realer Beispiele: von der manuellen Injektion bis zur Nutzung des integrierten .NET-Containers. Sie werden sehen, wie Sie starre Kopplungen vermeiden und den Code für Modultests geeignet machen.
Das Problem ohne Dependency Injection
Stellen wir uns ein typisches Szenario vor: Die Klasse OrderService verarbeitet Bestellungen und sendet E-Mail-Benachrichtigungen. Ohne DI sieht der Code so aus:
public class EmailSender{ public void Send(string to, string subject, string body) { Console.WriteLine($"Sende E-Mail an {to}: {subject}"); }}
public class OrderService{ private EmailSender _emailSender;
public OrderService() { // Starre Kopplung – die Klasse erstellt die Abhängigkeit selbst _emailSender = new EmailSender(); }
public void ProcessOrder(int orderId) { // Logik zur Bestellverarbeitung... _emailSender.Send("user@example.com", "Bestellung aufgegeben", $"Ihre Bestellung #{orderId} wurde angenommen."); }}Wo liegt das Problem? OrderService ist fest an eine bestimmte Implementierung von EmailSender gebunden. Wenn wir den E-Mail-Versand durch SMS oder Protokollierung ersetzen möchten, muss der Code von OrderService geändert werden. Auch das Testen wird erschwert: Es ist unmöglich, den echten EmailSender durch ein Mock-Objekt zu ersetzen.
Lösung: Abhängigkeitsinjektion über den Konstruktor
Der erste Schritt zu DI besteht darin, eine Abstraktion (Schnittstelle) zu definieren und die Abhängigkeit über den Konstruktor zu übergeben. Dies wird als Constructor Injection bezeichnet.
Schritt 1: Erstellen der Schnittstelle
public interface INotificationService{ void Send(string to, string subject, string body);}Schritt 2: Implementieren der Schnittstelle
public class EmailSender : INotificationService{ public void Send(string to, string subject, string body) { Console.WriteLine($"E-Mail gesendet: {subject} -> {to}"); }}
public class SmsSender : INotificationService{ public void Send(string to, string subject, string body) { Console.WriteLine($"SMS gesendet: {subject} -> {to}"); }}Schritt 3: Injizieren der Abhängigkeit über den Konstruktor
public class OrderService{ private readonly INotificationService _notificationService;
// Die Abhängigkeit wird von außen übergeben public OrderService(INotificationService notificationService) { _notificationService = notificationService; }
public void ProcessOrder(int orderId) { // Logik zur Bestellverarbeitung... _notificationService.Send("user@example.com", "Bestellung aufgegeben", $"Ihre Bestellung #{orderId} wurde angenommen."); }}Jetzt weiß OrderService nicht, welche konkrete Implementierung verwendet wird. Wir können EmailSender, SmsSender oder jede andere Klasse einsetzen, die INotificationService implementiert. Der Code ist flexibel und testbar geworden.
Verwendung des integrierten IoC-Containers in .NET
Es ist umständlich, alle Abhängigkeiten manuell zu erstellen. .NET verfügt über einen integrierten Container (IoC-Container), der den Lebenszyklus von Objekten automatisch verwaltet. Betrachten wir ein Beispiel für eine Konsolenanwendung.