In modernen .NET-Anwendungen ist Dependency Injection längst ein etablierter Standard. Die meisten Entwickler greifen dabei automatisch zu einem Container wie Microsoft.Extensions.DependencyInjection
. Das ist praktisch, gerade in größeren Applikationen oder wenn Middleware und Logging bereits über DI abstrahiert sind. Doch nicht jedes Szenario erfordert ein vollständiges Framework. In Modulen, kleinen Tools, Bibliotheken oder auch stark gekapselten Services kann ein DI-Container sogar kontraproduktiv sein – sei es aus Gründen der Performance, Komplexität oder Portabilität. In diesen Fällen ist manuelle Dependency Injection oft die bessere, weil klarere Wahl.
Warum ohne Container arbeiten?
Manuelle Dependency Injection bringt Vorteile, die in Container-getriebenen Projekten oft übersehen werden. Die Konstruktion der Objekte erfolgt explizit, wodurch der Aufbau der Anwendung jederzeit nachvollziehbar bleibt – auch ohne zusätzliche Tools oder Debugging. Vor allem für Bibliotheken, die eigenständig und unabhängig bleiben sollen, ist dieser Ansatz ideal. Wer beispielsweise ein Modul für Bildverarbeitung, Logging oder Persistenz entwickelt, möchte dessen Abhängigkeiten nicht per Konvention oder Registrierung konfigurieren müssen. Stattdessen werden diese beim Instanziieren direkt mitgegeben. Das verbessert nicht nur die Testbarkeit, sondern verhindert auch eine versteckte Kopplung an externe Containermechanismen.
Services sauber per Konstruktor verdrahten
Das Grundprinzip ist einfach: Jede Abhängigkeit wird explizit über den Konstruktor bereitgestellt. Das folgende Beispiel zeigt eine UserService
-Implementierung, die auf ein simples ILogger
-Interface zugreift:
public interface ILogger { void Log(string message); } public class ConsoleLogger : ILogger { public void Log(string message) => Console.WriteLine($"[LOG] {message}"); } public interface IUserService { void CreateUser(string name); } public class UserService : IUserService { private readonly ILogger _logger; public UserService(ILogger logger) { _logger = logger; } public void CreateUser(string name) { _logger.Log($"User {name} created"); } }
In der Anwendung oder im Main-Einstiegspunkt wird der Service einfach mit der gewünschten Implementierung erstellt. Es gibt keine Registrierung, keine Lebenszyklusverwaltung und keine Konventionen – der Entwickler behält jederzeit die Kontrolle darüber, welche Objekte wie und wann erzeugt werden.
var logger = new ConsoleLogger(); var userService = new UserService(logger); userService.CreateUser("Max");
Strukturierung über manuelle Factory oder ServiceLocator
In Szenarien mit mehreren Komponenten kann es sinnvoll sein, eine zentrale Klasse bereitzustellen, die für die Erzeugung und Wiederverwendung von Abhängigkeiten zuständig ist. Diese fungiert als Mini-DI-Container, allerdings ohne Reflection oder Registrierung. So lässt sich z. B. ein Logger als Singleton erzeugen, während Services, die diesen benötigen, bei Bedarf instanziiert werden:
public class ServiceProvider { private readonly ILogger _logger = new ConsoleLogger(); public IUserService CreateUserService() => new UserService(_logger); }
Diese Form der manuellen Verdrahtung eignet sich besonders für interne Bibliotheken oder für Szenarien, in denen der Overhead eines vollständigen Containers nicht gerechtfertigt ist. Auch das Verhalten zur Laufzeit bleibt transparent: Der Entwickler weiß immer, wann was erzeugt wird – ohne auf Logging im Container angewiesen zu sein.
Tests profitieren von expliziten Abhängigkeiten
Manuelle Konstruktion hat einen weiteren Vorteil: Unit Tests werden einfacher. Es ist kein Setup für Container oder Registrierung notwendig, die Tests übergeben ihre Abhängigkeiten direkt und können beliebige Mocks oder Fakes einsetzen. Die Schnittstelle zwischen Testcode und Produktionscode bleibt stabil, verständlich und kontrollierbar.
public class FakeLogger : ILogger { public List<string> Logs = new(); public void Log(string message) => Logs.Add(message); } [Fact] public void CreateUser_Should_Log_Correct_Message() { var logger = new FakeLogger(); var service = new UserService(logger); service.CreateUser("Lisa"); Assert.Contains("User Lisa created", logger.Logs); }
Auch wenn ich in vielen Modulen auf manuelle Konstruktion setze, verzichte ich im Anwendungsrahmen selten auf den Komfort eines DI-Containers. In größeren Systemen, wo hunderte Services, Middleware-Komponenten oder Konfigurationsklassen zusammenspielen, ist ein Framework wie Microsoft.Extensions.DependencyInjection
unverzichtbar. Es automatisiert Konventionen, reduziert Boilerplate und ermöglicht zentrale Steuerung über Scopes, Lebenszyklen und Schnittstellen-Registrierung. Wichtig ist nur: Die Module selbst sollten unabhängig davon funktionieren – mit explizitem Konstruktor-Contract und ohne magische Anforderungen an die Umgebung.