Criando um “LINQ Query Provider” para Mongo – Parte 2 – OrderBy

Publicado em 25/02/2012

3


Olá pessoal. Tudo certo?!

O primeiro post dessa série ficou bastante “denso”. Tivemos que começar o suporte a LINQ do zero. Implementamos suporte para filtro, usando a cláusula/operador Where, bem como toda a “tradução” para o modelo de objetos do driver mongo para c#.

No post de hoje, vamos continuar o trabalho criando suporte para ordenação. Para isso, vamos implementar os operadores OrderBy, OrderByDescending, ThenBy e ThenByDescending.

O projeto está no Github. Estou tomando o cuidado de fazer commits em “baby steps”. Dessa forma, você pode ver como fui criando o código, passo-a-passo, quase pareando.

Espero pull-requests.

O objetivo

Como disse, meu objetivo é oferecer uma interface de programação alternativa para o driver oficial do MongoDB. Logo, no lugar de ter que escrever algo assim:

var q2 = collection
    .Find(Query.GT("age", 20))
    .SetSortOrder("age");

.. que é específico para o domínio do Mongo. Desejo poder escrever algo assim:

var q = from d in collection.AsQueryable()
        where d["age"] > 20
        orderby d["age"] 
        select d;

… que, embora seja mais verboso, está em uma notação que já é familiar para desenvolvedores .net.

AsQueryable – um helper

No primeiro post, começamos o desenvolvimento de uma classe chamada QueryableMongo que “adapta” uma Collection para a interface Linq. Nela, adicionei um método estático, Create, para gerar uma instância (além do construtor).

Entretanto, desenvolvedores .NET estão mais habituados a utilizar um método chamado AsQueryable para habilitar tal interface. Por isso, fizemos essa implementação:

using MongoDB.Bson;
using MongoDB.Driver;

namespace LinqToMongo
{
    public static class MongoExtensions
    {
        public static QueryableMongo AsQueryable(this MongoCollection that)
        {
            return new QueryableMongo(that);
        }
    }
}

Como pode ver, é um extension method simples. Entretanto, colabora com um princípio que considero fundamental:

Sempre considere alternativas para facilitar a compreensão, escrita e manutenção de códigos que utilizem componentes/frameworks que você desenvolve.

Testes e cenários de uso

Como disse no último post, estou usando TDD nesse projeto. Logo, os testes que escrevi foram sendo escritos conforme o código avançava. Entretanto, para facilitar a leitura, apresento todos aqui. Veja:

[Test]
public void BasicOrderByUsingExtensionMethods ()
{
    var setup = QueryableMongo.Create(null)
        .OrderBy(d => d["name"]);

    var sortBy = ((QueryableMongo) setup).GetSortBy();
    sortBy.ToString().Should().Be("{ \"name\" : 1 }");
}

[Test]
public void BasicThenByUsingExtensionMethods()
{
    var setup = QueryableMongo.Create(null)
        .OrderBy(d => d["name"])
        .ThenBy(d => d["age"]);

    var sortBy = ((QueryableMongo)setup).GetSortBy();
    sortBy.ToString().Should().Be("{ \"name\" : 1, \"age\" : 1 }");
}

[Test]
public void BasicOrderByUsingLinqSyntax()
{
    var setup = from d in QueryableMongo.Create(null)
                orderby d["name"]
                select d;

    var sortBy = ((QueryableMongo)setup).GetSortBy();
    sortBy.ToString().Should().Be("{ \"name\" : 1 }");
}

[Test]
public void BasicThenByUsingLinqSyntax()
{
    var setup = from d in QueryableMongo.Create(null)
                orderby d["name"], d["age"]
                select d;

    var sortBy = ((QueryableMongo)setup).GetSortBy();
    sortBy.ToString().Should().Be("{ \"name\" : 1, \"age\" : 1 }");
}

[Test]
public void BasicThenByDescendingUsingLinqSyntax()
{
    var setup = from d in QueryableMongo.Create(null)
                orderby d["name"], d["age"] descending 
                select d;

    var sortBy = ((QueryableMongo)setup).GetSortBy();
    sortBy.ToString().Should().Be("{ \"name\" : 1, \"age\" : -1 }");
}

[Test]
public void BasicOrderByDescendingUsingExtensionMethods()
{
    var setup = QueryableMongo.Create(null)
        .OrderByDescending(d => d["name"]);

    var sortBy = ((QueryableMongo)setup).GetSortBy();
    sortBy.ToString().Should().Be("{ \"name\" : -1 }");
}

[Test]
public void BasicOrderByDescendingUsingLinqSyntax()
{
    var setup = from d in QueryableMongo.Create(null)
                orderby d["name"] descending 
                select d;

    var sortBy = ((QueryableMongo)setup).GetSortBy();
    sortBy.ToString().Should().Be("{ \"name\" : -1 }");
}

Algumas coisas por considerar:

  • Escrevi testes para a notação de métodos e de consulta oferecida pelo LINQ;
  • Desenvolvi um método, chamado GetSortBy, responsável por gerar um objeto, parte do modelo de objetos do Mongo, para descrever a ordenação que estou solicitando.

Extraindo informações de ordenação de uma expression de consulta LINQ

Vamos passo-a-passo. A classe QueryableMongo disponibiliza o método GetSortBy para “pegar” a informação de ordenação. Vejamos como foi implementada:

public SortByBuilder GetSortBy()
{
    return ((MongoQueryProvider) Provider).GetMongoSortByBuilder(Expression);
}

O tipo de retorno é “SortByBuilder” (parte do driver do MongoDB). Entretanto, repare como o processamento em sí é realizado no Provider. Vejamos:

internal SortByBuilder GetMongoSortByBuilder(Expression expression)
{
    var visitor = new Visitor();
    visitor.Visit(expression);
    return visitor.SortByBuilder;
}

Assim como ocorreu com expressões Query, relagamos a “descoberta” do fitro ao Visitor que percorre o objeto. Eis como ele está atualmente:

private class Visitor : ExpressionVisitor
{
    public IMongoQuery Where { get; private set; }
    public MongoCollection Collection { get; private set; }
    public SortByBuilder SortByBuilder { get; private set;  }

    public Visitor()
    {
        SortByBuilder = new SortByBuilder();
        //Where = new BsonDocument();
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        if (node.Value is QueryableMongo)
            Collection = (node.Value as QueryableMongo).Collection;

        return base.VisitConstant(node);
    }

    protected override Expression VisitMethodCall(
        MethodCallExpression node
        )
    {
        var methodName = node.Method.Name;
        if (methodName == "Where")
        {
            Visit(node.Arguments[0]);
            var filter = ((UnaryExpression) node.Arguments[1]).Operand;
            Where = Query.And(
                Where,
                ExpressionToQueryConverter.Convert((Expression<Func>) filter)
                );
            return node;
        }

        if (methodName.StartsWith("OrderBy") || methodName.StartsWith("ThenBy"))
        {
            Visit(node.Arguments[0]);
            var unary = (UnaryExpression) node.Arguments[1];
            var o = (Expression<Func>) unary.Operand;
            var m = (MethodCallExpression) o.Body;
            var c = (ConstantExpression) m.Arguments[0];

            if (methodName.EndsWith("Descending"))
                SortByBuilder.Descending(c.Value.ToString());
            else
                SortByBuilder.Ascending(c.Value.ToString());
                    
            return node;
        }

        throw new NotSupportedException(
            string.Format("'{0}' method is not supported", node.Method.Name)
            );
    }
}

O que fizemos foi adicionar suporte especial para os métodos OrderBy, ThenBy, OrderByDescending e ThenByDescending. Veja que implementamos sempre um “passeio” pelos parâmetros que são passados no método.

Para você entender melhor, entenda que essa consulta,

var q = from d in collection.AsQueryable()
        where d["age"] > 30
        orderby d["age"], d["name"]
        select d;

em notação convencional fica assim:

var q = collection.AsQueryable()
    .Where(d => d["age"] > 30)
    .OrderBy(d => d["age"])
    .ThenBy(d => d["name"]);

E ignorando a facilidade dos Extension Methods, fica assim:

var q = Queryable.ThenBy(
    Queryable.OrderBy(
        Queryable.Where(collection.AsQueryable(), d => d["age"] > 30),
        d => d["age"]
    ), 
    d => d["name"]);

Quando o Visitor começa a inspecionar essa Expression, acaba encontrando, inicialmente, a chamada para o método ThenBy. Então,

  1. Aciona o Visitor recursivamente para inspecionar o primeiro parâmetro. Ou seja: a chamada para o método OrderBy (que volta a esse mesmo ponto);
  2. Pega a expression correspondente ao segundo parâmetro, assumindo que trata-se de um método lambda;
  3. Ignora os parâmetros do Lambda e pega apenas o corpo (Body) da expressão;
  4. Assume que a expressão Lambda conterá apenas uma chamada para o método get_item (pegar o valor da propriedade indexada [como d[“name”]);
  5. Pega o primeiro parâmetro dessa chamada, que deverá ser o nome do campo;
  6. De acordo com o nome do método (OrderBy, ThenBy, ..) define se é uma ordenação ascending ou descending.

Acho que ficou mais claro.

Fazendo a consulta ao Mongo considerando a ordenação

Como indicado no primeiro post, na infraestrutura do Linq, é o método Execute do provider que realiza a interface com o Mongo. Lembre-se que esse método é evocado, sob demanda, sempre que a enumeração é solicitada. Veja:

public TResult Execute(Expression expression)
{
    var visitor = new Visitor();
    visitor.Visit(expression);

    var presult = visitor.Where == null ? 
        visitor.Collection.FindAll() : 
        visitor.Collection.Find(visitor.Where);

    var sortBy = visitor.SortByBuilder;
            
    return (TResult) (object) presult.SetSortOrder(sortBy);
}

Como pode perceber, executo o Visitor para extrair as informações que preciso e utilizo, convencionalmente, o modelo de objetos do Mongo.

Bacana!

Era isso.

Publicado em: Post