Validando domínio de maneira simples em C#

Comments
biblioteca validacao dominio
25 October 2012

Faz algum tempo que nao começo um projeto novo onde eu tenha a oportunidade de pensar em todos detalhes envolvidos na arquitetura do sistema. Esses dias comecei um novo projeto e a oportunidade apareceu. Junto com ela veio também aquele sentimento de perfeccionismo que nos obriga a achar a melhor maneira possível pra resolver um problema e muitas vezes acaba fazendo com que muito tempo seja perdido e nenhuma soluçao seja encontrada já que somos totalmente livres pra fazer o que quisermos.

Precisava pensar em como fazer pequenas validaçoes em meus objetos de domínio pra garantir que nao fiquem em um estado inválido.

Dessa vez decidi que nao ia deixar isso acontecer e tomei a decisao mais pragmática possivel: vou achar algo bacana pronto e por pra rodar. Rapidinho achei a biblioteca FluentValidation que de acordo com o próprio autor faz o que eu precisava.

A small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects - Fluent Validation

E um exemplo básico de uso retirado da documentaçao:

using FluentValidation;

public class CustomerValidator: AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleFor(customer => customer.Surname).NotEmpty();
    RuleFor(customer => customer.Forename).NotEmpty().WithMessage("Please specify a first name");
    RuleFor(customer => customer.Company).NotNull();
    RuleFor(customer => customer.Discount).NotEqual(0).When(customer => customer.HasDiscount);
    RuleFor(customer => customer.Address).Length(20, 250);
    RuleFor(customer => customer.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
  }

  private bool BeAValidPostcode(string postcode) {
    // custom postcode validating logic goes here
  }
}

Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();
ValidationResult results = validator.Validate(customer);

bool validationSucceeded = results.IsValid;
IList<ValidationFailure> failures = results.Errors;

No entanto, seguindo a idéia de manter as coisas simples, construir uma nova classe só pra colocar minhas validaçoes me pareceu demais. Pra nao perder mais tempo e resolver de vez resolvi implementar minha soluçao.

Primeiro aos testes:

[TestFixture]
public class NovoUsuarioTest
{
    [Test, TestCaseSource("Cenarios")]
    public string validacoes_basicas(NovoUsuario usuario)
    {
        var erros = new List<string>();
        var valido = usuario.IsValid();

        return usuario.Errors.First();
    }


    private static IEnumerable Cenarios()
    {
        yield return new TestCaseData(new NovoUsuario() { Senha = "abc", ConfirmacaoSenha = "abc" })
            .SetName("Email obrigatório")
            .Returns(Mensagens.Usuario.EmailEhObrigatorio);

        yield return new TestCaseData(new NovoUsuario() { Email = "abc@gmail.com", ConfirmacaoSenha = "abc" })
            .SetName("Senha obrigatória")
            .Returns(Mensagens.Usuario.SenhaEhObrigatoria);

        yield return new TestCaseData(new NovoUsuario() { Email = "abc@gmail.com", Senha = "abc" })
            .SetName("Confirmaçao de senha obrigatória")
            .Returns(Mensagens.Usuario.ConfirmacaoSenhaEhObrigatoria);

        yield return new TestCaseData(new NovoUsuario() { Email = "abc@gmail.com", Senha = "abc1", ConfirmacaoSenha = "abc" })
            .SetName("Senha e confirmaçao devem ser iguais")
            .Returns(Mensagens.Usuario.SenhaEConfirmacaoDevemSerIguais);
    }
}

Usei TestCases do NUnit pra descrever os 4 cenarios que queria testar. Na sequencia desenhei a biblioteca pensando em como gostaria de consumi-la:

public class NovoUsuario : ValidationBase<NovoUsuario>
{
    public string Id { get; set; }
    public string Email { get; set; }
    public string Senha { get; set; }
    public string ConfirmacaoSenha { get; set; }

    public override bool IsValid()
    {
        Require(x => x.Email, Mensagens.Usuario.EmailEhObrigatorio);
        Require(x => x.Senha, Mensagens.Usuario.SenhaEhObrigatoria);
        Require(x => x.ConfirmacaoSenha, Mensagens.Usuario.ConfirmacaoSenhaEhObrigatoria);
        Enforce(x => x.Senha == x.ConfirmacaoSenha, Mensagens.Usuario.SenhaEConfirmacaoDevemSerIguais);

        return base.Errors.Count == 0;
    }
}

Bem simples né? A idéia é ter apenas dois métodos:

  1. Require -> Garante que uma propriedade esta preenchida
  2. Enforce -> Recebe uma lambda que permite passar validaçoes mais elaboradas, mas ainda assim nao tao complicadas

Por último, o código que fez meus testes passarem:

public abstract class ValidationBase<T> where T : ValidationBase<T>
{
    public List<String> Errors { get; private set; }

    public ValidationBase()
    {
        this.Errors = new List<string>();
    }

    protected void Require(Expression<Func<T, object>> property, string message)
    {
        var prop = property.Compile();
        var value = prop((T)this);

        var isValid = false;

        if (value == null)
            isValid = false;
        else if (value.GetType() == typeof(String))
            isValid = !String.IsNullOrWhiteSpace(value.ToString());
        else if (value.GetType() == typeof(Guid))
            isValid = (Guid)value != Guid.Empty;
        else if (value.GetType() == typeof(int))
            isValid = (int)value == 0;
        else if (value.GetType() == typeof(decimal))
            isValid = (decimal)value == 0;
        else if (value.GetType() == typeof(DateTime))
            isValid = (DateTime)value == DateTime.MinValue;         

        if (!isValid)
            this.Errors.Add(message);
    }

    protected void Enforce(Func<T, bool> validation, string message)
    {
        var isValid = false;            

        if (validation != null)
            isValid = validation.Invoke((T)this);
        else
            throw new ArgumentNullException("validation");

        if (!isValid)
            this.Errors.Add(message);
    }

    public abstract bool IsValid();
}

Simples, mas poderosa o suficiente pra me oferecer que eu preciso agora.

Quando eu precisar de mais conversamos novamente sobre o assunto. :)


<< Blog de volta e em nova plataforma
Como armazenar senhas de forma segura em sua aplicaçao - Parte 1>> 
comments powered by Disqus
tucaz

tucaz

.NET Software Developer
About
All Posts
RSS
@tucaz
GitHub