30 maio, 2016 4 Comentários AUTOR: elemarjr CATEGORIAS: Sem categoria

Construindo um sistema de validações personalizável (aka Brincando com predicados)

Tempo de leitura: 5 minutos

Validar dados é uma atividade importante para qualquer sistema! Seja em um caso simples, como o tratamento de entradas em um formulário, ou em situações mais complexas, como na verificação de pré-condições para execução de um processo, a validação é sempre importante.

party.Invite(
    employees.Where(e => e.Age > 20 && e.Age < 70)
    );

No código anterior, um convite para uma festa é enviado para todos os funcionários com idade maior que 20 e menor que 70.

Embora o código acima seja fácil de escrever e entender, ele implicará em alteração sempre que as regras para convite sejam alteradas. Algo mais interessante seria:

party.Invite(
    employees.Where(predicatesStore.Load<Employee>("elegibleToParty"))
    );

Aqui, o "predicate" é carregado de uma base externa ao código.

Nesse post, compartilho um pequeno sistema de tipos que pode servir como inspiração para criação de um sistema de validações configurável.

Uma fina camada de abstração para Predicate

Segundo a documentação oficial, Predicate:

Represents the method that defines a set of criteria and determines whether the specified object meets those criteria.

Entretanto, um Predicate não é fácil de persistir. O que faremos é criar uma "camada" simples para definição de predicados.

public abstract class PredicateBase<T>
{
    public abstract Predicate<T> Build();

    public static implicit operator Predicate<T>(PredicateBase<T> source)
    {
        return source.Build();
    }

    public static implicit operator Func<T, bool>(PredicateBase<T> source)
    {
        var predicate = source.Build();
        return e => predicate(e);
    }
}

Algumas implementações concretas

Agora que já temos nosso sistema minimamente definido, vejamos o que pode ser implementado.

Comecemos com algumas operadores básicos.

public class LT<T> : PredicateBase<T>
    where T : IComparable
{
    public T Value { get; private set; }

    public LT(T value)
    {
        Value = value;
    }

    public override Predicate<T> Build()
    {
        return e => Value.CompareTo(e) > 0;
    }
}


public class GT<T> : PredicateBase<T>
    where T : IComparable
{
    public T Value { get; private set; }

    public GT(T value)
    {
        Value = value;
    }

    public override Predicate<T> Build()
    {
        return e => Value.CompareTo(e) < 0;
    }
}

public class EQ<T> : PredicateBase<T>
    where T : IComparable
{
    public T Value { get; private set; }

    public EQ(T value)
    {
        Value = value;
    }

    public override Predicate<T> Build()
    {
        return e => Value.CompareTo(e) == 0;
    }
}

Depois alguns operadores relacionais:

public class Any<T> : PredicateBase<T>
{
    public IEnumerable<PredicateBase<T>> Elements { get; private set; }

    [JsonConstructor]
    public Any(IEnumerable<PredicateBase<T>> elements)
    {
        Elements = elements;
    }

    public Any(params PredicateBase<T>[] elements)
    {
        Elements = elements;
    }

    public override Predicate<T> Build()
    {
        var buildedElements = Elements.Select(e => e.Build());
        return e => buildedElements.Any(be => be(e));
    }
}

public class All<T> : PredicateBase<T>
{
    public IEnumerable<PredicateBase<T>> Elements { get; private set; }

    [JsonConstructor]
    public All(IEnumerable<PredicateBase<T>> elements)
    {
        Elements = elements;
    }

    public All(params PredicateBase<T>[] elements)
    {
        Elements = elements;
    }

    public override Predicate<T> Build()
    {
        var buildedElements = Elements.Select(e => e.Build());
        return e => buildedElements.All(be => be(e));
    }
}

Por fim, vamos criar algum mecanismo para navegar na estrutura do objeto que desejamos validar.

public class Map<T, TB> : PredicateBase<TB>
{
    public string PropertyPath { get; }
    public PredicateBase<T> Then { get; }

    public Map(
        Expression<Func<TB, T>> memberAccessExpression,
        PredicateBase<T> then
        )
    {
        Then = then;
        PropertyPath = memberAccessExpression.ToPropertyPath();
    }

    [JsonConstructor]
    public Map(
        string propertyPath,
        PredicateBase<T> then
        )
    {
        Then = then;
        PropertyPath = propertyPath;
    }

    public override Predicate<TB> Build()
    {
        var buildedThen = Then.Build();

        var parameter = Expression.Parameter(typeof(TB), "e");
        var members = PropertyPath.Split('.');

        var mi = typeof(TB).GetMember(members[0]).First();
        var ma = Expression.MakeMemberAccess(parameter, mi);

        for (var i = 1; i < members.Length; i++)
        {
            mi = mi.GetMemberType().GetMember(members[i]).First();
            ma = Expression.MakeMemberAccess(ma, mi);
        }

        var expression = Expression.Lambda<Func<TB, T>>(
            ma, parameter);

        return e => buildedThen(expression.Compile()(e));
    }
}

Facilitando a construção dos predicados

Já temos nossa fina camada de abstração. Já podemos escrever nossa instrução original dessa forma:

party.Invite(employees.Where(
    new Map<int, Employee>(e => e.Age, new All<int>(new GT<int>(20), new LT<int>(70)))
    ));

Agora vamos ver como torná-la mais simples de usar através de alguns breves facilitadores:

public static class PredicatesLang
{
    public static PredicateBase<T> And<T>(
        this PredicateBase<T> left,
        PredicateBase<T> right
        )
    {
        return new All<T>(left, right);
    }

    public static PredicateBase<T> Or<T>(
        this PredicateBase<T> left,
        PredicateBase<T> right
        )
    {
        return new Any<T>(left, right);
    }

    public static LT<T> LT<T>(T value)
        where T : IComparable
    {
        return new LT<T>(value);
    }

    public static GT<T> GT<T>(T value)
        where T : IComparable
    {
        return new GT<T>(value);
    }

    public static EQ<T> EQ<T>(T value)
        where T : IComparable
    {
        return new EQ<T>(value);
    }

    public static WhenIs<T, TB> When<T, TB>(Expression<Func<TB, T>> input)
    {
        return new WhenIs<T, TB>(input);
    }

    public class WhenIs<T, TB>
    {
        private readonly Expression<Func<TB, T>> _source;

        public WhenIs(Expression<Func<TB, T>> source)
        {
            _source = source;
        }

        public PredicateBase<TB> Is(PredicateBase<T> predicate)
        {
            return new Map<T, TB>(_source, predicate);
        }
    }
}

Assim, podemos escrever algo um pouco mais expressivo:

party.Invite(employees.Where(
    When<int, Employee>(e => e.Age).Is(GT(20).And(LT(70)))
    ));

Persistindo predicados no RavenDB

Como nossos predicados agora são classes simples, podemos utilizar RavenDB para os armazenar.

public class PredicatesStore
{
    private DocumentStore _ds;
    private JsonSerializer _serializer;

    public PredicatesStore()
    {
        _ds = new DocumentStore
        {
            Url = "http://localhost:8080/",
            DefaultDatabase = "FunWithPredicates",
            Conventions = { FindTypeTagName = t => "Predicates" },
        };

        _ds.Initialize();
        _serializer = _ds.Conventions.CreateSerializer();
    }

    public void Save<T>(string predicateName, PredicateBase<T> predicate)
    {
        using (var session = _ds.OpenSession())
        {
            var doc = new Holder<T>(predicateName, predicate);
            session.Store(doc);
            session.SaveChanges();
        }
    }

    public PredicateBase<T> Load<T>(string predicateName)
    {
        using (var session = _ds.OpenSession())
        {
            var doc = session.Load<Holder<T>>(predicateName);
            return doc.Predicate;
        }
    }

    public class Holder<T>
    {
        public string Id { get; private set; }
        public PredicateBase<T> Predicate { get; }

        public Holder(string id, PredicateBase<T> predicate)
        {
            Id = id;
            Predicate = predicate;
        }
    }
}

Bacana, não? Podemos agora salvar nosso predicado:

var ps = new PredicatesStore();
ps.Save(
    "elegibleToParty",
    When<int, Employee>(e => e.Age).Is(GT(20).And(LT(70)))
);

Que teria essa aparência no RavenDB:

{
    "Predicate": {
        "$type": "FunWithPredicates.Map`2[[System.Int32, mscorlib],[FunWithPredicates.Employee, FunWithPredicates]], FunWithPredicates",
        "PropertyPath": "Age",
        "Then": {
            "$type": "FunWithPredicates.All`1[[System.Int32, mscorlib]], FunWithPredicates",
            "Elements": [
                {
                    "$type": "FunWithPredicates.GT`1[[System.Int32, mscorlib]], FunWithPredicates",
                    "Value": 20
                },
                {
                    "$type": "FunWithPredicates.LT`1[[System.Int32, mscorlib]], FunWithPredicates",
                    "Value": 70
                }
            ]
        }
    }
}

E que poderia ser facilmente utilizado:

party.Invite(employees.Where(
    ps.Load<Employee>("elegibleToParty")    
    ));

Era isso.

4 Comentários

  1. Thiago Custódio (@thdotnet) 9 meses ago says:

    Muito bom Elemar. Eu teria feito com (XML ou JSON) + Reflection + Strategy Pattern, mas sua abordagem fica muito mais simples por conta da abordagem funcional que o LINQ trás para o C#.

    Responder
  2. Felipe Albuquerque de Almeida 9 meses ago says:

    Show de bola! Vou colocar em prática essa sugestão... 😉

    Responder
  3. David Soares 8 meses ago says:

    Uma gota de lágrima correu pelo canto do rosto... quebrei muita pedra com linq expression tentando fazer isso... Praticamente expôs o mecanismo por trás do AutoMapper, a sintaxe lembra bastante... Fantástico!!! Conheci seu blog a pouco tempo e já sou fã!!!

    Responder
  4. David Soares 8 meses ago says:

    Dúvida: É possível armazenar um predicado em uma base relacional? Motivo: pra evitar a fadiga 🙂 . Em ambientes corporativos, a decisão de utilizar um NoSQL passa por diversas equipes, áreas e pode demorar e muito, tipo coisa de 1 ano... e é bem capaz de decidirem por um middleware que não faz sentido nenhum...

    Responder