Muitas vezes precisamos abstrair características complexas, tentando simplificar uma consulta Linq para um campo que retorne apenas true ou false, pois isto é muito útil, principalmente quando usamos Entity Framework. O padrão de comportamento especificação busca tornar características, que muitas vezes complexas, em algo mais simples, legível, reusável e de fácil manutenção, com uma abordagem mais elegante.
Além de auxiliar na criação de consulta, Specification pode ser bastante útil em validações, onde uma entidade de domínio precise checar uma série de informações e retornar se ela satisfaz um cenário válido para alguma finalidade específica ou até mesmo persistir os dados no banco. Em resumo, Specification é a união de regras de negócios e expressões boolianas.
Projeto de exemplo
O projeto de exemplo mostra dois usos da abordagem: uma para definir entidades com características específicas e outra para validar entidades, que pode usado juntamente com notificações e eventos. Para isso crie uma solução chamada Specification, e adicione um projeto do tipo Class Library, chamado Specification.Domain.
A interface ISpecification<T> que define o contrato de implementação do método IsSatisfiedBy() e que recebe a especificação baseada em regras de negócio, que são geradas a partir dos métodos And(), AndNot(), Or(), OrNot() e Not() ou uma expresão Linq.
|
namespace Specification.Domain.Interfaces.Specifications { public interface ISpecification<T> { bool IsSatisfiedBy(T candidate); ISpecification<T> And(ISpecification<T> specification); ISpecification<T> AndNot(ISpecification<T> specification); ISpecification<T> Or(ISpecification<T> specification); ISpecification<T> OrNot(ISpecification<T> specification); ISpecification<T> Not(ISpecification<T> specification); } } |
Crie uma pasta chamada Specifications no projeto de domínio, e dentro dela inclua a classe CompositeSpecification, que será a base para as especificações, implementando todos os métodos da interface anterior.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
using Specification.Domain.Interfaces.Specifications; namespace Specification.Domain.Specifications { public abstract class CompositeSpecification<T> : ISpecification<T> { public abstract bool IsSatisfiedBy(T candidate); public ISpecification<T> And(ISpecification<T> specification) => new AndSpecification<T>(this, specification); public ISpecification<T> AndNot(ISpecification<T> specification) => new AndNotSpecification<T>(this, specification); public ISpecification<T> Or(ISpecification<T> specification) => new OrSpecification<T>(this, specification); public ISpecification<T> OrNot(ISpecification<T> specification) => new OrNotSpecification<T>(this, specification); public ISpecification<T> Not(ISpecification<T> specification) => new NotSpecification<T>(specification); } } |
Com a classe base criada, podemos criar as classes de operadores para definir a regra de negócio, que irá retornar uma resposta true ou false. Para isso, adicione as classes a seguir na pasta Specifications:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
using Specification.Domain.Interfaces.Specifications; namespace Specification.Domain.Specifications { public class AndSpecification<T> : CompositeSpecification<T> { private readonly ISpecification<T> _leftSpecification; private readonly ISpecification<T> _rightSpecification; public AndSpecification(ISpecification<T> left, ISpecification<T> right) { _leftSpecification = left; _rightSpecification = right; } public override bool IsSatisfiedBy(T candidate) => _leftSpecification.IsSatisfiedBy(candidate) && _rightSpecification.IsSatisfiedBy(candidate); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
using Specification.Domain.Interfaces.Specifications; namespace Specification.Domain.Specifications { public class AndNotSpecification<T> : CompositeSpecification<T> { private readonly ISpecification<T> _leftSpecification; private readonly ISpecification<T> _rightSpecification; public AndNotSpecification(ISpecification<T> left, ISpecification<T> right) { _leftSpecification = left; _rightSpecification = right; } public override bool IsSatisfiedBy(T candidate) => (_leftSpecification.IsSatisfiedBy(candidate) && _rightSpecification.IsSatisfiedBy(candidate)) != true; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
using Specification.Domain.Interfaces.Specifications; namespace Specification.Domain.Specifications { public class OrSpecification<T> : CompositeSpecification<T> { private readonly ISpecification<T> _leftSpecification; private readonly ISpecification<T> _rightSpecification; public OrSpecification(ISpecification<T> left, ISpecification<T> right) { _leftSpecification = left; _rightSpecification = right; } public override bool IsSatisfiedBy(T candidate) { return _leftSpecification.IsSatisfiedBy(candidate) || _rightSpecification.IsSatisfiedBy(candidate); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
using Specification.Domain.Interfaces.Specifications; namespace Specification.Domain.Specifications { public class OrNotSpecification<T> : CompositeSpecification<T> { private readonly ISpecification<T> _leftSpecification; private readonly ISpecification<T> _rightSpecification; public OrNotSpecification(ISpecification<T> left, ISpecification<T> right) { _leftSpecification = left; _rightSpecification = right; } public override bool IsSatisfiedBy(T candidate) => (_leftSpecification.IsSatisfiedBy(candidate) || _rightSpecification.IsSatisfiedBy(candidate)) != true; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
using Specification.Domain.Interfaces.Specifications; namespace Specification.Domain.Specifications { public class NotSpecification<T> : CompositeSpecification<T> { private readonly ISpecification<T> _notSpecification; public NotSpecification(ISpecification<T> not) { _notSpecification = not; } public override bool IsSatisfiedBy(T candidate) => !_notSpecification.IsSatisfiedBy(candidate); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
using System; namespace Specification.Domain.Specifications { public class ExpressionSpecification<T> : CompositeSpecification<T> { private readonly Func<T, bool> _expression; public ExpressionSpecification(Func<T, bool> expression) { if (expression == null) throw new ArgumentException(); _expression = expression; } public override bool IsSatisfiedBy(T candidate) => _expression(candidate); } } |
No projeto de domínio, crie duas pastas chamada Entities e ValueObjects. E em ValueObjects inclua uma classe base para os objetos de valor chamada ValueObject e dentro de Entities inclua uma classe base para as entidades chamada Entity. Estas duas classes terão as propriedades e métodos básicos para as especificações de validação e uma função IsValid que retornará true se os dados preenchidos atenderem os requisitos.
|
using Specification.Domain.Specifications; namespace Specification.Domain.ValueObjects { public abstract class ValueObject { protected CompositeSpecification<object> ValidSpecification = null; public bool IsValid() { return ValidSpecification?.IsSatisfiedBy(this) ?? true; } } } |
|
using Specification.Domain.Specifications; namespace Specification.Domain.Entities { public abstract class Entity { protected CompositeSpecification<object> ValidSpecification = null; public bool IsValid() { return ValidSpecification?.IsSatisfiedBy(this) ?? true; } } } |
Na pasta ValueObjects teremos uma classe de Email que herda de ValueObject, e na pasta de Entities as classes Category e Person e ambas herdam de Entity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
using Specification.Domain.Specifications.ValueObjects; namespace Specification.Domain.ValueObjects { public class Email : ValueObject { public const int AddressMinLength = 3; public const int AddressMaxLength = 255; public Email(string address) { Address = address; ValidSpecification = new EmailValidSpecification<object>(); } public string Address { get; } public override string ToString() => Address; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
namespace Specification.Domain.Entities { public class Category : Entity { public const int DescriptionMinLength = 1; public const int DescriptionMaxLength = 20; public Category(int categoryId, string description) { CategoryId = categoryId; Description = description; } public int CategoryId { get; } public string Description { get; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
using System; using Specification.Domain.Specifications.Entities; using Specification.Domain.ValueObjects; namespace Specification.Domain.Entities { public class Person : Entity { public const int NameMinLength = 2; public const int NameMaxLength = 50; public Person(Guid personId, string name, Email email, Category category) { PersonId = personId; Name = name; Email = email; Category = category; ValidSpecification = new PersonValidSpecification<object>(); } public Guid PersonId { get;} public string Name { get; } public Email Email { get; } public Category Category { get; } } } |
Neste ponto, a propriedade ValidSpecification ainda não possui suas especificaçãos que validam pessoa e o e-mail, e para isso temos que criar as classes com as regras de negócio.
Entre na pasta Specification, e dentro dela crie uma pasta chamada ValueObjects e neste diretório inclua uma classe chamada EmailValidSpecification que herda de CompositeSpecification.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
using System.Text.RegularExpressions; using Specification.Domain.ValueObjects; namespace Specification.Domain.Specifications.ValueObjects { public class EmailValidSpecification<T> : CompositeSpecification<T> { private readonly bool _required; public EmailValidSpecification(bool required = false) { _required = required; } public override bool IsSatisfiedBy(T candidate) { var email = candidate as Email; if (string.IsNullOrEmpty(email?.Address) && !_required) return true; if ((email?.Address ?? "").Length < Email.AddressMinLength) return false; if ((email?.Address ?? "").Length > Email.AddressMaxLength) return false; const string pattern = @"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$"; return Regex.IsMatch(email?.Address ?? "", pattern); } } } |
Na pasta Entities de Specification, inclua uma classe chamada PersonNameValidSpecification, que irá validar o nome da pessoa.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
using Specification.Domain.Entities; namespace Specification.Domain.Specifications.Entities { public class PersonNameValidSpecification<T> : CompositeSpecification<T> { private readonly bool _required; public PersonNameValidSpecification(bool required = false) { _required = required; } public override bool IsSatisfiedBy(T candidate) { var person = candidate as Person; if (string.IsNullOrEmpty(person?.Name) && !_required) return true; if ((person?.Name ?? "").Length < Person.NameMinLength) return false; if ((person?.Name ?? "").Length > Person.NameMaxLength) return false; return true; } } } |
Com as especificação de validação de e-mail e de nome prontas, podemos adicionar uma especificação que irá validar a pessoa como um todo, e para isso inclua uma classe chamada PersonValidSpecification na pasta Entities de Specifications.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
using Specification.Domain.Entities; using Specification.Domain.Specifications.ValueObjects; using Specification.Domain.ValueObjects; namespace Specification.Domain.Specifications.Entities { public class PersonValidSpecification<T> : CompositeSpecification<T> { public override bool IsSatisfiedBy(T candidate) { var person = candidate as Person; var personNameSpecification = new PersonNameValidSpecification<Person>(true); if (!personNameSpecification.IsSatisfiedBy(person)) return false; var emailSpecification = new EmailValidSpecification<Email>(); if (!emailSpecification.IsSatisfiedBy(person?.Email)) return false; return true; } } } |
Ainda temos um especificação para definir se uma pessoa é cliente, que pode ser usada em filtros de listas baseadas em Linq, portanto inclua o objeto PersonCustomerSpecification na pasta Entities de Specifications.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
using Specification.Domain.Entities; namespace Specification.Domain.Specifications.Entities { public class PersonCustomerSpecification<T> : CompositeSpecification<T> { private readonly int _categoryId; public PersonCustomerSpecification() { _categoryId = 1; } public override bool IsSatisfiedBy(T candidate) { var person = candidate as Person; return person != null && person.Category?.CategoryId == _categoryId; } } } |
Projeto de Apresentação
Crie um projeto do tipo Class Library, chamado Specification.Domain, para que, via console, testarmos algumas formas de usar as especificações. E então, no método Main da classe Program, crie uma lista de pessoas através de uma variável do tipo List.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
(...) // Person list var people = new List<Person> { new Person(Guid.NewGuid(), "Jacob 1", new Email("jacob1@gmail.com"), new Category(2, "Partner")), new Person(Guid.NewGuid(), "Jacob 2", new Email("jacob2@gmail.com"), new Category(1, "Customer")), new Person(Guid.NewGuid(), "Jacob 3", new Email("jacob3@gmail.com"), new Category(2, "Partner")), new Person(Guid.NewGuid(), "Jacob 4", new Email("jacob4_gmail.com"), new Category(1, "Customer")), new Person(Guid.NewGuid(), "Jacob 5", new Email("jacob5@gmail.com"), new Category(1, "Customer")), new Person(Guid.NewGuid(), "Jacob 6", new Email("jacob6@gmail.com"), new Category(1, "Customer")), new Person(Guid.NewGuid(), "Jacob 7", new Email("jacob7@gmail.com"), null) }; Console.WriteLine(":: ALL PEOPLE ::"); foreach (var item in people) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } (...) |
A seguir vamos listar apenas os clientes, usando a especificação PersonCustomerSpecification.
|
(...) Console.WriteLine(""); Console.WriteLine(":: CUSTOMER ::"); ISpecification<Person> personCustomersSpecification = new PersonCustomerSpecification<Person>(); var customer = people.FindAll(x => personCustomersSpecification.IsSatisfiedBy(x)); foreach (var item in customer) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } (...) |
Neste outro exemplo, retornarmos todas as pessoas que são parceiras, usando uma expressão Linq:
|
(...) Console.WriteLine(""); Console.WriteLine(":: PARTNER ::"); ISpecification<Person> partnerSpecification = new ExpressionSpecification<Person>(x => x.Category?.CategoryId == 2); var partners = people.FindAll(x => partnerSpecification.IsSatisfiedBy(x)); foreach (var item in partners) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } (...) |
A seguir é usada uma especificação que filtra apenas entidades válidas usando a especificação pronta, mas esta operação pode ser feita também chamando o método IsValid da própria entidade.
|
(...) Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(""); Console.WriteLine(":: ALL VALID ::"); ISpecification<Person> validSpecification = new PersonValidSpecification<Person>(); var validPeople = people.Where(x => validSpecification.IsSatisfiedBy(x)); foreach (var item in validPeople) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } (...) |
Toda a classe Program pode ser vista a seguir, com outros exemplos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
|
using System; using System.Collections.Generic; using System.Linq; using Specification.Domain.Entities; using Specification.Domain.Interfaces.Specifications; using Specification.Domain.Specifications; using Specification.Domain.Specifications.Entities; using Specification.Domain.ValueObjects; namespace Specification.Prompt { internal class Program { public static void Main(string[] args) { // Person list var people = new List<Person> { new Person(Guid.NewGuid(), "Jacob 1", new Email("jacob1@gmail.com"), new Category(2, "Partner")), new Person(Guid.NewGuid(), "Jacob 2", new Email("jacob2@gmail.com"), new Category(1, "Customer")), new Person(Guid.NewGuid(), "Jacob 3", new Email("jacob3@gmail.com"), new Category(2, "Partner")), new Person(Guid.NewGuid(), "Jacob 4", new Email("jacob4_gmail.com"), new Category(1, "Customer")), new Person(Guid.NewGuid(), "Jacob 5", new Email("jacob5@gmail.com"), new Category(1, "Customer")), new Person(Guid.NewGuid(), "Jacob 6", new Email("jacob6@gmail.com"), new Category(1, "Customer")), new Person(Guid.NewGuid(), "Jacob 7", new Email("jacob7@gmail.com"), null) }; Console.WriteLine(":: ALL PEOPLE ::"); foreach (var item in people) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } // Specifications usages Console.WriteLine(""); Console.WriteLine(":: CUSTOMER ::"); ISpecification<Person> personCustomersSpecification = new PersonCustomerSpecification<Person>(); var customer = people.FindAll(x => personCustomersSpecification.IsSatisfiedBy(x)); foreach (var item in customer) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } Console.WriteLine(""); Console.WriteLine(":: PARTNER ::"); ISpecification<Person> partnerSpecification = new ExpressionSpecification<Person>(x => x.Category?.CategoryId == 2); var partners = people.FindAll(x => partnerSpecification.IsSatisfiedBy(x)); foreach (var item in partners) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } Console.WriteLine(""); Console.WriteLine(":: WITHOUT CATEGORY ::"); ISpecification<Person> nullSpecification = new ExpressionSpecification<Person>(x => x.Category == null); var nullCategory = people.FindAll(x => nullSpecification.IsSatisfiedBy(x)); foreach (var item in nullCategory) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } Console.WriteLine(""); Console.WriteLine(":: WITH AND WITHOUT CATEGORY ::"); ISpecification<Person> customersSpecification = new ExpressionSpecification<Person>(x => x.Category?.CategoryId == 1); var allWithCategorySpecification = customersSpecification.Or(partnerSpecification); var includeNull = allWithCategorySpecification.Or(nullSpecification); var allAndNullCategory = people.FindAll(x => includeNull.IsSatisfiedBy(x)); foreach (var item in allAndNullCategory) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(""); Console.WriteLine(":: ALL VALID ::"); ISpecification<Person> validSpecification = new PersonValidSpecification<Person>(); var validPeople = people.Where(x => validSpecification.IsSatisfiedBy(x)); foreach (var item in validPeople) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } Console.ForegroundColor = ConsoleColor.Magenta; Console.WriteLine(""); Console.WriteLine(":: VALID CUSTOMERS ::"); var validCustomers = people.Where(x => validSpecification.IsSatisfiedBy(x) && customersSpecification.IsSatisfiedBy(x)); foreach (var item in validCustomers) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } Console.ForegroundColor = ConsoleColor.White; Console.WriteLine(""); Console.WriteLine(":: VALID PARTNERS ::"); var validPartners = people.Where(x => validSpecification.IsSatisfiedBy(x) && partnerSpecification.IsSatisfiedBy(x)); foreach (var item in validPartners) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(""); Console.WriteLine(":: INVALID ::"); var invalidPeople = people.Where(x => !validSpecification.IsSatisfiedBy(x)); foreach (var item in invalidPeople) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine(""); Console.WriteLine(":: ISVALID ::"); var isvalidPeople = people.Where(x => x.IsValid()); foreach (var item in isvalidPeople) { Console.WriteLine(item.PersonId + " | " + item.Name + " | " + item.Email.Address + " | " + item.Category?.Description); } Console.ReadKey(); } } } |
E aqui temos a saída completa do console.

Conclusão
Specification é um padrão bastante flexível, tornando as regras de negócio do domínio mais claras. Entretanto, as validações podem ser simplificadas, usando pacotes de terceiros, como por exemplo o Flunt, que une esta abordagem com a Domain Notification, e pode residir em uma camada transversal do projeto.
O projeto completo pode ser baixado no GitHub:
https://github.com/tiagopariz/Specification
Referências