Delen via


Richtlijnen voor afhankelijkheidsinjectie

Dit artikel bevat algemene richtlijnen en aanbevolen procedures voor het implementeren van afhankelijkheidsinjectie in .NET-toepassingen.

Ontwerpen van diensten voor afhankelijkheidsinjectie

Bij het ontwerpen van services voor afhankelijkheidsinjectie:

  • Vermijd statusgevoelige, statische klassen en leden. Vermijd het maken van een globale status door apps te ontwerpen om in plaats daarvan singleton-services te gebruiken.
  • Vermijd directe instantiëring van afhankelijke klassen binnen services. Directe instantiëring koppelt de code aan een bepaalde implementatie.
  • Maak services klein, goed gefactoreerd en eenvoudig getest.

Als een klasse veel geïnjecteerde afhankelijkheden heeft, kan het een teken zijn dat de klasse te veel verantwoordelijkheden heeft en het SRP (Single Responsibility Principle) schendt. Probeer de klasse te herstructureren door een deel van de verantwoordelijkheden naar nieuwe klassen te verplaatsen.

Verwijdering van diensten

De container is verantwoordelijk voor het opruimen van de typen die het aanmaakt, en roept Dispose aan op IDisposable-exemplaren. Services die zijn omgezet vanuit de container, mogen nooit worden verwijderd door de ontwikkelaar. Als een type of factory als singleton is geregistreerd, verwijdert de container de singleton automatisch.

In het volgende voorbeeld worden de diensten gemaakt door de servicecontainer en automatisch opgeruimd.

namespace ConsoleDisposable.Example;

public sealed class TransientDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}

Het voorgaande wegwerpproduct is bedoeld om een korte levensduur te hebben.

namespace ConsoleDisposable.Example;

public sealed class ScopedDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}

Het voorgaande wegwerpartikel is bedoeld om een beperkte levensduur te hebben.

namespace ConsoleDisposable.Example;

public sealed class SingletonDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}

Het voorgaande wegwerpproduct is bedoeld om een levensduur van een singleton te hebben.

using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();

using IHost host = builder.Build();

ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();

ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();

await host.RunAsync();

static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
    Console.WriteLine($"{scope}...");

    using IServiceScope serviceScope = services.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;

    _ = provider.GetRequiredService<TransientDisposable>();
    _ = provider.GetRequiredService<ScopedDisposable>();
    _ = provider.GetRequiredService<SingletonDisposable>();
}

In de console voor foutopsporing ziet u de volgende voorbeelduitvoer nadat deze is uitgevoerd:

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

Services die niet zijn gemaakt door de servicecontainer

Kijk eens naar de volgende code:

// Register example service in IServiceCollection
builder.Services.AddSingleton(new ExampleService());

In de voorgaande code:

  • Het ExampleService exemplaar wordt niet aangemaakt door de servicecontainer.
  • In het framework worden de services niet automatisch verwijderd.
  • De ontwikkelaar is verantwoordelijk voor het verwijderen van de services.

IDisposable-richtlijnen voor tijdelijke en gedeelde exemplaren

Tijdelijke, beperkte levensduur

Scenario

De app vereist een IDisposable exemplaar met een tijdelijke levensduur voor een van de volgende scenario's:

  • Het exemplaar wordt opgelost in de root scope (hoofdcontainer).
  • De instantie moet worden verwijderd voordat de scope afloopt.

Oplossing

Gebruik het factory-patroon om een instantie buiten het bovenliggende bereik te maken. In deze situatie heeft de app doorgaans een Create methode waarmee de constructor van het uiteindelijke type rechtstreeks wordt aangeroepen. Als het laatste type andere afhankelijkheden heeft, kan de factory het volgende doen:

Gedeeld exemplaar, beperkte levensduur

Scenario

De app vereist een gedeeld exemplaar IDisposable voor meerdere services, maar het IDisposable exemplaar moet een beperkte levensduur hebben.

Oplossing

Registreer het object met een gescope levensduur. Gebruik IServiceScopeFactory.CreateScope om een nieuwe IServiceScope te maken. Gebruik het bereik IServiceProvider om vereiste services te verkrijgen. Verwijder de scope wanneer deze niet meer nodig is.

Algemene IDisposable richtlijnen

  • Registreer IDisposable geen exemplaren met een tijdelijke levensduur. Gebruik in plaats daarvan het fabriekspatroon zodat de opgeloste service handmatig kan worden verwijderd nadat deze is gebruikt.
  • Los geen instanties op met een tijdelijke of omvangslevensduur in de hoofdscope IDisposable. De enige uitzondering hierop is als de app maakt/opnieuw maakt en verwijdert IServiceProvider, maar dit is geen ideaal patroon.
  • Voor het ontvangen van een IDisposable afhankelijkheid via DI is het niet nodig dat de ontvanger IDisposable implementeert. De ontvanger van de IDisposable afhankelijkheid mag Dispose niet aanroepen op die afhankelijkheid.
  • Gebruik scopes om de levensduur van services te beheren. Scopes zijn niet hiërarchisch en er is geen bijzondere relatie tussen scopes.

Zie Implementeer een Dispose methode of implementeer een DisposeAsync methode voor meer informatie over het opschonen van resources. Houd ook rekening met de tijdelijke wegwerpservices die zijn vastgelegd in het containerscenario , omdat het betrekking heeft op het opschonen van resources.

Standaard servicecontainer vervanging

De ingebouwde servicecontainer is ontworpen voor de behoeften van het framework en de meeste consumenten-apps. U wordt aangeraden de ingebouwde container te gebruiken, tenzij u een specifieke functie nodig hebt die niet wordt ondersteund, zoals:

  • Eigenschapsinjectie
  • Injectie op basis van naam (alleen.NET 7 en eerdere versies). Zie Keyed-services voor meer informatie.)
  • Onderliggende containers
  • Aangepast levensduurbeheer
  • Func<T> ondersteuning voor luie initialisatie
  • Registratie op basis van conventies

De volgende containers van derden kunnen worden gebruikt met ASP.NET Core-apps:

Schroefdraadveiligheid

Maak threadveilige singletondiensten. Als een singleton-service afhankelijk is van een tijdelijke service, kan de tijdelijke service ook threadveiligheid vereisen, afhankelijk van hoe deze wordt gebruikt door de singleton.

De factorymethode van een singleton-service, zoals het tweede argument voor AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), hoeft niet thread-safe te zijn. Net als een typeconstructorstatic wordt het gegarandeerd slechts één keer door een enkele thread aangeroepen.

Aanbevelingen

  • async/await en Task service-gebaseerde resolutie wordt niet ondersteund. Omdat C# geen asynchrone constructors ondersteunt, gebruikt u asynchrone methoden nadat de service synchroon is opgelost.
  • Vermijd het opslaan van gegevens en configuratie rechtstreeks in de servicecontainer. Het winkelwagentje van een gebruiker mag bijvoorbeeld meestal niet worden toegevoegd aan de servicecontainer. Voor de configuratie moet het optiespatroon worden gebruikt. Vermijd op dezelfde manier 'gegevenshouder'-objecten die alleen bestaan om toegang tot een ander object toe te staan. Het is beter om het werkelijke item via DI aan te vragen.
  • Voorkom statische toegang tot services. Vermijd bijvoorbeeld het vastleggen van IApplicationBuilder.ApplicationServices als een statisch veld of een eigenschap voor gebruik elders.
  • Houd DI-fabrieken snel en synchroon.
  • Vermijd het gebruik van het service locator-patroon. Roep bijvoorbeeld niet GetService aan om een service-exemplaar te verkrijgen wanneer u in plaats daarvan DI kunt gebruiken.
  • Een andere servicezoekervariatie om te voorkomen, is het injecteren van een factory die afhankelijkheden tijdens runtime oplost. Beide werkwijzen combineren Inversion of Control-strategieën .
  • Vermijd aanroepen van BuildServiceProvider bij het configureren van services. Het aanroepen BuildServiceProvider gebeurt meestal wanneer de ontwikkelaar een service wil oplossen bij het registreren van een andere service. Gebruik in plaats daarvan een overloadfunctie die IServiceProvider om deze reden opneemt.
  • Tijdelijke wegwerpdiensten worden opgevangen door de container voor verwijdering. Dit kan leiden tot een geheugenlek als het wordt opgelost vanuit een container op het hoogste niveau.
  • Schakel bereikvalidatie in om ervoor te zorgen dat de app geen singletons heeft die scoped services gebruiken. Zie Bereikvalidatie voor meer informatie.
  • Gebruik alleen singleton-levensduur voor services met hun eigen status die duur is om te maken of wereldwijd gedeeld. Vermijd het gebruik van singleton-levensduur voor services die zelf geen status hebben. De meeste .NET IoC-containers gebruiken 'Tijdelijk' als het standaardbereik. Overwegingen en nadelen van singletons:
    • Threadveiligheid: Een singleton moet threadveilig worden geïmplementeerd.
    • Koppeling: Het kan andere niet-gerelateerde aanvragen koppelen.
    • Testuitdagingen: Gedeelde status en koppeling kunnen het testen van eenheden moeilijker maken.
    • Geheugenimpact: Een singleton kan een grote objectgrafiek actief in het geheugen houden gedurende de levensduur van de toepassing.
    • Fouttolerantie: als een singleton of een deel van de afhankelijkheidsstructuur mislukt, kan deze niet eenvoudig worden hersteld.
    • Opnieuw laden van configuratie: Singletons kunnen over het algemeen geen ondersteuning bieden voor 'hot reload' van configuratiewaarden.
    • Bereiklekken: Een singleton kan per ongeluk gescopeerde of tijdelijke afhankelijkheden vastleggen, waardoor ze effectief worden gepromoveerd tot singletons en onbedoelde bijwerkingen veroorzaken.
    • Initialisatie-overhead: Wanneer een service wordt opgelost, moet de IoC-container het singleton-exemplaar opzoeken. Als het nog niet bestaat, moet het op een threadveilige manier aangemaakt worden. Een staatloze tijdelijke service is daarentegen erg goedkoop om te maken en te vernietigen.

Net als bij alle sets aanbevelingen kunnen situaties optreden waarin het negeren van een aanbeveling vereist is. Uitzonderingen zijn zeldzaam, meestal speciale gevallen binnen het framework zelf.

DI is een alternatief voor statische/globale objecttoegangspatronen. Mogelijk kunt u de voordelen van DI niet realiseren als u deze combineert met statische objecttoegang.

Voorbeeld van antipatronen

Naast de richtlijnen in dit artikel zijn er verschillende antipatronen die u moet vermijden. Sommige van deze antipatronen zijn leerervaringen die voortkomen uit het ontwikkelen van de runtimes zelf.

Waarschuwing

Dit zijn voorbeelden van antipatronen, kopieer de code niet , gebruik deze patronen niet en vermijd deze patronen in alle kosten.

Tijdelijke wegwerpservices vastgelegd door container

Wanneer u Transient services registreert die IDisposable implementeren, houdt de DI-container deze verwijzingen standaard vast en verwijdert deze niet totdat de container wordt verwijderd wanneer de toepassing stopt, als ze zijn verkregen vanuit de container, of totdat het bereik wordt verwijderd als ze zijn verkregen vanuit een bereik. Dit kan veranderen in een geheugenlek als het wordt opgelost op containerniveau.

Antipatroon: Transiënte wegwerpartikelen zonder dispose. Niet kopiëren!

In het voorgaande antipatroon worden 1.000 ExampleDisposable objecten geïnstantieerd en gegrond. Ze worden pas verwijderd nadat het serviceProvider exemplaar is verwijderd.

Zie Fouten opsporen in een geheugenlek in .NET voor meer informatie over het opsporen van fouten in geheugenlekken.

Asynchrone DI-factories kunnen deadlocks veroorzaken

De term 'DI-fabrieken' verwijst naar de overbelastingsmethoden die bestaan bij het aanroepen van Add{LIFETIME}. Er zijn overloads die een Func<IServiceProvider, T> accepteren waarbij T de service is die wordt geregistreerd, en de parameter implementationFactory wordt genoemd. De implementationFactory kan worden opgegeven als een lambda-expressie, lokale functie of methode. Als de fabriek asynchroon is en u gebruikt Task<TResult>.Result, veroorzaakt dit een impasse.

Antipatroon: Deadlock met async factory. Niet kopiëren!

In de voorgaande code wordt implementationFactory een lambda-expressie gegeven waarbij de body Task<TResult>.Result aanroept op een Task<Bar> retournerende methode. Dit veroorzaakt een impasse. De GetBarAsync methode emuleert gewoon een asynchrone werkbewerking met Task.Delayen roept vervolgens aan GetRequiredService<T>(IServiceProvider).

Antipatroon: Deadlock met asynchroon fabrieksintern probleem. Niet kopiëren!

Zie Asynchrone programmering voor meer informatie over asynchrone richtlijnen : Belangrijke informatie en advies. Zie Fouten opsporen in een impasse in .NET voor meer informatie over het opsporen van impasses.

Wanneer u dit antipatroon uitvoert en er een impasse optreedt, kunt u de twee threads die wachten bekijken in het venster Parallel Stacks van Visual Studio. Voor meer informatie, zie Threads en taken weergeven in het venster Parallelle stacks.

Afhankelijkheid in gevangenschap

De term 'captive dependency' werd bedacht door Mark Seemann en verwijst naar de onjuiste configuratie van de levensduur van de service, waarbij een langerlevende service een kortere service-captive bevat.

Antipatroon: Captive-afhankelijkheid. Niet kopiëren!

In de voorgaande code wordt Foo als een singleton geregistreerd en wordt Bar binnen een bereik geregistreerd - wat op het eerste gezicht geldig lijkt. Neem echter de implementatie van Foo in overweging.

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

Voor het Foo-object is een Bar-object vereist, en omdat Foo een singleton is en Bar is begrensd, is dit een onjuiste configuratie. Zoals het is, Foo wordt slechts eenmaal geïnstantieerd en houdt Bar vast gedurende zijn levensduur, die langer is dan de bedoelde beperkte levensduur van Bar. Overweeg om scopes te valideren door deze door te geven aan de validateScopes: trueBuildServiceProvider(IServiceCollection, Boolean). Wanneer u de scopes valideert, krijgt u een InvalidOperationException met een bericht dat lijkt op "Kan scoped service 'Bar' niet gebruiken vanuit singleton 'Foo'.".

Zie Bereikvalidatie voor meer informatie.

Scoped-service als singleton

Wanneer u scoped services gebruikt, wordt de service een singleton als u geen nieuwe scope creëert of zich niet binnen een bestaande scope bevindt.

Antipatroon: De scoped-service wordt singleton. Niet kopiëren!

In de voorgaande code wordt Bar opgehaald binnen een IServiceScope, wat juist is. Het antipatroon is het ophalen van Bar buiten het bereik en de variabele krijgt de naam avoid om aan te geven welk voorbeeld ophalen onjuist is.

Zie ook