Cele mai bune practici, sfaturi și trucuri pentru dependența principală a dependenței ASP.NET

În acest articol, voi împărtăși experiențele și sugestiile mele despre utilizarea Dependenței Injection în aplicațiile de bază ASP.NET. Motivația din spatele acestor principii este;

  • Proiectarea eficientă a serviciilor și a dependențelor acestora.
  • Prevenirea problemelor cu mai multe filetări.
  • Prevenirea scurgerilor de memorie.
  • Prevenirea eventualelor erori.

Acest articol presupune că sunteți deja familiarizați cu dependența de injecție și ASP.NET Core la un nivel de bază. Dacă nu, citiți mai întâi documentația ASP.NET Core Dependency Injection.

Elementele de bază

Injecția constructorului

Injecția constructorului este utilizată pentru a declara și obține dependențele unui serviciu de construcția serviciului. Exemplu:

clasa publică ProductService
{
    private readonly IProductRepository _productRepository;
    public ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Delete (id id)
    {
        _productRepository.Delete (id);
    }
}

ProductService injectează IProductRepository ca o dependență în constructorul său, apoi îl folosește în metoda Delete.

Bune practici:

  • Definiți dependențele necesare în mod explicit în constructorul de servicii. Astfel, serviciul nu poate fi construit fără dependențele sale.
  • Alocați dependența injectată unui câmp / proprietate numai de citire (pentru a preveni atribuirea accidentală a altei valori în cadrul unei metode).

Injecția proprietății

Recipientul standard de injecție de dependență ASP.NET Core nu acceptă injecția de proprietăți. Puteți utiliza, însă, un alt recipient care susține injecția proprietății. Exemplu:

folosind Microsoft.Extensions.Logging;
folosind Microsoft.Extensions.Logging.Abstractions;
nume de spatiu MyApp
{
    clasa publică ProductService
    {
        public ILogger  Logger {get; a stabilit; }
        private readonly IProductRepository _productRepository;
        public ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void Delete (id id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Șters un produs cu id = {id}");
        }
    }
}

ProductService declară o proprietate Logger cu setter public. Containerul de injecție de dependență poate seta Loggerul dacă este disponibil (înregistrat la containerul DI înainte).

Bune practici:

  • Folosiți injecția de proprietate numai pentru dependențe opționale. Aceasta înseamnă că serviciul dvs. poate funcționa corespunzător fără aceste dependențe furnizate.
  • Utilizați modelul obiectului nul (ca în acest exemplu), dacă este posibil. În caz contrar, verificați întotdeauna nulul în timp ce utilizați dependența.

Localizator de servicii

Modelul de localizare al serviciilor este un alt mod de obținere a dependențelor. Exemplu:

clasa publică ProductService
{
    private readonly IProductRepository _productRepository;
    ILOGGER privat privat  _logger;
    public ProductService (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    public void Delete (id id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Șters un produs cu id = {id}");
    }
}

ProductService injectează IServiceProvider și rezolvă dependențele care îl utilizează. GetRequiredService aruncă o excepție dacă dependența solicitată nu a fost înregistrată înainte. Pe de altă parte, GetService doar returnează nul în acest caz.

Când rezolvați serviciile din interiorul constructorului, acestea sunt eliberate atunci când este lansat serviciul. Deci, nu vă pasă de eliberarea / eliminarea serviciilor rezolvate în interiorul constructorului (la fel ca și constructorul și injecția de proprietăți).

Bune practici:

  • Nu folosiți modelul de localizare a serviciilor, acolo unde este posibil (dacă tipul de serviciu este cunoscut în timpul dezvoltării). Pentru că face implicit dependențele. Aceasta înseamnă că nu este posibil să vedeți dependențele cu ușurință în timp ce creați o instanță a serviciului. Acest lucru este deosebit de important pentru testele de unitate în care poate doriți să batjocoriți unele dependențe ale unui serviciu.
  • Rezolvați dependențele din constructorul de servicii, dacă este posibil. Rezolvarea într-o metodă de serviciu face ca aplicația dvs. să fie mai complicată și predispusă la erori. Voi acoperi problemele și soluțiile în secțiunile următoare.

Timpuri de viață

Există trei durate de viață a serviciului în dependența de dependență de bază a ASP.NET:

  1. Serviciile tranzitorii sunt create de fiecare dată când sunt injectate sau solicitate.
  2. Serviciile vizate sunt create pe domeniu de aplicare. Într-o aplicație web, fiecare solicitare web creează un nou domeniu de servicii separat. Aceasta înseamnă că serviciile identificate sunt în general create pe cerere web.
  3. Serviciile Singleton sunt create pe container. Asta înseamnă, în general, că sunt create o singură dată pe aplicație și apoi utilizate pentru întreaga durată de viață a aplicației.

Containerul DI ține evidența tuturor serviciilor rezolvate. Serviciile sunt eliberate și dispuse la sfârșitul vieții lor:

  • Dacă serviciul are dependențe, acestea sunt de asemenea eliberate și eliminate automat.
  • Dacă serviciul implementează interfața IDispozabilă, metoda Eliminați este apelată automat la lansarea serviciului.

Bune practici:

  • Înregistrați-vă serviciile ca fiind tranzitorii, acolo unde este posibil. Pentru că este simplu să proiectăm servicii tranzitorii. În general, nu vă pasă de scurgeri de memorie multi-thread și de memorie și știți că serviciul are o durată scurtă de viață.
  • Folosiți cu atenție durata de viață a serviciului cu scopuri, deoarece poate fi complicat dacă creezi scopuri de servicii pentru copii sau utilizați aceste servicii dintr-o aplicație non-web.
  • Folosiți cu atenție durata de viață a singletonului, de atunci trebuie să vă confruntați cu probleme de scurgere a memoriei și potențialelor filetări.
  • Nu depindeți de un serviciu tranzitoriu sau orientat de la un serviciu singleton. Deoarece serviciul tranzitoriu devine o instanță singleton atunci când un serviciu singleton îl injectează și asta poate cauza probleme dacă serviciul tranzitoriu nu este proiectat pentru a susține un astfel de scenariu. Containerul implicit DI al ASP.NET Core aruncă deja excepții în astfel de cazuri.

Rezolvarea serviciilor într-un corp de metodă

În unele cazuri, poate fi necesar să rezolvați un alt serviciu într-o metodă a serviciului dvs. În astfel de cazuri, asigurați-vă că eliberați serviciul după utilizare. Cel mai bun mod de a vă asigura că este să creați un scop de serviciu. Exemplu:

clasă publică PriceCalculator
{
    privat readonly IServiceProvider _serviceProvider;
    public PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Calculați (produs produs, număr de int,
      Tipul tipStrategyServiceType)
    {
        folosind (var range = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) environment.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            preț var = produs.Preț * număr;
            preț retur + impozitStrategie.CalculateTax (preț);
        }
    }
}

PriceCalculator injectează IServiceProvider în constructorul său și îl atribuie unui câmp. PriceCalculator îl folosește în interiorul metodei Calcul pentru a crea un serviciu pentru copii. Folosește domeniul de aplicare range.ServiceProvider pentru a rezolva serviciile, în loc de instanța injectată _serviceProvider. Astfel, toate serviciile rezolvate din sfera de aplicare sunt eliberate / eliminate automat la sfârșitul declarației de utilizare.

Bune practici:

  • Dacă rezolvați un serviciu într-un corp de metodă, creați întotdeauna un domeniu de servicii pentru copii pentru a vă asigura că serviciile rezolvate sunt eliberate corespunzător.
  • Dacă o metodă primește IServiceProvider ca argument, atunci puteți rezolva direct serviciile de la ea fără să vă preocupați de eliberare / eliminare. Crearea / gestionarea domeniului de serviciu este o responsabilitate a codului care apelează metoda dvs. Respectând acest principiu, vă curățați codul.
  • Nu rețineți o referință la un serviciu rezolvat! În caz contrar, aceasta poate provoca scurgeri de memorie și veți accesa un serviciu eliminat atunci când utilizați mai târziu referința obiectului (cu excepția cazului în care serviciul rezolvat este singleton).

Servicii Singleton

Serviciile Singleton sunt concepute în general pentru a păstra o stare a aplicației. Un cache este un bun exemplu de stări de aplicație. Exemplu:

clasa publică FileService
{
    private readonly ConcurrentDictionary  _cache;
    public FileService ()
    {
        _cache = new ConcurrentDictionary  ();
    }
    public byte [] GetFileContent (string filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            returnare File.ReadAllBytes (filePath);
        });
    }
}

FileService pur și simplu cache conținutul fișierului pentru a reduce lecturile pe disc. Acest serviciu ar trebui să fie înregistrat ca singleton. În caz contrar, cache-ul nu va funcționa așa cum era de așteptat.

Bune practici:

  • Dacă serviciul deține o stare, acesta trebuie să acceseze starea respectivă într-un mod sigur. Deoarece toate cererile utilizează simultan aceeași instanță a serviciului. Am folosit ConcurrentDictionary în loc de Dicționar pentru a asigura siguranța firului.
  • Nu utilizați servicii orientate sau tranzitorii din serviciile singleton. Deoarece, serviciile tranzitorii s-ar putea să nu fie proiectate pentru a fi sigure. Dacă trebuie să le utilizați, atunci aveți grijă de multi-threading în timp ce utilizați aceste servicii (folosiți blocarea de exemplu).
  • Scurgerile de memorie sunt cauzate în general de serviciile singleton. Nu sunt eliberate / eliminate până la sfârșitul cererii. Deci, dacă inițiază clase (sau injectează), dar nu le eliberează / le elimină, vor rămâne și în memorie până la sfârșitul aplicației. Asigurați-vă că le eliberați / le aruncați la momentul potrivit. Consultați Rezolvarea serviciilor într-o secțiune de mai sus a unui corp de metode.
  • Dacă introduceți date în cache (conținutul fișierului din acest exemplu), ar trebui să creați un mecanism de actualizare / invalidare a datelor din cache atunci când se schimbă sursa de date originală (când un fișier din cache se schimbă pe disc pentru acest exemplu).

Servicii Scoped

Durata de viață urmărită mai întâi pare un candidat bun pentru a stoca datele de la solicitarea web. Deoarece ASP.NET Core creează un domeniu de servicii pentru fiecare solicitare web. Deci, dacă înregistrați un serviciu ca obiect, acesta poate fi partajat în timpul unei solicitări web. Exemplu:

public class RequestItemsService
{
    Dictionar privat readonly  _items;
    public RequestItemsService ()
    {
        _items = new Dictionary  ();
    }
    public void Set (numele șirului, valoarea obiectului)
    {
        _items [nume] = valoare;
    }
    obiect public Get (nume șir)
    {
        return _items [nume];
    }
}

Dacă înregistrați RequestItemsService ca scop și îl injectați în două servicii diferite, atunci puteți obține un element adăugat de la un alt serviciu, deoarece acestea vor partaja aceeași instanță RequestItemsService. Aceasta este ceea ce ne așteptăm de la servicii acoperite.

Dar .. este posibil să nu fie întotdeauna așa. Dacă creați un domeniu de serviciu pentru copii și rezolvați RequestItemsService din sfera copilului, atunci veți primi o nouă instanță a RequestItemsService și nu va funcționa așa cum vă așteptați. Așadar, un serviciu identificat nu înseamnă întotdeauna o instanță pentru fiecare solicitare web.

S-ar putea să credeți că nu faceți o greșeală atât de evidentă (rezolvarea unui scop în interiorul unui cadru pentru copii). Dar aceasta nu este o greșeală (o utilizare foarte regulată) și cazul poate să nu fie atât de simplu. Dacă există un grafic de dependență mare între serviciile dvs., nu puteți ști dacă cineva a creat un scop pentru copii și a rezolvat un serviciu care injectează un alt serviciu ... care în sfârșit injectează un serviciu cu scopuri.

Bun antrenament:

  • Un serviciu orientat poate fi considerat ca o optimizare în cazul în care este injectat de prea multe servicii într-o solicitare web. Astfel, toate aceste servicii vor folosi o singură instanță a serviciului în timpul aceleiași solicitări web.
  • Serviciile vizate nu trebuie să fie proiectate ca sigure. Deoarece, acestea ar trebui să fie utilizate în mod normal de o singură solicitare web / thread. Dar ... în acest caz, nu ar trebui să partajați scopuri de servicii între diferite fire!
  • Aveți grijă dacă proiectați un serviciu cu scopul de a partaja date între alte servicii într-o solicitare web (explicată mai sus). Puteți stoca pe fiecare cerere web date în HttpContext (injectați IHttpContextAccessor pentru a-l accesa), care este cel mai sigur mod de a face acest lucru. Durata de viață a HttpContext nu este definită. De fapt, nu este deloc înregistrat în DI (de aceea nu îl injectați, ci injectați în schimb IHttpContextAccessor). Implementarea HttpContextAccessor folosește AsyncLocal pentru a partaja același HttpContext în timpul unei solicitări web.

Concluzie

Injecția de dependență pare simplă de utilizat la început, dar există probleme potențiale de multi-filetare și scurgeri de memorie dacă nu respectați niște principii stricte. Am împărtășit câteva principii bune bazate pe propriile experiențe în timpul dezvoltării cadrului ASP.NET Boilerplate.