Árvore de páginas

Para trabalhar com nossa camada de domínio precisamos definir nossas entidades, suas validações, criar nossos repositórios e serviços de domínio.

Criando o projeto

Para começarmos a trabalhar temos que criar um projeto com suporte a .NET Core.

Para isso através do Visual Studio vamos criar nosso projeto acessando: File -> New -> Project -> .NET Standard -> Class Library.

Com o projeto criado vamos adicionar os seguintes pacotes via nuget: Tnf.App, Tnf.App.Builder disponível em nosso package source: https://www.myget.org/F/tnf/api/v3/index.json

Definindo uma Entidade

As entidades são um dos conceitos fundamentais do DDD (Domain Driven Design) onde Eric Evans descreve-o como "um objeto que não é fundamentalmente definido por seus atributos, mas sim por um fio de continuidade e identidade".

Entidades são derivadas de classe Entity no TNF onde esta já possui uma chave (do tipo int ou informada através de um parâmetro genérico):

Quando a entidade é criada também é definido um enumerador, com códigos únicos que descrevem os erros de negócio da entidade.

Person.cs
using System;
using Tnf.Domain.Entities;

namespace Tnf.Architecture.Domain.Registration
{
    public class Person : Entity
    {
        public string Name { get; internal set; }
        public DateTime CreationTime { get; internal set; }

        public Person() => CreationTime = DateTime.Now;

        public enum Error
        {
            Unexpected = 0,
            PersonIdMustHaveValue = 1,
            PersonNameMustHaveValue = 2
        }
    }
}

Note que o exemplo acima está marcado como public para que ela seja exposta para fora da camada de domínio, por exemplo, na camada de Mapper e de Repository.

Validação de Entidades de Domínio

Para validar qualquer tipo de regra em cima de uma entidade usamos alguns patterns: Specification, Builder e Notification.

Em um de seus artigos descrevendo o uso do Notification pattern, Martin Flowler faz uma citação sobre o uso de exception dentro de uma aplicação descrita pelos autores Dave Thomas Andy Hunt em seu livro "The Pragmatic Programmer: From Journeyman to Master" (versão traduzida): "Acreditamos que exceções raramente devem ser usadas como parte de um fluxo normal em um programa: exceções devem ser reservadas para eventos inesperados".

No TNF pensamos da mesma forma: não devemos usar exceção para simples validações de nossas entidades de domínio.

Podemos resumir o notification pattern como uma estrutura (representada por um DTO) que coleta erros e os expõe de forma tratadas aos diversos níveis da aplicação.

Temos que ter em mente que entidades de domínio são a representação de nosso objeto de negócio evitando que elas sejam criadas de forma anêmica ou em um estado inválido (sem comportamento).

Criando uma especificação

Abaixo vemos um exemplo de criação de uma especificação para a entidade Person:

PersonNameShouldHaveValueSpecification.cs
using System;
using System.Linq.Expressions;
using Tnf.Specifications;

namespace Tnf.Architecture.Domain.Registration.Specifications
{
    internal class PersonNameShouldHaveValueSpecification : Specification<Person>
    {
        public override string LocalizationSource { get; protected set; } = AppConsts.LocalizationSourceName;
        public override Enum LocalizationKey { get; protected set; } = Person.Error.PersonNameMustHaveValue;
 
        public override Expression<Func<Person, bool>> ToExpression()
        {
            return (p) => !string.IsNullOrWhiteSpace(p.Name);
        }
    }
}

Dentro do pacote Tnf.App.Specifications podemos utilizar através da herança a classe Specification<Entity> que recebe como parâmetro a entidade a ser validada.

Pense na especificação como sendo uma regra para sua entidade. A regra acima valida para que o campo "Name" de nossa classe Person não seja inválido.

Criando um Builder

O pattern de Builder é usado para construir nossa entidade de negocio, não apenas isso, mas também agregar todas as especificações e retornar uma estrutura informado se a entidade é valida ou não para ser utilizada.

Pense no Builder como um agregador de regras para a entidade de negócio.

Podemos ver sua definição para a entidade criada Person disponível a seguir:

PersonBuilder.cs
using Tnf.Architecture.CrossCutting;
using Tnf.Architecture.Domain.Registration.Specifications;
using Tnf.App.Builder;

namespace Tnf.Architecture.Domain.Registration
{
    public class PersonBuilder : Builder<Person>
    {
        public PersonBuilder(INotificationHandler notification) : base(notification) { }
        public PersonBuilder(INotificationHandler notification, Person instance) : base(notification, instance) { }

        public PersonBuilder WithId(int id)
        {
            Instance.Id = id;
            return this;
        }

        public PersonBuilder WithName(string name)
        {
            Instance.Name = name;
            return this;
        }
 
        protected override void Specifications()
        {
            AddSpecification(new PersonNameShouldHaveValueSpecification());
        }
    }
}

O builder acima cria a entidade de forma fluente, adicionando valores a entidade e através do método sobrescrito Build executando todas as regras (especificações) de nossa entidade.

Internamente é chamado o método Validate da classe Builder para executar todas as Specifications adicionadas no método sobrescrito Specifications() e se a regra das especificações adicionadas não for valida, ele adicionará uma Notification no objeto único do request com a mensagem especificada pelos campos LocalizationSource e LocalizationKey utilizando a classe LocalizationHelper para obter a mensagem de erro através da localização.

Definindo um Repositório

Quando precisamos consultar uma fonte de dados em nosso domínio, precisamos fazer isso através de um repositório.

Temos que ter em mente que o repositório é apenas definido nesta camada e não criado.

Nossa camada de domínio tem a responsabilidade de implementar a interface (contrato) de nosso repositório deixando para que outra camada de infraestrutura realize sua implementação.

Esta camada não conheçe a fonte da informação obtida/persistida (Relacional, NoSQL, Arquivo, etc).

A interface IRepository fará a injeção de dependência pela convenção de quem a implementar.

Todo novo repositório deve ser herdado desta interface:

IPersonRepository.cs
public interface IPersonRepository : IRepository
{
	int CreatePerson(Person person);
}

Criando um Serviço de Domínio

Quando falamos em serviço de domínio podemos falar em processos de negócio. Um serviço de domínio tem a característica de representar esses processos de forma clara e definida utilizando toda a infraestrutura necessária (Repositórios).

Para trabalhar com serviços de domínio, temos a interface IDomainService e uma classe chamada AppDomainService.

Tanto a interface como a classe base quando implementadas já utilizam as convenções ao realizar a injeção da dependência automaticamente.

O serviço por default recebe um repositório podendo conter um ou mais de acordo com a necessidade:

IRegistrationService.cs
using Tnf.Domain.Services;
using Tnf.Dto;

namespace Tnf.Architecture.Domain.Interfaces.Services
{
    public interface IRegistrationService : IDomainService
    {
        int Register(PersonBuilder builder);
    }
}

Podemos perceber a herança da interface IDomainService e o retorno sendo o tipo da chave primária da entidade no método Register, ele retornará só a chave que recebe do repositório pois a camada de Application  já possui o Dto de retorno mas não possui o valor da cahve primária.

O método Register recebe um PersonBuilder que a camada de Application irá mandar preenchido e quem chamará o método Build é a própria camada de Domain para que só a essa camada mexa com a entidade.

O código a seguir exemplifica a implementação de nosso serviço de domínio, usando o repositório e o builder criado para a entidade Person.

RegistrationService.cs
using Tnf.Architecture.Domain.Interfaces.Services;
using Tnf.App.Domain.Services;
using Tnf.Dto;

namespace Tnf.Architecture.Domain.Registration
{
    public class RegistrationService : AppDomainService<IPersonRepository>, IRegistrationService
    {
		private readonly IPersonRepository _repository;
        public RegistrationService(IPersonRepository repository)
		{
			_repository = repository;
		}
        public int Register(PersonBuilder builder)
        {
            var person = builder.Build();
            if (Notification.HasNotification())
				return 0;
            return _repository.CreatePerson(person);
        }
    }
}

No inicio do método Register definimos o objeto de retorno e logo após chamamos o método Build do Builder para construir nossa entidade.

O método Build nos retorna a estrutura da Entidade e se foi levantado notificações  durante o build podemos verificar se existe notificações com o campo exposto pela classe Tnf.App.Domain.Services.AppDomainService Notification.

Quando a entidade é apta a ser utilizada podemos consumir nosso repositório através da propriedade Repository que corresponde a IPersonRepository recebida pela injeção de interface no construtor da classe herdada AppDomainService<IPersonRepository>.

Neste exemplo temos apenas um repositório, mas lembre-se que temos presente a injeção de dependência do framework e podemos usar quantos forem necessários em nosso serviço de domínio.

  • Sem rótulos