ASP .NET Core - Implementando a Onion Architecture - IV


Neste artigo vamos criar uma aplicação ASP .NET Core WEBAPI fazendo uma implementação básica da arquitetura em Cebola ou Onion Architecture.

Continuando a terceira parte do artigo vamos iniciar a implementação da camada Infrastructure no projeto eStore.Persistence.

Implementação da camada Infrastructure : projeto eStore.Persistence

A camada Infrastructure é a camada mais externa da arquitetura cebola que lida com as necessidades de infraestrutura e fornece a implementação de suas interfaces de repositório. É aqui que conectamos a lógica de acesso a dados ou a lógica de registro ou a lógica de chamadas de serviço. Apenas a camada de infraestrutura sabe sobre o banco de dados e a tecnologia de acesso a dados (Entity framework ou Ado.net) e outras camadas não sabem nada sobre de onde vêm os dados e como estão sendo armazenados.

Nesta camada temos os seguintes projetos:   

  1. Identity - Onde definimos as configurações se os recursos de autenticação e autorização relacionados com o Identity;
  2. Persistence - Onde definimos o contexto e suas configurações, implementamos os repositórios e aplicamos o Migrations;
  3. Shared - Onde definimos os registros dos serviços e a injeção de dependência;

Vamos iniciar incluindo as referências aos seguintes pacotes do EF Core neste projeto:

Para incluir os pacotes use o menu Tools->..-> Manage Nuget Packages for Solution e na guia Browse selecione e instale os pacotes ou abra a janela Package Manager Console e digite o comando: install-package <nome-pacote>

A versão atual de todos os pacotes usados é a versão 5.0.4.

A seguir vamos criar 3 pastas no projeto Persistence :

Definindo o contexto e configurando as entidades

Abra a pasta Context e crie nesta pasta a classe ApplicationDbContext que herda de DbContext.

Nesta classe vamos definir as opções do Contexto usando DbContextOptions e passando as opções para a classe base. As definições são feitas quando do registro do serviço do contexto no arquivo Startup no método ConfigureServices onde definimos o provedor do banco de dados e a string de conexão.

Nesta classe definimos o mapeamento entre as entidade do domínio e o banco de dados usando a classe DbSet<T>.

No método OnModelCreating vamos usar o método ApplyConfigurationsFromAssembly para aplicar algumas configurações que iremos fazer nas entidades Product e Category definidas neste assembly.

Essas configurações devem ser criadas em um arquivo separado e implementar a interface IEntityTypeConfiguration<T>.

Nota:  O método OnModelCreating é chamado quando seu contexto é criado pela primeira vez para construir o modelo e seus mapeamentos na memória;

1- ApplicationDbContext

using eStore.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace eStore.Persistence.Context
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext>
            options)
            : base(options)
        {
        }
        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
        }
    }
}

A seguir vamos criar a pasta EntityConfiguration no projeto e nesta classe vamos criar as classes onde vamos definir as configurações usando a Fluent API para as entidades do contexto.

Vamos começar com a entidade Category criando a classe CategoryConfiguration que implementa a interface IEntityTypeConfiguration<Category>

2- CategoryConfiguration

using eStore.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace eStore.Persistence.EntityConfiguration
{
    public class CategoryConfiguration : IEntityTypeConfiguration<Category>
    {
        public void Configure(EntityTypeBuilder<Category> builder)
        {
            builder.HasKey(t => t.Id);
            builder.Property(p => p.Name).HasMaxLength(100).IsRequired();
            builder.HasData(
              new Category(1, "Material Escolar"),
              new Category(2, "Eletrônicos"),
              new Category(3, "Acessórios")
           );
        }
    }
}

Na implementação do método Configure usamos uma instância de EntityTypeBuilder<T> para definir as configurações que o EF Core vai usar quando aplicar o Migrations e também usamos a propriedade HasData que será usada para popular a tabela Category no banco de dados com os dados definidos.

Vamos repetir esse procedimento criando a classe ProductConfiguration:

using eStore.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace eStore.Persistence.EntityConfiguration
{
    public class ProductConfiguration : IEntityTypeConfiguration<Product>
    {
        public void Configure(EntityTypeBuilder<Product> builder)
        {
            builder.HasKey(t => t.Id);
            builder.Property(p => p.Name).HasMaxLength(100).IsRequired();
            builder.Property(p => p.Description).HasMaxLength(200).IsRequired();
            builder.Property(p => p.Price).HasPrecision(10, 2);

            builder.HasOne(e => e.Category).WithMany(e => e.Products).HasForeignKey(e => e.CategoryId);    
        }
    }
}

Aqui neste código estamos definindo as configurações para algumas propriedades da entidade Product que serão usadas pelo EF Core na aplicação do Migrations e geração da tabela Product no banco de dados.

Usamos também o método HasOne da Fluent API do EF Core para configurar um lado de um relacionamento um-para- muitos ou uma extremidade de um relacionamento um para um.

Para configurar totalmente um relacionamento válido, é necessário seguir o padrão Has/With e emparelhar o uso de HasOne com o método WithMany onde especificamos a chave estrangeira como sendo CategoryId.

Implementando os repositórios para Category e Product

Os repositórios  destinam-se a criar uma camada de abstração entre a camada de acesso a dados e a camada de lógica de negócios de um aplicativo. A implementação desses padrões pode ajudar a isolar o aplicativo de alterações no armazenamento de dados e pode facilitar o teste de unidade automatizado ou TDD (desenvolvimento orientado por testes).

Neste projeto vamos implementar as classes concretas do repositório para cada tipo de entidade. Para a entidade Category e para a entidade Product, visto que já definimos as respectivas interfaces na camada Domain. Assim ao criar uma instância do repositório, vamos usar a interface para passar uma referência a qualquer objeto que implemente a interface do repositório.

A principal justificativa que eu vou dar para não implementar um repositório genérico é a seguinte:

"Um repositório é parte de um domínio que esta sendo modelado e um domínio não é genérico, Nem todas as entidades podem ser excluídas, nem todas as entidades podem ser adicionadas, nem todas as entidades têm um repositório. As consultas variam muito; a API do repositório torna-se tão única quanto a própria entidade."

Abra a pasta Repositories do projeto e nesta pasta crie a classe CategoryRepository que vai implementar a interface ICategoryRepository criada na camada Domain.

1- CategoryRepository

using eStore.Domain.Entities;
using eStore.Domain.Interfaces;
using eStore.Persistence.Context;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace eStore.Persistence.Repositories
{
    public class CategoryRepository : ICategoryRepository
    {
        private ApplicationDbContext _categoryContext;
        public CategoryRepository(ApplicationDbContext context)
        {
            _categoryContext = context;
        }
        public async Task<IEnumerable<Category>> GetCategories()
        {
            return await _categoryContext.Categories.ToListAsync();
        }
        public async Task<Category> GetById(int? id)
        {
            return await _categoryContext.Categories.FindAsync(id);
        }
        public async Task<Category> Create(Category product)
        {
            _categoryContext.Add(product);
            var result = await _categoryContext.SaveChangesAsync();
            return product;
        }

        public async Task<Category> Update(Category product)
        {
            _categoryContext.Update(product);
            await _categoryContext.SaveChangesAsync();
            return product;
        }

        public async Task<Category> Remove(Category product)
        {
            _categoryContext.Remove(product);
            await _categoryContext.SaveChangesAsync();
            return product;
        }
    }

No construtor da classe CategoryRepository injetamos uma instância do nosso contexto - ApplicationDbContext - e assim poderemos acessar os dados da nossa aplicação via contexto e realizar as consultas e persistência dos dados.

Implementamos a seguir o contrato definido na interface para os métodos que permite acessar, incluir, alterar e excluir informações. Fizemos isso criando métodos assíncronos usando Task/async e await.

Agora vamos realizar a implementação do repositório para produtos criando a classe ProductRepository:

2- ProductRepository

using eStore.Domain.Entities;
using eStore.Domain.Interfaces;
using eStore.Persistence.Context;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace eStore.Persistence.Repositories
{
    public class ProductRepository : IProductRepository
    {
        private ApplicationDbContext _productContext;
        public ProductRepository(ApplicationDbContext context)
        {
            _productContext = context;
        }
        public async Task<IEnumerable<Product>> GetProducts()
        {
            return await _productContext.Products.ToListAsync();
        }
        public async Task<Product> GetById(int? id)
        {
            return await _productContext.Products.FindAsync(id);
        }
        public async Task<Product> Create(Product product)
        {
            _productContext.Add(product);
            var result = await _productContext.SaveChangesAsync();
            return product;
        }
        public async Task<Product> Update(Product product)
        {
            _productContext.Update(product);
            await _productContext.SaveChangesAsync();
            return product;
        }
        public async Task<Product> Remove(Product product)
        {
            _productContext.Remove(product);
            await _productContext.SaveChangesAsync();
            return product;
        }
    }
}

Neste código injetamos no construtor a instância do nosso contexto e implementamos os métodos definidos na interface IProductRepository.

Precisamos agora registrar os serviços e os repositórios criados e vamos fazer isso na próxima parte do artigo no projeto Shared.

"Não estejais inquietos por coisa alguma; antes as vossas petições sejam em tudo conhecidas diante de Deus pela oração e súplica, com ação de graças."
Filipenses 4:6

Referências:


José Carlos Macoratti