Além disso, tentarei levar em consideração os comentários da primeira parte,
Arquitetura cebola
Suponha que estejamos projetando um aplicativo para registrar quais livros lemos, mas para precisão, queremos registrar até quantas páginas foram lidas. Sabemos que este é um programa pessoal que precisamos em nosso smartphone, como um bot para telegramas e, possivelmente, para desktop, então fique à vontade para escolher esta opção de arquitetura:
(Tg Bot, Phone App, Desktop) => Asp.net Web Api => Banco de Dados
Crie um projeto no Visual Studio do tipo Asp.net Core, onde posteriormente selecionamos o tipo de projeto Web Api.
Como é diferente do normal?
Primeiro, a classe do controlador herda da classe ControllerBase, que é projetada para ser a base para MVC sem suporte para retornar visualizações (código html).
Em segundo lugar, ele foi projetado para implementar serviços REST cobrindo todos os tipos de solicitações HTTP e, em resposta às solicitações, você recebe json com uma indicação explícita do status da resposta. Além disso, você verá que o controlador padrão será marcado com o atributo [ApiController], que possui opções úteis especificamente para a API.
Agora você precisa decidir como armazenar os dados. Como sei que não leio mais que 12 livros por ano, o arquivo csv será suficiente para mim, que representará o banco de dados.
Então eu crio uma classe que descreve o livro:
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; }
}
}
E então descrevo a aula para trabalhar com o banco de dados:
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;
}
}
}
Então a questão é pequena, adicionar a API para poder interagir com ela:
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);
}
}
E então tudo o que resta é adicionar a IU, o que seria conveniente. E tudo funciona!
Legal! Mas não, a esposa pediu que ela também tivesse acesso a uma coisa tão conveniente.
Que dificuldades nos esperam? Primeiro, agora você precisa adicionar uma coluna para todos os livros que indicará o ID do usuário. Confie em mim, não será confortável com um arquivo csv. Além disso, agora você precisa adicionar os próprios usuários! E mesmo agora algum tipo de lógica é necessária para que minha esposa não veja que estou terminando de ler a terceira coleção de Dontsova em vez do prometido Tolstoi.
Vamos tentar expandir este projeto para os requisitos exigidos: A
capacidade de criar uma conta de usuário, que será capaz de armazenar uma lista de seus livros e adicionar quantos ele leu.
Honestamente, eu queria escrever um exemplo, mas a quantidade de coisas que eu não gostaria de fazer matou fortemente o desejo:
Criação de um controlador que seria responsável por autorizar e enviar dados ao usuário;
Criação de uma nova entidade User, bem como de um handler para ela;
Empurrar a lógica para o próprio controlador, o que o tornaria inchado, ou para uma classe separada;
Reescrevendo a lógica de trabalhar com o "banco de dados", porque agora ou dois arquivos csv, ou vá para o banco de dados ...
Como resultado, obtivemos um grande monólito, cuja expansão é muito “dolorosa”. Ele tem um grande conjunto de links estreitos no aplicativo. Um objeto fortemente limitado depende de outro objeto; isso significa que alterar um objeto em um aplicativo fortemente acoplado geralmente requer a alteração de vários outros objetos. Isso não é difícil quando o aplicativo é pequeno, mas é muito difícil fazer alterações em um aplicativo de nível empresarial.
Os laços fracos significam que dois objetos são independentes e um objeto pode usar o outro sem ser dependente dele. Esse tipo de relacionamento visa reduzir as interdependências entre os componentes do sistema, a fim de reduzir o risco de que mudanças em um componente venham a exigir mudanças em qualquer outro componente.
Referência da história
«» .
« » 2008 . , , , . — , , , .
« » 2008 . , , , . — , , , .
Portanto, tentaremos implementar nosso aplicativo no estilo Onion para mostrar as vantagens deste método.
A arquitetura Onion é a divisão de um aplicativo em camadas. Além disso, existe um nível independente, que fica no centro da arquitetura.
A arquitetura Onion depende muito da Inversão de Dependências. A interface do usuário interage com a lógica de negócios por meio de interfaces.
Princípio de Inversão de Dependência
(Dependency Inversion Principle) , , . :
. .
. .
. .
. .
Um projeto clássico neste estilo tem quatro camadas:
- Nível de objeto de domínio (núcleo)
- Nível do repositório (Repo)
- Nível de serviço
- Camada de front-end (Web / Teste de unidade) (Api)
Todas as camadas são direcionadas para o centro (Core). O centro é independente.
Nível de objeto de domínio
Esta é a parte central do aplicativo que descreve os objetos que funcionam com o banco de dados.
Vamos criar um novo projeto na solução, que terá o tipo de saída "Class Library". Eu o chamei de WebApiTest.Core.
Vamos criar uma classe BaseEntity que terá propriedades comuns de objetos.
BaseEntity.cs
public class BaseEntity
{
public int id { get; set; }
}
Fora do topo
, «id», , dateAdded, dateModifed ..
A seguir, vamos criar uma classe Book que herda 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 nosso aplicativo, isso será o suficiente por enquanto, então vamos passar para o próximo nível.
Nível de repositório
Agora, vamos prosseguir para a implementação do nível do repositório. Crie um projeto de Biblioteca de Classes denominado WebApiTest.Repo
Usaremos injeção de dependência, portanto, passaremos parâmetros pelo construtor para torná-los mais flexíveis. Assim, criamos uma interface de repositório comum para operações de entidade para que possamos desenvolver um aplicativo fracamente acoplado. O fragmento de código a seguir é para a 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();
}
Agora, vamos implementar uma classe de repositório para realizar operações de banco de dados em uma entidade que implementa IRepository. Este repositório contém um construtor com um parâmetro pathToBase, então quando instanciamos o repositório, passamos o caminho do arquivo para que a classe saiba de onde buscar os dados.
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;
}
Desenvolvemos a entidade e o contexto necessários para trabalhar com o banco de dados.
Nível de serviço
Agora estamos criando a terceira camada da arquitetura cebola, que é a camada de serviço. Eu o chamei de WebApiText.Service. Essa camada interage com aplicativos da web e projetos de repositório.
Criamos uma interface chamada IBookService. Essa interface contém a assinatura de todos os métodos acessados pela camada externa no objeto Livro.
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);
}
Agora vamos implementá-lo na 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);
}
}
Nível de interface externa
Agora criamos a última camada da arquitetura onion, que, no nosso caso, é a interface externa com a qual os aplicativos externos (bot, desktop, etc.) irão interagir. Para criar essa camada, limpamos nosso projeto WebApiTest.Api removendo a classe Book e limpando BooksController. Este projeto oferece uma oportunidade para operações com o banco de dados de entidades, bem como um controlador para realizar essas operações.
Como o conceito de injeção de dependência é central para um aplicativo ASP.NET Core, agora precisamos registrar tudo o que criamos para uso no aplicativo.
Injeção de dependência
Em pequenos aplicativos ASP.NET MVC, podemos substituir com relativa facilidade uma classe por outra, em vez de usar um contexto de dados, usar outro. No entanto, em grandes aplicações, isso já será problemático de fazer, especialmente se tivermos dezenas de controladores com centenas de métodos. Nessa situação, um mecanismo como injeção de dependência pode vir em nosso auxílio.
E se antes no ASP.NET 4 e em outras versões anteriores fosse necessário usar vários contêineres IoC externos para instalar dependências, como Ninject, Autofac, Unity, Castelo de Windsor, StructureMap, então o ASP.NET Core já tem um contêiner de injeção de dependência embutido, que representado pela interface IServiceProvider. E as próprias dependências também são chamadas de serviços, por isso o contêiner pode ser chamado de provedor de serviços. Este contêiner é responsável por mapear dependências para tipos específicos e por injetar dependências em vários objetos.
No início, usamos hard linking para usar CsvDB no controlador.
private CsvDB db;
public BookController()
{
db = new CsvDB();
}
À primeira vista, não há nada de errado com isso, mas, por exemplo, o esquema de conexão do banco de dados mudou: em vez de Csv, decidi usar MongoDB ou MySql. Além disso, pode ser necessário alterar dinamicamente uma classe para outra.
Nesse caso, um link físico liga o controlador a uma implementação específica do repositório. Este código é mais difícil de manter e mais difícil de testar à medida que seu aplicativo cresce. Portanto, é recomendável deixar de usar componentes rigidamente acoplados por componentes fracamente acoplados.
Usando uma variedade de técnicas de injeção de dependência, você pode gerenciar o ciclo de vida dos serviços que você cria. Os serviços gerados por Depedency Injection podem ser de um dos seguintes tipos:
- Transient: . , . ,
- Scoped: . , .
- Singleton: ,
Os métodos AddTransient (), AddScoped () e AddSingleton () correspondentes são usados para criar cada tipo de serviço no contêiner de núcleo .net incorporado.
Poderíamos usar um contêiner padrão (provedor de serviços), mas ele não suporta passagem de parâmetro, então terei que usar a biblioteca Autofac.
Para fazer isso, adicione dois pacotes ao projeto via NuGet: Autofac e Autofac.Extensions.DependencyInjection.
Agora alteramos o método ConfigureServices no arquivo Startup.cs para:
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);
}
Desta forma, vinculamos todas as implementações às suas interfaces.
Vamos voltar ao nosso projeto WebApiTest.Api.
Tudo o que resta é mudar 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);
}
}
Pressione F5, espere o navegador abrir, vá para / books e ...
[{"name":"Test","author":"Test","pages":100,"readedPages":0,"id":1}]
Resultado:
Neste texto, quis atualizar todo o meu conhecimento sobre o padrão arquitetônico Onion, bem como sobre injeção de dependências, usando Autofac.
Acho que o objetivo foi alcançado, obrigado pela leitura;)
n-Tier
n- .
— . . , .
. ( ). , . . , - .
— . . , .
. ( ). , . . , - .