Βασικές πρακτικές έγχυσης εξαρτημάτων ASP.NET, συμβουλές και κόλπα

Σε αυτό το άρθρο, θα μοιραστώ τις εμπειρίες και τις προτάσεις μου σχετικά με τη χρήση της έγχυσης εξαρτήσεων σε εφαρμογές ASP.NET Core. Τα κίνητρα αυτών των αρχών είναι:

  • Ο σχεδιασμός των υπηρεσιών και οι εξαρτήσεις τους.
  • Πρόληψη ζητημάτων πολλαπλών σπειρωμάτων.
  • Αποτροπή διαρροών μνήμης.
  • Πρόληψη πιθανών σφαλμάτων.

Αυτό το άρθρο υποθέτει ότι είστε ήδη εξοικειωμένοι με το Injection Dependency and ASP.NET Core σε ένα βασικό επίπεδο. Αν όχι, διαβάστε πρώτα την τεκμηρίωση ένεσης βασικής βάσης ASP.NET Core.

Βασικά

Έγχυση κατασκευαστή

Η ένεση του κατασκευαστή χρησιμοποιείται για να δηλώσει και να αποκτήσει εξαρτήσεις μιας υπηρεσίας στην κατασκευή της υπηρεσίας. Παράδειγμα:

δημόσια classServiceService
{
    ιδιωτικό readonly IProductRepository _productRepository;
    δημόσια υπηρεσία προϊόντων (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }}
    δημόσιο κενό Διαγραφή (int id)
    {
        _productRepository.Delete (id);
    }}
}}

Το ProductService εισάγει το IProductRepository ως εξάρτηση στον κατασκευαστή του και στη συνέχεια το χρησιμοποιεί μέσα στη μέθοδο Delete.

Καλές πρακτικές:

  • Καθορίστε ρητά τις απαιτούμενες εξαρτήσεις στον κατασκευαστή υπηρεσίας. Έτσι, η υπηρεσία δεν μπορεί να κατασκευαστεί χωρίς τις εξαρτήσεις της.
  • Αντιστοιχίστε την εξάρτηση που έχει εγχυθεί σε ένα πεδίο / ιδιότητα μόνο για ανάγνωση (για να αποφευχθεί τυχαία ανάθεση μιας άλλης αξίας σε αυτήν μέσα σε μια μέθοδο).

Ενέχυρο ιδιοκτησίας

Το βασικό δοχείο έγχυσης εξαρτημάτων ASP.NET Core δεν υποστηρίζει την έγχυση της ιδιότητας. Αλλά μπορείτε να χρησιμοποιήσετε ένα άλλο δοχείο που υποστηρίζει την ένεση ιδιοκτησίας. Παράδειγμα:

χρησιμοποιώντας το Microsoft.Extensions.Logging;
χρησιμοποιώντας το Microsoft.Extensions.Logging.Abstractions;
όνομα χώρου MyApp
{
    δημόσια classServiceService
    {
        δημόσιο ILogger  Logger {get; σειρά; }}
        ιδιωτικό readonly IProductRepository _productRepository;
        δημόσια υπηρεσία προϊόντων (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Καταχωρητής = NullLogger  .Instance;
        }}
        δημόσιο κενό Διαγραφή (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Διαγράφηκε ένα προϊόν με id = {id}");
        }}
    }}
}}

Το ProductService δηλώνει μια ιδιότητα Logger με το δημόσιο setter. Το δοχείο έγχυσης εξαρτήσεων μπορεί να ρυθμίσει το Logger αν είναι διαθέσιμο (καταχωρήθηκε στο δοχείο DI πριν).

Καλές πρακτικές:

  • Χρησιμοποιήστε την ένεση ιδιοκτησίας μόνο για προαιρετικές εξαρτήσεις. Αυτό σημαίνει ότι η υπηρεσία σας μπορεί να λειτουργήσει σωστά χωρίς αυτές τις εξαρτήσεις που παρέχονται.
  • Χρησιμοποιήστε το Null Pattern Pattern (όπως σε αυτό το παράδειγμα), αν είναι δυνατόν. Διαφορετικά, ελέγχετε πάντα για μηδενική χρήση της εξάρτησης.

Υπηρεσία εντοπισμού

Το μοτίβο εντοπισμού εξυπηρέτησης είναι ένας άλλος τρόπος απόκτησης εξαρτήσεων. Παράδειγμα:

δημόσια classServiceService
{
    ιδιωτικό readonly IProductRepository _productRepository;
    private readonly ILogger  _logger;
    δημόσια Υπηρεσία Προϊόντων (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > ();
            NullLogger  .Instance;
    }}
    δημόσιο κενό Διαγραφή (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Διαγράφηκε ένα προϊόν με id = {id}");
    }}
}}

Το ProductService εγχέει IServiceProvider και επιλύει εξαρτήσεις χρησιμοποιώντας το. Η GetRequiredService ρίχνει την εξαίρεση αν η αιτούμενη εξάρτηση δεν είχε καταχωρηθεί πριν. Από την άλλη πλευρά, η GetService μόλις επιστρέφει σε αυτή την περίπτωση.

Όταν επιλύετε υπηρεσίες εντός του κατασκευαστή, απελευθερώνονται όταν κυκλοφορήσει η υπηρεσία. Επομένως, δεν σας ενδιαφέρει η απελευθέρωση / διάθεση υπηρεσιών που επιλύθηκαν στο εσωτερικό του κατασκευαστή (ακριβώς όπως ο κατασκευαστής και η έγχυση της ιδιοκτησίας).

Καλές πρακτικές:

  • Μην χρησιμοποιείτε το μοτίβο εντοπισμού εξυπηρέτησης όπου είναι εφικτό (εάν ο τύπος υπηρεσίας είναι γνωστός στο χρόνο ανάπτυξης). Επειδή κάνει τις εξαρτήσεις σιωπηρές. Αυτό σημαίνει ότι δεν είναι δυνατή η εύκολη προβολή των εξαρτήσεων κατά τη δημιουργία μιας παρουσίας της υπηρεσίας. Αυτό είναι ιδιαίτερα σημαντικό για δοκιμές μονάδων όπου μπορεί να θέλετε να χλευάζετε μερικές εξαρτήσεις μιας υπηρεσίας.
  • Επιλύστε τις εξαρτήσεις στον κατασκευαστή υπηρεσιών αν είναι δυνατόν. Η επίλυση σε μια μέθοδο υπηρεσίας καθιστά την εφαρμογή σας πιο περίπλοκη και επιρρεπής σε σφάλματα. Θα καλύψω τα προβλήματα και τις λύσεις στις επόμενες ενότητες.

Χρόνοι ζωής υπηρεσίας

Υπάρχουν τρεις διάρκειες ζωής στην εφαρμογή ASP.NET Core Dependency Injection:

  1. Οι παροδικές υπηρεσίες δημιουργούνται κάθε φορά που εισάγονται ή ζητούνται.
  2. Οι σκοπευμένες υπηρεσίες δημιουργούνται ανά πεδίο εφαρμογής. Σε μια εφαρμογή ιστού, κάθε αίτηση ιστού δημιουργεί ένα νέο πεδίο χωριστών υπηρεσιών. Αυτό σημαίνει ότι οι γενικές υπηρεσίες γενικά δημιουργούνται ανά αίτηση ιστού.
  3. Οι υπηρεσίες Singleton δημιουργούνται ανά δοχείο DI. Αυτό γενικά σημαίνει ότι δημιουργούνται μόνο μία φορά ανά εφαρμογή και στη συνέχεια χρησιμοποιούνται για ολόκληρο τον χρόνο ζωής της εφαρμογής.

Ο περιέκτης DI παρακολουθεί όλες τις επιλυμένες υπηρεσίες. Οι υπηρεσίες απελευθερώνονται και διατίθενται όταν τελειώνουν οι ζωές τους:

  • Αν η υπηρεσία έχει εξαρτήσεις, αυτομάτως απελευθερώνεται και διατίθεται.
  • Εάν η υπηρεσία υλοποιεί τη διεπαφή IDisposable, η μέθοδος Dispose αποκαλείται αυτόματα για την απελευθέρωση της υπηρεσίας.

Καλές πρακτικές:

  • Καταχωρήστε τις υπηρεσίες σας ως παροδικές, όπου είναι δυνατόν. Επειδή είναι απλό να σχεδιάζετε μεταβατικές υπηρεσίες. Γενικά, δεν ενδιαφέρεστε για διαρροές πολλαπλών σπειρωμάτων και μνήμης και ξέρετε ότι η υπηρεσία έχει μικρή διάρκεια ζωής.
  • Χρησιμοποιήστε προσεκτικά τη διάρκεια ζωής της υπηρεσίας, καθώς μπορεί να είναι δύσκολη αν δημιουργήσετε πεδία υπηρεσιών παιδιών ή χρησιμοποιήσετε αυτές τις υπηρεσίες από μια μη-web εφαρμογή.
  • Χρησιμοποιήστε την προσοχή του singleton προσεκτικά από τότε που θα πρέπει να ασχοληθείτε με τα πολλαπλά σπειρώματα και τα πιθανά προβλήματα διαρροής μνήμης.
  • Μην εξαρτάτε από μια παροδική υπηρεσία ή μια υπηρεσία που καλύπτεται από μια υπηρεσία singleton. Επειδή η παροδική υπηρεσία γίνεται μια περίπτωση singleton όταν μια υπηρεσία singleton το έγχυμα και αυτό μπορεί να προκαλέσει προβλήματα εάν η παροδική υπηρεσία δεν έχει σχεδιαστεί για να υποστηρίξει ένα τέτοιο σενάριο. Το προεπιλεγμένο δοχείο DI του ASP.NET πυρήνα εκτελεί ήδη εξαιρέσεις σε τέτοιες περιπτώσεις.

Επίλυση υπηρεσιών σε ένα σώμα μεθόδου

Σε ορισμένες περιπτώσεις, μπορεί να χρειαστεί να επιλύσετε μια άλλη υπηρεσία με μια μέθοδο της υπηρεσίας σας. Σε τέτοιες περιπτώσεις, βεβαιωθείτε ότι έχετε αποδεσμεύσει την υπηρεσία μετά τη χρήση. Ο καλύτερος τρόπος για να διασφαλιστεί αυτό είναι να δημιουργηθεί ένα πεδίο υπηρεσίας. Παράδειγμα:

δημόσια τάξη PriceCalculator
{
    ιδιωτικό readonly IServiceProvider _serviceProvider;
    δημόσιος τιμοκατάλογος (υπηρεσία IServiceProviderProvider)
    {
        _serviceProvider = serviceProvider;
    }}
    δημόσιο float Υπολογίστε (προϊόν προϊόντος, int count,
      Πληκτρολογήστε taxStrategyServiceType)
    {
        χρησιμοποιώντας (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) πεδίο εφαρμογής. Υπηρεσία εξυπηρέτησης
              .GetRequiredService (taxStrategyServiceType);
            τιμή var = τιμή προϊόντος.
            τιμή επιστροφής + taxStrategy.CalculateTax (τιμή);
        }}
    }}
}}

Το PriceCalculator εισάγει τον IServiceProvider στον κατασκευαστή του και τον αναθέτει σε ένα πεδίο. Το PriceCalculator στη συνέχεια το χρησιμοποιεί μέσα στη μέθοδο Υπολογισμός για να δημιουργήσει ένα πεδίο υπηρεσίας παιδιού. Χρησιμοποιεί το πεδίο εφαρμογής.ServiceProvider για την επίλυση υπηρεσιών, αντί για την ένεση _serviceProvider. Έτσι, όλες οι υπηρεσίες που επιλύονται από το πεδίο απελευθερώνονται / διατίθενται αυτόματα στο τέλος της δήλωσης χρήσης.

Καλές πρακτικές:

  • Αν επιλύετε μια υπηρεσία σε ένα σώμα μεθόδου, δημιουργείτε πάντοτε ένα πεδίο υπηρεσίας παιδιού για να διασφαλίσετε ότι οι επιλυθείσες υπηρεσίες απελευθερώνονται σωστά.
  • Εάν μια μέθοδος παίρνει IServiceProvider ως επιχείρημα, τότε μπορείτε να επιλύσετε άμεσα υπηρεσίες από αυτήν χωρίς να ενδιαφέρεται για την απελευθέρωση / διάθεση. Η δημιουργία / διαχείριση πεδίου υπηρεσίας αποτελεί ευθύνη του κωδικού που καλεί τη μέθοδο σας. Ακολουθώντας αυτή την αρχή καθιστά τον κώδικα σας καθαρότερο.
  • Μην κρατάτε αναφορά σε μια επιλυμένη υπηρεσία! Διαφορετικά, μπορεί να προκαλέσει διαρροές μνήμης και θα έχετε πρόσβαση σε μια υπηρεσία που διατίθεται όταν χρησιμοποιείτε την αναφορά αντικειμένου αργότερα (εκτός αν η επιλυθείσα υπηρεσία είναι singleton).

Υπηρεσίες Singleton

Οι υπηρεσίες Singleton έχουν γενικά σχεδιαστεί για να διατηρούν μια κατάσταση εφαρμογής. Μια μνήμη cache είναι ένα καλό παράδειγμα καταστάσεων εφαρμογής. Παράδειγμα:

δημόσια class FileService
{
    ιδιωτικό readonly ConcurrentDictionary <συμβολοσειρά, byte []> _cache;
    δημόσια FileService ()
    {
        _cache = new ConcurrentDictionary <συμβολοσειρά, byte []> ();
    }}
    δημόσιο byte [] GetFileContent (string filePath)
    {
        επιστροφή _cache.GetOrAdd (filePath, _ =>
        {
            επιστροφή του File.ReadAllBytes (filePath);
        });
    }}
}}

Το FileService απλά αποθηκεύει τα περιεχόμενα του αρχείου για να μειώσει τις αναγνώσεις του δίσκου. Αυτή η υπηρεσία θα πρέπει να καταχωρηθεί ως singleton. Διαφορετικά, η προσωρινή αποθήκευση δεν θα λειτουργήσει όπως αναμένεται.

Καλές πρακτικές:

  • Εάν η υπηρεσία κρατά μια κατάσταση, θα πρέπει να έχει πρόσβαση σε αυτήν την κατάσταση με τρόπο ασφαλή για το νήμα. Επειδή όλα τα αιτήματα χρησιμοποιούν ταυτόχρονα την ίδια εμφάνιση της υπηρεσίας. Χρησιμοποίησα το ConcurrentDictionary αντί για το λεξικό για να διασφαλίσω την ασφάλεια του νήματος.
  • Μην χρησιμοποιείτε υπηρεσίες που καλύπτονται από το πεδίο εφαρμογής ή μεταβατικές υπηρεσίες από υπηρεσίες singleton. Επειδή οι παροδικές υπηρεσίες ενδέχεται να μην έχουν σχεδιαστεί για να είναι ασφαλείς. Εάν πρέπει να τα χρησιμοποιήσετε, φροντίστε να χρησιμοποιήσετε αυτές τις υπηρεσίες (για παράδειγμα, κλειδώστε το για παράδειγμα).
  • Οι διαρροές μνήμης γενικά προκαλούνται από υπηρεσίες singleton. Δεν απελευθερώνονται / διατίθενται μέχρι το τέλος της αίτησης. Επομένως, εάν δημιουργούν στιγμιότυπα τάξεων (ή ενέχουν) αλλά δεν τους απελευθερώνουν / απορρίπτουν, θα παραμείνουν στη μνήμη και μέχρι το τέλος της εφαρμογής. Βεβαιωθείτε ότι έχετε απελευθερώσει / απορρίψει τις σωστές ώρες. Δείτε τις Υπηρεσίες Επίλυσης σε μια ενότητα Body Body παραπάνω.
  • Εάν αποθηκεύετε προσωρινά δεδομένα (περιεχόμενα αρχείων σε αυτό το παράδειγμα), θα πρέπει να δημιουργήσετε έναν μηχανισμό ενημέρωσης / ακύρωσης των αποθηκευμένων δεδομένων όταν αλλάζει η αρχική προέλευση δεδομένων (όταν ένα αρχείο προσωρινής αποθήκευσης αλλάζει στο δίσκο για αυτό το παράδειγμα).

Υπηρεσίες σκοπού

Ο σκοπός της ζωής αρχικά φαίνεται καλός υποψήφιος για αποθήκευση ανά δεδομένα αιτήματος ιστού. Επειδή ο πυρήνας ASP.NET δημιουργεί ένα πεδίο υπηρεσίας ανά αίτηση ιστού. Επομένως, αν καταχωρίσετε μια υπηρεσία ως πεδίου εφαρμογής, μπορεί να μοιραστεί κατά τη διάρκεια μιας αίτησης ιστού. Παράδειγμα:

δημόσια class RequestItemsService
{
    ιδιωτικό readonly Λεξικό  _items;
    δημόσια RequestItemsService ()
    {
        _items = νέο λεξικό  ();
    }}
    δημόσιο κενό Σετ (όνομα συμβολοσειράς, τιμή αντικειμένου)
    {
        _items [όνομα] = τιμή;
    }}
    δημόσιο αντικείμενο Get (όνομα συμβολοσειράς)
    {
        επιστροφή _items [όνομα];
    }}
}}

Αν καταχωρίσετε το RequestItemsService ως πεδίο εφαρμογής και το εισάγετε σε δύο διαφορετικές υπηρεσίες, τότε μπορείτε να λάβετε ένα στοιχείο που προστέθηκε από άλλη υπηρεσία επειδή θα μοιράζονται την ίδια περίπτωση RequestItemsService. Αυτό προσδοκούμε από τις υπηρεσίες με βάση το πεδίο εφαρμογής.

Αλλά .. το γεγονός μπορεί να μην είναι πάντα έτσι. Εάν δημιουργήσετε ένα πεδίο υπηρεσίας παιδιού και επιλύσετε το RequestItemsService από την εμβέλεια του παιδιού, τότε θα πάρετε μια νέα εμφάνιση του RequestItemsService και δεν θα λειτουργήσει όπως περιμένετε. Επομένως, η υπηρεσία με βάση την έννοια δεν σημαίνει πάντοτε την εμφάνιση ανά διαδικτυακό αίτημα.

Μπορείτε να σκεφτείτε ότι δεν κάνετε ένα τέτοιο προφανές λάθος (την επίλυση μιας εμβέλειας εντός ενός πεδίου παιδιού). Αλλά, αυτό δεν είναι ένα λάθος (μια πολύ τακτική χρήση) και η υπόθεση μπορεί να μην είναι τόσο απλή. Εάν υπάρχει μεγάλη γραφική παράσταση εξαρτήσεων μεταξύ των υπηρεσιών σας, δεν μπορείτε να ξέρετε αν κάποιος δημιούργησε ένα παιδικό πεδίο και επιλύθηκε μια υπηρεσία που εισάγει μια άλλη υπηρεσία ... η οποία τελικά εισάγει μια υπηρεσία με πεδία εφαρμογής.

Καλή πρακτική:

  • Μια υπηρεσία με βάση την εμβέλεια μπορεί να θεωρηθεί ως βελτιστοποίηση όπου εισάγεται από πάρα πολλές υπηρεσίες σε ένα αίτημα ιστού. Έτσι, όλες αυτές οι υπηρεσίες θα χρησιμοποιήσουν μια ενιαία παρουσία της υπηρεσίας κατά τη διάρκεια του ίδιου αιτήματος ιστού.
  • Δεν πρέπει να σχεδιάζονται ασφαλείς υπηρεσίες ως ασφαλείς για τα νήματα. Επειδή, θα πρέπει να χρησιμοποιούνται κανονικά από ένα μόνο αίτημα ιστού / νήμα. Αλλά ... σε αυτή την περίπτωση, δεν πρέπει να μοιράζεστε πεδία υπηρεσιών μεταξύ διαφορετικών νημάτων!
  • Προσέξτε αν σχεδιάζετε μια υπηρεσία με σκοπό την ανταλλαγή δεδομένων μεταξύ άλλων υπηρεσιών σε ένα αίτημα ιστού (εξηγείται παραπάνω). Μπορείτε να αποθηκεύσετε δεδομένα ανά αίτημα διαδικτύου μέσα στο HttpContext (εισάγετε το IHttpContextAccessor για να το αποκτήσετε πρόσβαση) που είναι ο ασφαλέστερος τρόπος να το κάνετε αυτό. Η διάρκεια ζωής του HttpContext δεν περιορίζεται. Στην πραγματικότητα, δεν είναι καταχωρημένο στο DI (γι 'αυτό δεν το εγχύετε, αλλά εισάγετε το IHttpContextAccessor αντί). Η εφαρμογή HttpContextAccessor χρησιμοποιεί το AsyncLocal για να μοιραστεί το ίδιο HttpContext κατά τη διάρκεια μιας αίτησης ιστού.

συμπέρασμα

Η ένεση εξαρτήσεων φαίνεται απλή στην αρχή, αλλά υπάρχουν πιθανά προβλήματα πολλαπλών σπειρωμάτων και διαρροής μνήμης αν δεν ακολουθείτε ορισμένες αυστηρές αρχές. Έχω μοιραστεί μερικές καλές αρχές με βάση τις δικές μου εμπειρίες κατά την ανάπτυξη του πλαισίου ASP.NET Boilerplate.