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,
- 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);
- Pega a expression correspondente ao segundo parâmetro, assumindo que trata-se de um método lambda;
- Ignora os parâmetros do Lambda e pega apenas o corpo (Body) da expressão;
- 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”]);
- Pega o primeiro parâmetro dessa chamada, que deverá ser o nome do campo;
- 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.






Alexsandro (@alexsandro_xpt)
25/02/2012
Elemar ja conhecia o http://normproject.org ?
elemarjr
25/02/2012
Sim! Mencionei ele no meu primeiro POST.
Como eu disse, essa é uma série didática. A ideia é aprender mais de Mongo e de LINQ.
[]s
Elemar JR
BTW,
O próprio driver deverá ter suporte a LINQ nas próximas versões.