Dependency injection is een softwarepatroon waarbij een klasse zijn afhankelijkheden niet zelf aanmaakt, maar ze van buitenaf ontvangt. In C# gebruik je dit door afhankelijkheden via de constructor, een property of een methode door te geven, vaak in combinatie met een DI-container zoals de ingebouwde IServiceCollection in .NET. Het patroon maakt je code beter testbaar, losser gekoppeld en eenvoudiger te onderhouden.
Dependency injection is een van de fundamentele software design patterns in C# en speelt een grote rol in moderne technische softwareontwikkeling. In dit artikel beantwoorden we de meest gestelde vragen over DI in C#, van de basisprincipes tot veelgemaakte fouten.
Hoe werkt dependency injection onder de motorkap?
Dependency injection werkt door het aanmaken van objecten te scheiden van het gebruik ervan. In plaats van dat een klasse zelf een instantie aanmaakt van een andere klasse, delegeert het die verantwoordelijkheid aan een externe partij: de DI-container. Die container beheert de levenscyclus van objecten en injecteert ze waar nodig.
Stel je hebt een klasse MotorController die afhankelijk is van een ILogger. Zonder dependency injection maakt MotorController zelf een Logger aan. Met DI geef je die logger mee via de constructor. De DI-container weet welke concrete implementatie bij de interface hoort en zorgt voor de koppeling op het moment dat het object wordt aangemaakt.
Dit mechanisme werkt via een zogenaamd service registration-systeem. Je registreert welke interface aan welke implementatie is gekoppeld, en de container lost die afhankelijkheden automatisch op wanneer een object wordt aangevraagd. Dit is de kern van hoe dependency injection in C# functioneert.
Wat zijn de drie vormen van dependency injection?
De drie vormen van dependency injection zijn constructor injection, property injection en method injection. Constructor injection is verreweg de meest gebruikte en aanbevolen vorm in C# en .NET-applicaties.
- Constructor injection: Afhankelijkheden worden meegegeven via de constructor. Dit maakt duidelijk welke afhankelijkheden een klasse nodig heeft en zorgt dat een object altijd in een geldige staat is.
- Property injection: Afhankelijkheden worden ingesteld via publieke properties. Dit is handig voor optionele afhankelijkheden, maar maakt het minder duidelijk wat een klasse echt nodig heeft.
- Method injection: Een afhankelijkheid wordt meegegeven als parameter van een specifieke methode. Dit is geschikt als de afhankelijkheid alleen voor die ene methode relevant is.
In de praktijk gebruik je bij technische softwareontwikkeling bijna altijd constructor injection. Het is het meest expliciet, het beste te testen en het best ondersteund door de .NET DI-container.
Wat is het verschil tussen dependency injection en inversion of control?
Inversion of control (IoC) is het bredere principe: de controle over het aanmaken en beheren van objecten wordt omgekeerd, van de klasse zelf naar een externe partij. Dependency injection is een specifieke techniek om dat principe te implementeren. IoC is het concept, DI is een van de manieren om het toe te passen.
Een andere manier om IoC te implementeren is bijvoorbeeld via het Service Locator-patroon, waarbij een klasse zelf een centrale registry raadpleegt om afhankelijkheden op te halen. Dat patroon heeft echter nadelen op het gebied van testbaarheid en transparantie, waardoor dependency injection in C# de voorkeur geniet.
In de context van inversion of control C# geldt: zodra je de .NET DI-container gebruikt, pas je automatisch zowel IoC als DI toe. De container neemt de controle over, en DI is het mechanisme waarmee afhankelijkheden worden doorgegeven.
Hoe gebruik je de ingebouwde DI-container in .NET?
De ingebouwde DI-container in .NET gebruik je via IServiceCollection, die je configureert in de opstartcode van je applicatie. Je registreert services, koppelt interfaces aan implementaties en laat de container de rest afhandelen.
De stappen zijn als volgt:
- Maak een interface aan, bijvoorbeeld
IMotionService. - Maak een implementatie, bijvoorbeeld
MotionService : IMotionService. - Registreer de koppeling in
Program.csofStartup.csviaservices.AddScoped<IMotionService, MotionService>(). - Injecteer de interface via de constructor in de klasse die hem nodig heeft.
- De container lost de afhankelijkheid automatisch op bij het aanmaken van het object.
Dit is het basispatroon dat in vrijwel elke moderne .NET-applicatie wordt toegepast, van webapplicaties tot embedded softwareoplossingen voor machines en apparaten. Voor complexere scenario’s kun je ook externe DI-frameworks zoals Autofac gebruiken of Microsoft.Extensions.DependencyInjection uitbreiden met extra functionaliteit.
Wanneer kies je voor Transient, Scoped of Singleton?
De levensduur van een service in de .NET DI-container bepaal je met Transient, Scoped of Singleton. De keuze hangt af van hoe lang een object in leven moet blijven en of het gedeeld mag worden tussen aanvragen of componenten.
Transient maakt bij elke aanvraag een nieuw object aan. Dit is geschikt voor lichtgewicht, stateless services die geen gedeelde toestand mogen hebben.
Scoped maakt een object eenmalig aan per scope, in een webapplicatie is dat doorgaans per HTTP-request. Dit is de meest gebruikte optie voor services die tijdens een aanvraag consistent moeten zijn, zoals een databasecontext.
Singleton maakt het object eenmalig aan voor de gehele levensduur van de applicatie. Gebruik dit alleen voor services die echt gedeeld mogen worden en geen veranderlijke toestand bijhouden die tot race conditions kan leiden.
In technische softwareomgevingen, zoals machinebesturing of real-time systemen, is de keuze extra relevant. Een Singleton die onbedoeld gedeelde toestand beheert in een multithreaded omgeving kan tot subtiele en moeilijk te debuggen fouten leiden.
Welke veelgemaakte fouten maak je met dependency injection in C#?
De meest voorkomende fouten met dependency injection in C# zijn verkeerde levensduurconfiguraties, te veel afhankelijkheden in één klasse en het gebruik van de DI-container als Service Locator. Elk van deze fouten ondermijnt de voordelen van het patroon.
Concrete valkuilen zijn:
- Een Scoped service injecteren in een Singleton: De Scoped service leeft langer dan bedoeld en kan leiden tot onverwacht gedrag of geheugenlekkages.
- Constructor overloading: Een klasse met tien of meer geïnjecteerde afhankelijkheden is een teken dat de klasse te veel verantwoordelijkheden heeft. Splits hem op.
- DI-container aanroepen vanuit de applicatiecode: Dit is het Service Locator anti-pattern. De container hoort alleen in de compositieroot te worden gebruikt, niet verspreid door je codebase.
- Concrete klassen registreren zonder interface: Dit maakt unit testing moeilijker omdat je geen mock kunt injecteren.
- Vergeten services te registreren: Dit leidt tot runtime-fouten die soms pas laat in het testproces worden ontdekt.
Door deze fouten te kennen en actief te vermijden, haal je het meeste uit dependency injection als software design pattern in C#.
Hoe PROMEXX werkt met dependency injection en C#
Bij PROMEXX werken we dagelijks met C# in complexe technische omgevingen: van machinebesturing en robotica tot real-time softwaresystemen voor de hightech industrie. Dependency injection is daarin geen theorie, maar een praktisch fundament dat we toepassen in echte projecten bij toonaangevende klanten.
Als software engineer bij ons profiteer je van:
- Projecten waarbij je C# toepast in uitdagende, tastbare systemen zoals motion control, vision en mechatronica
- Een omgeving waar software design patterns zoals DI geen schoolboekstof zijn, maar dagelijkse praktijk
- Kennissessies, trainingen en coaching om je technische vaardigheden structureel te blijven ontwikkelen
- Een vaste thuisbasis bij een klein, inhoudelijk sterk team, met afwisselende opdrachten bij grote hightechbedrijven
Ben jij een ervaren C# developer die energie krijgt van technisch uitdagende software? Bekijk dan onze openstaande vacature voor C# Software Engineer en ontdek wat PROMEXX voor jou kan betekenen.
Veelgestelde vragen
Kan ik dependency injection ook gebruiken in een C# console-applicatie, of is het alleen voor webapplicaties?
Dependency injection is zeker niet beperkt tot webapplicaties. Je kunt u003ccodeu003eMicrosoft.Extensions.DependencyInjectionu003c/codeu003e eenvoudig toevoegen als NuGet-pakket aan elke C#-applicatie, inclusief console-apps, desktop-applicaties en embedded systemen. Je bouwt handmatig een u003ccodeu003eServiceCollectionu003c/codeu003e op, registreert je services en bouwt een u003ccodeu003eServiceProvideru003c/codeu003e die de afhankelijkheden oplost. Dit is precies hoe het ook werkt in technische omgevingen zoals machinebesturing, waar geen webframework aanwezig is.
Hoe schrijf ik unit tests voor klassen die gebruikmaken van dependency injection?
Het grote voordeel van DI is juist dat het unit testing eenvoudig maakt. Omdat afhankelijkheden via de constructor worden meegegeven als interfaces, kun je in je tests eenvoudig een mock of stub injecteren met een framework zoals Moq of NSubstitute. Je instantieert de klasse direct in je test zonder de DI-container te betrekken, geeft de gemockte afhankelijkheden mee via de constructor en test het gedrag geïsoleerd. De container is in unit tests volledig overbodig.
Wat is het verschil tussen de ingebouwde .NET DI-container en een extern framework zoals Autofac?
De ingebouwde u003ccodeu003eIServiceCollectionu003c/codeu003e van .NET biedt de basisfunctionaliteit die voor de meeste applicaties meer dan voldoende is: de drie levensduren, open generics en factory-registraties. Externe frameworks zoals Autofac, Ninject of Castle Windsor bieden geavanceerdere mogelijkheden, zoals property injection via conventies, decorators, named registrations en uitgebreidere module-ondersteuning. Voor de meeste projecten is de ingebouwde container de beste keuze vanwege de eenvoud en native integratie; stap alleen over naar een extern framework als je specifieke functionaliteit nodig hebt die de standaard container niet biedt.
Hoe ga ik om met afhankelijkheden die configuratiewaarden of opties nodig hebben?
Gebruik hiervoor het Options-patroon van .NET via u003ccodeu003eIOptionsu0026lt;Tu0026gt;u003c/codeu003e, u003ccodeu003eIOptionsSnapshotu0026lt;Tu0026gt;u003c/codeu003e of u003ccodeu003eIOptionsMonitoru0026lt;Tu0026gt;u003c/codeu003e. Je definieert een configuratieklasse, koppelt die aan een sectie in u003ccodeu003eappsettings.jsonu003c/codeu003e via u003ccodeu003eservices.Configureu0026lt;MijnOptiesu0026gt;(configuration.GetSection(u0022MijnSectieu0022))u003c/codeu003e en injecteert u003ccodeu003eIOptionsu0026lt;MijnOptiesu0026gt;u003c/codeu003e in de klasse die de waarden nodig heeft. Dit is de aanbevolen manier om configuratie en DI te combineren, omdat het testbaar blijft en je geen statische configuratietoegang nodig hebt.
Wat doe ik als ik meerdere implementaties van dezelfde interface wil registreren?
De standaard .NET DI-container ondersteunt het injecteren van meerdere implementaties via u003ccodeu003eIEnumerableu0026lt;IMyServiceu0026gt;u003c/codeu003e: registreer elke implementatie afzonderlijk en injecteer de collectie in je klasse. Als je een specifieke implementatie op naam of conditie wilt ophalen, kun je een factory-patroon gebruiken of overstappen op Autofac, dat named en keyed registrations native ondersteunt. Wees voorzichtig met het Service Locator-patroon als workaround; een goed ontworpen factory of strategy-patroon is vrijwel altijd een betere oplossing.
Hoe herken ik in bestaande code dat dependency injection verkeerd is toegepast?
Veelvoorkomende signalen zijn: klassen die u003ccodeu003enewu003c/codeu003e gebruiken om concrete afhankelijkheden aan te maken, constructors met meer dan vijf à zes parameters, directe aanroepen naar een container of service locator vanuit business logic, en services zonder bijbehorende interface. Ook statische klassen die gedeelde toestand beheren zijn vaak een teken dat DI niet consequent is toegepast. Deze code smells wijzen er doorgaans op dat de klasse te veel verantwoordelijkheden heeft en moeilijk te testen of te vervangen is.
Is dependency injection ook zinvol in kleine of eenvoudige projecten, of is het overkill?
Voor zeer kleine scripts of wegwerptools kan DI inderdaad meer overhead introduceren dan het oplevert. Zodra een project echter groeit, meerdere lagen heeft of unit tests vereist, betaalt DI zich snel terug in onderhoudbaarheid en testbaarheid. In de praktijk is het verstandig om DI ook in kleinere projecten toe te passen als je verwacht dat ze groeien of langdurig onderhouden worden; het is veel eenvoudiger om DI vanaf het begin in te bouwen dan om het later aan bestaande code toe te voegen.
Gerelateerde artikelen
- Heeft embedded software een goede toekomst?
- Wat maakt een softwareproject in de hightech inhoudelijk uitdagend?
- Waarom is C nog steeds dominant in embedded development?
- Hoe behoud je een vaste thuisbasis terwijl je aan verschillende hightech projecten werkt?
- Wat is de impact van AI op embedded softwareontwikkeling?