ASP .NET Core - Implementando a Onion Architecture - II


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 primeira parte do artigo vamos iniciar a implementação da camada Core no projeto eStore.Domain.

Implementação da camada Core : projeto eStore.Domain

O projeto Domain contém os objetos do domínio, as Exceptions, as regras de validação, etc. Aqui vamos implementar as interfaces dos repositórios para podermos acessar a camada mais externa de forma desacoplada.

No projeto eStore.Domain da camada Core vamos criar uma pasta chamada Entities e nesta pasta criar as entidades que representam o nosso modelo de domínio representando pelas classes Product e Category.

Aqui devemos decidir quais as propriedades e comportamentos nosso domínio vai possuir. Para não tornar o exemplo muito longo e maçante eu vou definir um modelo de domínio bem simples onde :

1 -  Product vai possuir as propriedades : Id, Name, Description, Stock, Image e Price

2 -  Category vai possuir as propriedades : Id e Name

Existe um relacionamento um para muitos entre Category e Product onde uma categoria poderá ter um ou mais produtos. Assim uma categoria poderá ter uma lista de produtos.

No mundo real um Produto vai possuir mais propriedades e comportamentos e uma Categoria também. Mas vamos deixar assim.

O que você costuma encontrar como exemplo em muitos artigos é apenas a definição classes com suas propriedades. Seria como definir as nossas entidades assim:

1- Category

public class Category 
{
   public int Id { get; set; }
   public string Name { get; set; }
      
   public ICollection<Product> Products { get; set; }
}

2- Product

public class Product 
{
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
        public int Stock { get; set; }
        public string Image { get; set; }
        public Category Category { get; set; }
        public int CategoryId { get; set; }
}

Temos aqui duas classes POCO sem comportamentos e totalmente aberta onde qualquer um poderá tentar criar objetos destas classes sem garantir que esses objetos estejam em um estado válido.

Não existem lógica alguma nessas classes, não existe regra de negócio definida, não existe validação, e, assim temos um modelo de domínio anêmico e pobre que fere os princípios básicos da programação orientada a objetos.

Essa será a realidade que você vai encontrar em muitos artigos.

Agora vem a pergunta : "Então isso é ruim ?"  "Podemos considerar essa abordagem errada ?"

A resposta é : "Depende..."

Se você estiver apenas criando um exemplo básico para apresentar um funcionalidade ou um protótipo de uma pequena aplicação e quer apenas exibir algumas informações ou quer testar um recurso novo ou mesmo estiver criando uma aplicação pessoal como gerenciar o seu álbum de figurinhas, então não há nada de errado aqui isso  é o suficiente e vai servir ao seu propósito.

Agora se você esta criando uma aplicação comercial (line of bussiness) para produção, se você começar usando esta abordagem, começou pegando um atalho que poderá de levar a lugares sombrios fazendo com que seu projeto se torne uma massa amorfa de código difícil de manter , entender e estender.

Então vamos melhorar essa definição de domínio.

Nosso objetivo será isolar o domínio do mundo externo e incluir algum comportamento e lógica a ele mesmo não adotando premissas complexas.

Para isso vamos fazer o seguinte:

Assim nossas entidades deverá ser reescritas e vão possuir o seguinte código:

1- Category

using eStore.Domain.Validation;
using System.Collections.Generic;
namespace eStore.Domain.Entities
{
    public sealed class Category : Entity
    {
        public string Name { get; private set; }
        public Category(string name)
        {
            ValidateDomain(name);
        }
        public Category(int id, string name)
        {
            DomainExceptionValidation.When(id < 0, "Invalid Id value.");
            Id = id;
            ValidateDomain(name);
        }
        public void Update(string name)
        {
            ValidateDomain(name);
        }
        private void ValidateDomain(string name)
        {
            DomainExceptionValidation.When(string.IsNullOrEmpty(name), "Invalid name. Name is required");
            DomainExceptionValidation.When(name.Length < 3, "Invalid name, too short, minimum 3 characters");
            Name = name;
        }
        public ICollection<Product> Products { get; set; }
    }
}

1- Product

using eStore.Domain.Validation;
namespace eStore.Domain.Entities
{
    public sealed class Product : Entity
    {
        public string Name { get; private set; }
        public string Description { get; private set; }
        public decimal Price { get; private set; }
        public int Stock { get; private set; }
        public string Image { get; private set; }
        public Product(string name, string description, decimal price, int stock, string image)
        {
            ValidateDomain(name, description, price, stock, image);
        }
        public Product(int id, string name, string description, decimal price, int stock, string image)
        {
            DomainExceptionValidation.When(id < 0, "Invalid Id value.");
            Id = id;
            ValidateDomain(name, description, price, stock, image);
        }
        public void Update(string name, string description, decimal price, int stock, string image, int categoryId)
        {
            ValidateDomain(name, description, price, stock, image);
            CategoryId = categoryId;
        }
        private void ValidateDomain(string name, string description, decimal price, int stock, string image)
        {
            DomainExceptionValidation.When(string.IsNullOrEmpty(name), "Invalid name. Name is required");
            DomainExceptionValidation.When(name.Length < 3, "Invalid name, too short, minimum 3 characters");
            DomainExceptionValidation.When(string.IsNullOrEmpty(description), "Invalid description. Description is required");
            DomainExceptionValidation.When(description.Length < 5, "Invalid description, too short, minimum 5 characters");
            DomainExceptionValidation.When(price < 0, "Invalid price value");
            DomainExceptionValidation.When(stock < 0, "Invalid stock value");
            DomainExceptionValidation.When(image.Length > 250, "Invalid image name, too long, maximum 250 characters");
            Name = name;
            Description = description;
            Price = price;
            Stock = stock;
            Image = image;
        }
        public Category Category { get; set; }
        public int CategoryId { get; set; }
    }
}

Aqui estamos usando a classe abstrata Entity como classe base para fornecer a propriedade Id que será comum a ambas as entidades. Observe que a classe é abstrata e por isso esta sendo usado apenas como classe base não permitindo instanciação.

 public abstract class Entity
 {
        public int Id { get; protected set; }
 }

Além disso vamos criar uma pasta Validation no projeto Domain e definir a classe DomainExceptionValidation para tratar as exceções das validações do domínio:

using System;
namespace eStore.Domain.Validation
{
    public class DomainExceptionValidation : Exception
    {
        public DomainExceptionValidation(string error) : base(error)
        {
        }
        public static void When(bool hasError, string error)
        {
            if (hasError)
                throw new DomainExceptionValidation(error);
        }
    }
}

Agora estamos garantindo alguma estabilidade ao nosso modelo de domínio e ela agora 'esta mais esperto'  pois somente será possível  criar um objeto das entidades do domínio usando o construtor parametrizado e como estamos validando as propriedades usando o que seriam 'as nossas regras de negócio' o objeto somente será criado em um estado válido e estável.

Poderíamos ir mais a fundo, e, em um projeto do mundo real é o que você provavelmente faria mas creio que você já pegou a idéia básica...

Para ter uma visão do nosso modelo de domínio, das entidades e seus relacionamentos podemos incluir no projeto um Class Diagram, no menu Add New Item, e exibir o seguinte gráfico:

Para concluir a implementação desta camada vamos criar a pasta Interfaces e nesta pasta criar duas interfaces : ICategoryRepository e IProductRepository.

1- ICategoryRepository

    public interface ICategoryRepository
    {
        Task<IEnumerable<Category>> GetCategories();
        Task<Category> GetById(int? id);
        Task<Category> Create(Category category);
        Task<Category> Update(Category category);
        Task<Category> Remove(Category category);
    }

2- IProductRepository

    public interface IProductRepository
    {
        Task<IEnumerable<Product>> GetProducts();
        Task<Product> GetById(int? id);
        Task<Product> Create(Product product);
        Task<Product> Update(Product product);
        Task<Product> Remove(Product product);
    }

Estamos definindo interfaces com um contrato para acessar o repositório que será implementado na camada Infrastructure.

Aqui não temos implementação e não precisamos incluir referências a nenhum framework ou ferramenta ORM que no nosso exemplo seria o EF Core.

Assim teremos a seguinte visão da nossa solução na janela Solution Explorer:

Eu nem preciso destacar que você poderia ter criado outra estrutura de pastas e que talvez o nome Entities para a pasta onde definimos o domínio possa incomodar pois, segundo os conceitos do DDD, o ideal seria termos criado Entities e Value Objects mas eu não vou entrar nesse mérito nem nesses detalhes de implementação neste artigo.

Se você quer saber a diferença entre Entity e Value Object veja minha aula no youtube :  Domain Driven Design: Entity e Value Object.

O que importa é que se exibimos as propriedades do projeto Domain veremos que ele não possui nenhuma dependência. Ele não depende de nada como mostra a figura a seguir:

Porque definimos essas interfaces no domínio ?

A Onion Architecture depende muito do princípio de Inversão da Dependência, e, o núcleo do aplicativo precisa da implementação de interfaces principais, sendo que as classes das implementações vão residir nas bordas do aplicativo, e assim precisamos de algum mecanismo para injetar esse código em tempo de execução para que o aplicativo possa fazer algo útil como acessar os dados do repositório de forma desacoplada.

Na próxima parte do artigo iremos continuar a implementação da camada Core focando no projeto Application.

"Voz do que clama no deserto: Preparai o caminho do Senhor; endireitai no ermo vereda a nosso Deus.
Todo o vale será exaltado, e todo o monte e todo o outeiro será abatido; e o que é torcido se endireitará, e o que é áspero se aplainará."
Isaías 40:3,4

Referências:


José Carlos Macoratti