Además, intentaré tener en cuenta los comentarios de la primera parte,
Arquitectura de cebolla
Supongamos que estamos diseñando una aplicación para registrar qué libros hemos leído, pero para mayor precisión, queremos registrar incluso cuántas páginas se han leído. Sabemos que este es un programa personal que necesitamos en nuestro teléfono inteligente, como un bot para telegramas y, posiblemente, para escritorio, así que siéntase libre de elegir esta opción de arquitectura:
(Tg Bot, Phone App, Desktop) => Asp.net Web Api => Base de datos
Cree un proyecto en Visual studio del tipo Asp.net Core, donde además seleccionamos el tipo de proyecto Web Api.
¿En qué se diferencia del habitual?
Primero, la clase de controlador hereda de la clase ControllerBase, que está diseñada para ser la base para MVC sin soporte para devolver vistas (código html).
En segundo lugar, está diseñado para implementar servicios REST que cubren todo tipo de solicitudes HTTP y, en respuesta a las solicitudes, recibe json con una indicación explícita del estado de la respuesta. Además, verá que el controlador predeterminado estará marcado con el atributo [ApiController], que tiene opciones útiles específicamente para la API.
Ahora debe decidir cómo almacenar los datos. Como sé que no leo más de 12 libros al año, el archivo csv será suficiente para mí, que representará la base de datos.
Entonces creo una clase que describe el libro:
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; }
}
}
Y luego describo la clase para trabajar con la base de datos:
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;
}
}
}
Entonces el asunto es pequeño, agregar la API para poder interactuar con ella:
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);
}
}
Y luego todo lo que queda es agregar la interfaz de usuario, que sería conveniente. ¡Y todo funciona!
¡Frio! Pero no, la esposa pidió que ella también tuviera acceso a algo tan conveniente.
¿Qué dificultades nos esperan? Primero, ahora debe agregar una columna para todos los libros que indicará la identificación del usuario. Créame, no se sentirá cómodo con un archivo csv. Además, ¡ahora debe agregar los propios usuarios! E incluso ahora se necesita algún tipo de lógica para que mi esposa no vea que estoy terminando de leer la tercera colección de Dontsova en lugar del prometido Tolstoi.
Intentemos expandir este proyecto a los requisitos requeridos: La
capacidad de crear una cuenta de usuario, que podrá mantener una lista de sus libros y agregar cuántos leyó.
Honestamente, quería escribir un ejemplo, pero la cantidad de cosas que no quisiera hacer mató drásticamente el deseo:
Creación de un controlador que se encargaría de autorizar y enviar datos al usuario;
Creación de una nueva entidad Usuario, así como un manejador para la misma;
Empujar la lógica en el propio controlador, lo que lo hincharía, o en una clase separada;
Reescribiendo la lógica de trabajar con la "base de datos", porque ahora o dos archivos csv, o ir a la base de datos ...
Como resultado, obtuvimos un gran monolito, que es muy "doloroso" de expandir. Tiene un gran conjunto de vínculos estrechos en la aplicación. Un objeto fuertemente ligado depende de otro objeto; esto significa que cambiar un objeto en una aplicación estrechamente acoplada a menudo requiere cambiar varios otros objetos. Esto no es difícil cuando la aplicación es pequeña, pero la aplicación de nivel empresarial es demasiado difícil para realizar cambios.
Los lazos débiles significan que dos objetos son independientes y un objeto puede utilizar al otro sin depender de él. Este tipo de relación tiene como objetivo reducir las interdependencias entre los componentes del sistema para reducir el riesgo de que los cambios en un componente requieran cambios en cualquier otro componente.
Por tanto, intentaremos implementar nuestra aplicación al estilo Cebolla para mostrar las ventajas de este método.
La arquitectura de cebolla es la división de una aplicación en capas. Además, hay un nivel independiente, que está en el centro de la arquitectura.
La arquitectura de cebolla se basa en gran medida en la inversión de dependencias. La interfaz de usuario interactúa con la lógica empresarial a través de interfaces.
Principio de inversión de dependencia
(Dependency Inversion Principle) , , . :
. .
. .
. .
. .
Un proyecto clásico en este estilo tiene cuatro capas:
- Nivel de objeto de dominio (núcleo)
- Nivel de repositorio (Repo)
- Nivel de servicio
- Capa de interfaz (Web / Prueba unitaria) (Api)
Todas las capas están dirigidas hacia el centro (Core). El centro es independiente.
Nivel de objeto de dominio
Esta es la parte central de la aplicación que describe los objetos que trabajan con la base de datos.
Creemos un nuevo proyecto en la solución, que tendrá el tipo de salida "Biblioteca de clases". Lo llamé WebApiTest.Core.
Creemos una clase BaseEntity que tendrá propiedades comunes de los objetos.
BaseEntity.cs
public class BaseEntity
{
public int id { get; set; }
}
Por encima
, «id», , dateAdded, dateModifed ..
A continuación, creemos una clase Book que herede 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; }
}
{
public string name { get; set; }
public string author { get; set; }
public int pages { get; set; }
public int readedPages { get; set; }
}
Para nuestra aplicación, esto será suficiente por ahora, así que pasemos al siguiente nivel.
Nivel de repositorio
Ahora pasemos a implementar el nivel de repositorio. Cree un proyecto de biblioteca de clases llamado WebApiTest.Repo.
Usaremos la inyección de dependencia, por lo que pasaremos parámetros a través del constructor para hacerlos más flexibles. Por lo tanto, creamos una interfaz de repositorio común para las operaciones de la entidad de modo que podamos desarrollar una aplicación poco acoplada. El siguiente fragmento de código es para la interfaz de 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();
}
Ahora, implementemos una clase de repositorio para realizar operaciones de base de datos en una entidad que implementa IRepository. Este repositorio contiene un constructor con un parámetro pathToBase, por lo que cuando creamos una instancia del repositorio, pasamos la ruta del archivo para que la clase sepa de dónde obtener los datos.
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;
}
Hemos desarrollado la entidad y el contexto necesarios para trabajar con la base de datos.
Nivel de servicio
Ahora estamos creando la tercera capa de la arquitectura de cebolla, que es la capa de servicio. Lo llamé WebApiText.Service. Esta capa interactúa tanto con aplicaciones web como con proyectos de repositorio.
Creamos una interfaz llamada IBookService. Esta interfaz contiene la firma de todos los métodos a los que accede la capa externa en el objeto Libro.
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);
}
Ahora implementémoslo en la clase 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);
}
}
Nivel de interfaz externa
Ahora creamos la última capa de la arquitectura cebolla, que, en nuestro caso, es la interfaz externa, con la que interactuarán las aplicaciones externas (bot, escritorio, etc.). Para crear esta capa, limpiamos nuestro proyecto WebApiTest.Api eliminando la clase Book y limpiando BooksController. Este proyecto brinda una oportunidad para las operaciones con la base de datos de la entidad, así como un controlador para realizar estas operaciones.
Dado que el concepto de inyección de dependencia es fundamental para una aplicación ASP.NET Core, ahora necesitamos registrar todo lo que hemos creado para usar en la aplicación.
Inyección de dependencia
En aplicaciones pequeñas en ASP.NET MVC, podemos reemplazar con relativa facilidad una clase por otra, en lugar de usar un contexto de datos, usar otro. Sin embargo, en aplicaciones grandes esto ya será problemático, especialmente si tenemos docenas de controladores con cientos de métodos. En esta situación, un mecanismo como la inyección de dependencia puede venir en nuestra ayuda.
Y si anteriormente en ASP.NET 4 y otras versiones anteriores era necesario usar varios contenedores IoC externos para instalar dependencias, como Ninject, Autofac, Unity, Windsor Castle, StructureMap, entonces ASP.NET Core ya tiene un contenedor de inyección de dependencias incorporado, que representado por la interfaz IServiceProvider. Y las dependencias en sí mismas también se denominan servicios, por lo que el contenedor puede denominarse proveedor de servicios. Este contenedor es responsable de asignar dependencias a tipos específicos y de inyectar dependencias en varios objetos.
Al principio, usamos enlaces duros para usar CsvDB en el controlador.
private CsvDB db;
public BookController()
{
db = new CsvDB();
}
A primera vista, esto no tiene nada de malo, pero, por ejemplo, el esquema de conexión de la base de datos ha cambiado: en lugar de Csv, decidí usar MongoDB o MySql. Además, es posible que deba cambiar dinámicamente una clase a otra.
En este caso, un enlace físico vincula al controlador a una implementación específica del repositorio. Este código es más difícil de mantener y de probar a medida que crece la aplicación. Por lo tanto, se recomienda dejar de usar componentes acoplados rígidamente a componentes acoplados débilmente.
Usando una variedad de técnicas de inyección de dependencia, puede administrar el ciclo de vida de los servicios que crea. Los servicios generados por Depedency Injection pueden ser de uno de los siguientes tipos:
- Transient: . , . ,
- Scoped: . , .
- Singleton: ,
Los métodos AddTransient (), AddScoped () y AddSingleton () correspondientes se utilizan para crear cada tipo de servicio en el contenedor principal .net integrado.
Podríamos usar un contenedor estándar (proveedor de servicios), pero no admite el paso de parámetros, así que tendré que usar la biblioteca Autofac.
Para hacer esto, agregue dos paquetes al proyecto a través de NuGet: Autofac y Autofac.Extensions.DependencyInjection.
Ahora cambiamos el método ConfigureServices en el archivo Startup.cs a:
ConfigureServices
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 esta forma hemos vinculado todas las implementaciones a sus interfaces.
Volvamos a nuestro proyecto WebApiTest.Api.
Todo lo que queda es cambiar 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);
}
}
Presione F5, espere a que se abra el navegador, vaya a / books y ...
[{"name":"Test","author":"Test","pages":100,"readedPages":0,"id":1}]
Salir:
En este texto, quería actualizar todos mis conocimientos sobre el patrón arquitectónico Onion, así como sobre la inyección de dependencias, usando Autofac.
Creo que el objetivo está logrado, gracias por leer;)
n-nivel
n- .
— . . , .
. ( ). , . . , - .
— . . , .
. ( ). , . . , - .