Comment je me préparais pour une entrevue # 2

Dans la deuxième partie, je voulais mettre à jour ma compréhension des architectures Onion et n-Tier, ainsi que des frameworks DI (Autofac et net core intégré). Mais en regardant le volume du texte, j'ai réalisé que n-Tier sera décrit très brièvement, ce dont je m'excuse immédiatement.

Aussi, je vais essayer de prendre en compte les commentaires de la première partie,







Architecture d'oignon



Supposons que nous concevions une application afin d'enregistrer les livres que nous avons lus, mais pour plus de précision, nous voulons même enregistrer le nombre de pages lues. Nous savons qu'il s'agit d'un programme personnel dont nous avons besoin sur notre smartphone, comme un bot pour les télégrammes et, éventuellement, pour le bureau, alors n'hésitez pas à choisir cette option d'architecture:



(Tg Bot, Phone App, Desktop) => Asp.net Web Api => Base de données



Créer un projet dans Visual studio du type Asp.net Core, où nous sélectionnons ensuite le type de projet Web Api.

En quoi est-ce différent de l'habituel?

Tout d'abord, la classe de contrôleur hérite de la classe ControllerBase, qui est conçue pour être la classe de base pour MVC sans prise en charge du retour de vues (code html).

Deuxièmement, il est conçu pour implémenter des services REST couvrant tous les types de requêtes HTTP, et en réponse aux requêtes, vous recevez json avec une indication explicite de l'état de la réponse. De plus, vous verrez que le contrôleur créé par défaut sera marqué avec l'attribut [ApiController], qui a des options utiles spécifiquement pour l'API.



Vous devez maintenant décider comment stocker les données. Puisque je sais que je ne lis pas plus de 12 livres par an, le fichier csv me suffira, ce qui représentera la base de données.



Je crée donc une classe qui décrit le livre:

Book.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApiTest
{
    public class Book
    {
        public int id { get; set; }
        public string name { get; set; }
        public string author { get; set; }
        public int pages { get; set; }
        public int readedPages { get; set; }
    }
}

      
      









Et puis je décris la classe pour travailler avec la base de données:

CsvDB.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WebApiTest
{
    public class CsvDB
    {
        const string dbPath = @"C:\\csv\books.csv";

        private List<Book> books;

        private void Init()
        {
            if (books != null)
                return;
            string[] lines = File.ReadAllLines(dbPath);
            books = new List<Book>();
            foreach(var line in lines)
            {
                string[] cells = line.Split(';');
                Book newBook = new Book()
                {
                    id = int.Parse(cells[0]),
                    name = cells[1],
                    author = cells[2],
                    pages = int.Parse(cells[3]),
                    readedPages = int.Parse(cells[4])
                };
                books.Add(newBook);
            }
        }

        public int Add(Book item)
        {
            Init();
            int nextId = books.Max(x => x.id) + 1;
            item.id = nextId;
            books.Add(item);
            return nextId;
        }

        public void Delete(int id)
        {
            Init();
            Book selectedToDelete = books.Where(x => x.id == id).FirstOrDefault();
            if(selectedToDelete != null)
            {
                books.Remove(selectedToDelete);
            }
        }

        public Book Get(int id)
        {
            Init();
            Book book = books.Where(x => x.id == id).FirstOrDefault();
            return book;
        }

        public IEnumerable<Book> GetList()
        {
            Init();
            return books;
        }

        public void Save()
        {
            StringBuilder sb = new StringBuilder();
            foreach(var book in books)
                sb.Append($"{book.id};{book.name};{book.author};{book.pages};{book.readedPages}");
            File.WriteAllText(dbPath, sb.ToString());
        }

        public bool Update(Book item)
        {
            var selectedBook = books.Where(x => x.id == item.id).FirstOrDefault();
            if(selectedBook != null)
            {
                selectedBook.name = item.name;
                selectedBook.author = item.author;
                selectedBook.pages = item.pages;
                selectedBook.readedPages = item.readedPages;
                return true;
            }
            return false;
        }
    }
}

      
      







Ensuite, la question est petite, ajouter l'API pour pouvoir interagir avec elle:

BookController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace WebApiTest.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class BookController : ControllerBase
    {

        private CsvDB db;

        public BookController()
        {
            db = new CsvDB();
        }

        [HttpGet]
        public IEnumerable<Book> GetList() => db.GetList();

        [HttpGet("{id}")]
        public Book Get(int id) => db.Get(id);

        [HttpDelete("{id}")]
        public void Delete(int id) => db.Delete(id);

        [HttpPut]
        public bool Put(Book book) => db.Update(book);
    }
}

      
      







Et puis il ne reste plus qu'à ajouter l'interface utilisateur, ce qui serait pratique. Et tout fonctionne!

Cool! Mais non, la femme a demandé qu'elle ait également accès à une chose aussi pratique.

Quelles difficultés nous attendent? Tout d'abord, vous devez maintenant ajouter une colonne pour tous les livres qui indiquera l'identifiant de l'utilisateur. Croyez-moi, ce ne sera pas à l'aise avec un fichier csv. De plus, vous devez maintenant ajouter les utilisateurs eux-mêmes! Et même maintenant, une sorte de logique est nécessaire pour que ma femme ne voie pas que je finis de lire la troisième collection de Dontsova au lieu du Tolstoï promis.



Essayons d'étendre ce projet aux exigences requises: La

possibilité de créer un compte utilisateur, qui pourra garder une liste de ses livres et ajouter combien il en lira.

Honnêtement, je voulais écrire un exemple, mais le nombre de choses que je ne voudrais pas faire a fortement tué le désir:

Création d'un contrôleur qui serait chargé d'autoriser et d'envoyer des données à l'utilisateur;

Création d'une nouvelle entité Utilisateur, ainsi que d'un gestionnaire pour celle-ci;

Pousser la logique soit dans le contrôleur lui-même, ce qui le rendrait gonflé, soit dans une classe distincte;

Réécrire la logique de travailler avec la "base de données", car maintenant ou deux fichiers csv, ou allez dans la base de données ...



En conséquence, nous avons obtenu un grand monolithe, dont l'expansion est très «douloureuse». Il a un grand ensemble de liens étroits dans l'application. Un objet étroitement lié dépend d'un autre objet; cela signifie que changer un objet dans une application étroitement couplée nécessite souvent de changer un certain nombre d'autres objets. Ce n'est pas difficile lorsque l'application est petite, mais l'application de niveau entreprise est trop difficile à modifier.



Les liens faibles signifient que deux objets sont indépendants et qu'un objet peut utiliser l'autre sans en dépendre. Ce type de relation vise à réduire les interdépendances entre les composants du système afin de réduire le risque que des changements dans un composant nécessitent des changements dans tout autre composant.



Référence historique
«» .

« » 2008 . , , , . — , , , .







Par conséquent, nous allons essayer d'implémenter notre application dans le style Onion pour montrer les avantages de cette méthode.



L'architecture Onion est la division d'une application en couches. De plus, il y a un niveau indépendant, qui est au centre de l'architecture.



L'architecture Onion repose fortement sur l'inversion de dépendance. L'interface utilisateur interagit avec la logique métier via des interfaces.



Principe d'inversion de dépendance
(Dependency Inversion Principle) , , . :

. .

. .






Un projet classique de ce style comporte quatre couches:

  • Niveau d'objet de domaine (noyau)
  • Niveau du référentiel (Repo)
  • Niveau de service
  • Couche frontale (test Web / unitaire) (Api)




Toutes les couches sont dirigées vers le centre (Core). Le centre est indépendant.



Niveau d'objet de domaine



Il s'agit de la partie centrale de l'application qui décrit les objets qui fonctionnent avec la base de données.



Créons un nouveau projet dans la solution, qui aura le type de sortie "Bibliothèque de classes". Je l'ai nommé WebApiTest.Core



Créons une classe BaseEntity qui aura des propriétés communes d'objets.

BaseEntity.cs
    public class BaseEntity
    {
        public int id { get; set; }
    }
      
      







Hors sujet
, «id», , dateAdded, dateModifed ..



Ensuite, créons une classe Book qui hérite de BaseEntity

Book.cs
public class Book: BaseEntity

{

public string name { get; set; }

public string author { get; set; }

public int pages { get; set; }

public int readedPages { get; set; }

}





Pour notre application, cela suffira pour l'instant, alors passons au niveau suivant.



Niveau du référentiel



Passons maintenant à l'implémentation du niveau du référentiel. Créer un projet de bibliothèque de classes appelé WebApiTest.Repo

Nous utiliserons l' injection de dépendances, nous passerons donc des paramètres via le constructeur pour les rendre plus flexibles. Ainsi, nous créons une interface de référentiel commune pour les opérations d'entité afin de pouvoir développer une application faiblement couplée. L'extrait de code ci-dessous concerne l'interface IRepository.

IRepository.cs
    public interface IRepository <T> where T : BaseEntity
    {
        IEnumerable<T> GetAll();
        int Add(T item);
        T Get(int id);
        void Update(T item);
        void Delete(T item);
        void SaveChanges();
    }
      
      











Maintenant, implémentons une classe de référentiel pour effectuer des opérations de base de données sur une entité qui implémente IRepository. Ce référentiel contient un constructeur avec un paramètre pathToBase, donc lorsque nous instancions le référentiel, nous transmettons le chemin du fichier afin que la classe sache où récupérer les données.

CsvRepository.cs
public class CsvRepository<T> : IRepository<T> where T : BaseEntity
    {
        private List<T> list;
        private string dbPath;
        private CsvConfiguration cfg = new CsvConfiguration(CultureInfo.InvariantCulture)
        {
            HasHeaderRecord = false,
            Delimiter = ";"
        };
        public CsvRepository(string pathToBase)
        {
            dbPath = pathToBase;
            using (var reader = new StreamReader(pathToBase)) {
                using (var csv = new CsvReader(reader, cfg)) {
                    list = csv.GetRecords<T>().ToList(); }
            }
        }

        public int Add(T item)
        {
            if (item == null)
                throw new Exception("Item is null");
            var maxId = list.Max(x => x.id);
            item.id = maxId + 1;
            list.Add(item);
            return item.id;
        }

        public void Delete(T item)
        {
            if (item == null)
                throw new Exception("Item is null");
            list.Remove(item);
        }

        public T Get(int id)
        {
            return list.SingleOrDefault(x => x.id == id);
        }

        public IEnumerable<T> GetAll()
        {
            return list;
        }

        public void SaveChanges()
        {
            using (TextWriter writer = new StreamWriter(dbPath, false, System.Text.Encoding.UTF8))
            {
                using (var csv = new CsvWriter(writer, cfg))
                {
                    csv.WriteRecords(list);
                }
            }
        }

        public void Update(T item)
        {
            if(item == null)
                throw new Exception("Item is null");
            var dbItem = list.SingleOrDefault(x => x.id == item.id);
            if (dbItem == null)
                throw new Exception("Cant find same item");
            dbItem = item;
        }
      
      









Nous avons développé l'entité et le contexte nécessaires pour travailler avec la base de données.



Niveau de service



Nous créons maintenant la troisième couche de l'architecture de l'oignon, qui est la couche de service. Je l'ai nommé WebApiText.Service. Cette couche interagit avec les applications Web et les projets de référentiel.



Nous créons une interface nommée IBookService. Cette interface contient la signature de toutes les méthodes auxquelles accède la couche externe sur l'objet Book.

IBookService.cs

public interface IBookService
    {
        IEnumerable<Book> GetBooks();
        Book GetBook(int id);
        void DeleteBook(Book book);
        void UpdateBook(Book book);
        void DeleteBook(int id);
        int AddBook(Book book);
    }
      
      







Maintenant, implémentons-le dans la classe BookService

BookService.cs

public class BookService : IBookService
    {
        private IRepository<Book> bookRepository;
        public BookService(IRepository<Book> bookRepository)
        {
            this.bookRepository = bookRepository;
        }

        public int  AddBook(Book book)
        {
            return bookRepository.Add(book);
        }

        public void DeleteBook(Book book)
        {
            bookRepository.Delete(book);
        }
        public void DeleteBook(int id)
        {
            var book = bookRepository.Get(id);
            bookRepository.Delete(book);
        }

        public Book GetBook(int id)
        {
            return bookRepository.Get(id);
        }

        public IEnumerable<Book> GetBooks()
        {
            return bookRepository.GetAll();
        }

        public void UpdateBook(Book book)
        {
            bookRepository.Update(book);
        }
    }

      
      









Niveau d'interface externe





Nous créons maintenant la dernière couche de l'architecture oignon, qui, dans notre cas, est l'interface externe, avec laquelle les applications externes (bot, bureau, etc.) vont interagir. Pour créer cette couche, nous nettoyons notre projet WebApiTest.Api en supprimant la classe Book et en nettoyant le BooksController. Ce projet offre une opportunité d'opérations avec la base de données d'entités, ainsi qu'un contrôleur pour effectuer ces opérations.



Étant donné que le concept d'injection de dépendances est au cœur d'une application ASP.NET Core, nous devons maintenant enregistrer tout ce que nous avons créé pour une utilisation dans l'application.



Injection de dépendance



Dans les petites applications ASP.NET MVC, nous pouvons remplacer relativement facilement certaines classes par d'autres, au lieu d'utiliser un contexte de données, en utiliser un autre. Cependant, dans les grandes applications, cela sera déjà problématique à faire, surtout si nous avons des dizaines de contrôleurs avec des centaines de méthodes. Dans cette situation, un mécanisme tel que l'injection de dépendances peut nous venir en aide.



Et si plus tôt dans ASP.NET 4 et d'autres versions précédentes, il était nécessaire d'utiliser divers conteneurs IoC externes pour installer des dépendances, telles que Ninject, Autofac, Unity, le château de Windsor, StructureMap, alors ASP.NET Core a déjà un conteneur d'injection de dépendances intégré, qui représenté par l'interface IServiceProvider. Et les dépendances elles-mêmes sont également appelées services, c'est pourquoi le conteneur peut être appelé un fournisseur de services. Ce conteneur est responsable du mappage des dépendances vers des types spécifiques et de l'injection de dépendances dans divers objets.



Au tout début, nous avons utilisé la liaison matérielle pour utiliser CsvDB dans le contrôleur.

private CsvDB db;

        public BookController()
        {
            db = new CsvDB();
        }
      
      





À première vue, il n'y a rien de mal à cela, mais, par exemple, le schéma de connexion à la base de données a changé: au lieu de Csv, j'ai décidé d'utiliser MongoDB ou MySql. De plus, vous devrez peut-être changer dynamiquement d'une classe à une autre.



Dans ce cas, un lien physique lie le contrôleur à une implémentation spécifique du référentiel. Ce code est plus difficile à maintenir et à tester à mesure que votre application se développe. Par conséquent, il est recommandé de ne plus utiliser de composants à couplage rigide au profit de composants à couplage lâche.



À l'aide de diverses techniques d'injection de dépendances, vous pouvez gérer le cycle de vie des services que vous créez. Les services générés par l'injection de dépendances peuvent être de l'un des types suivants:



  • Transient: . , . ,
  • Scoped: . , .
  • Singleton: ,


Les méthodes AddTransient (), AddScoped () et AddSingleton () correspondantes sont utilisées pour créer chaque type de service dans le conteneur principal .net intégré.



Nous pourrions utiliser un conteneur standard (fournisseur de services), mais il ne prend pas en charge le passage de paramètres, je vais donc devoir utiliser la bibliothèque Autofac.



Pour ce faire, ajoutez deux packages au projet via NuGet: Autofac et Autofac.Extensions.DependencyInjection.

Maintenant, nous changeons la méthode ConfigureServices dans le fichier Startup.cs en:

Configurer les services
 public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            var builder = new ContainerBuilder();// 
            builder.RegisterType<CsvRepository<Book>>()// CsvRepository
                .As<IRepository<Book>>() //  IRepository
                .WithParameter("pathToBase", @"C:\csv\books.csv")//  pathToBase
                .InstancePerLifetimeScope(); //Scope
            builder.RegisterType<BookService>()
                .As<IBookService>()
                .InstancePerDependency(); //Transient 
            builder.Populate(services); // 
            var container = builder.Build();
            return new AutofacServiceProvider(container);
        }
      
      









De cette façon, nous avons lié toutes les implémentations à leurs interfaces.



Revenons à notre projet WebApiTest.Api.

Il ne reste plus qu'à changer BooksController.cs

BooksController.cs
[Route("[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private IBookService service;
        public BooksController(IBookService service)
        {
            this.service = service;
        }

        [HttpGet]
        public ActionResult<IEnumerable<Book>> Get()
        {
            return new JsonResult(service.GetBooks());
        }

        [HttpGet("{id}")]
        public ActionResult<Book> Get(int id)
        {
            return new JsonResult(service.GetBook(id));
        }

        [HttpPost]
        public void Post([FromBody] Book item)
        {
            service.AddBook(item);
        }

        [HttpPut("{id}")]
        public void Put([FromBody] Book item)
        {
            service.UpdateBook(item);
        }

        [HttpDelete("{id}")]
        public void Delete(int id)
        {
            service.DeleteBook(id);
        }
    }
      
      









Appuyez sur F5, attendez que le navigateur s'ouvre, accédez à / books et ...

[{"name":"Test","author":"Test","pages":100,"readedPages":0,"id":1}]
      
      







Résultat:



Dans ce texte, je souhaitais mettre à jour toutes mes connaissances sur le modèle architectural Onion, ainsi que sur l'injection de dépendances, toujours en utilisant Autofac.

Je pense que l'objectif est atteint, merci d'avoir lu;)



n-Tier
n- .

— . . , .

. ( ). , . . , - .




All Articles