Olá pessoal. Tudo certo?!
No último post, indiquei como utilizar o driver oficial do MongoDB para C#. Recebi um feedback, pelo twitter, do @breno_ferreira quanto a “sintaxe difícil”, principalmente para consultas. Por causa disso, resolvi começar um “Linq To Mongo”. Ou seja, um provider que permita fazer consultas no Mongo usando Linq (no lugar da API do driver).
Sim! Eu sei que há projetos disponíveis, com esse propósito, no mercado. Aliás, o @vquaiato destaca o NoRM (recomendo que você conheça, use e explore). Entretanto, o objetivo do que vamos fazer aqui é aproveitar a oportunidade de desenvolver código C# avançado.
Esta será uma série didática! Aliás, primarei por mais código e menos texto.
Para escrever esse projeto, estou adotando TDD!
Se quiser dar uma olhada no código completo, consulte o repositório no Github (Está tudo lá). Se sentir vontade de contribuir…
Convertendo expressões C# em Queries Mongo
Utilizando Expressions, podemos “traduzir” código que está escrito de uma forma para outra. Antes de escrevermos consultas em LINQ, considero fundamental que consigamos traduzir expressões lógicas para objetos Query do Mongo.
Veja os testes para entender melhor o que estou tentando explicar:
[Test]
public void SimpleEQ()
{
QueryComplete target = ExpressionToQueryConverter.Convert(d => d["name"] == "John");
QueryComplete q = Query.EQ("name", "John");
target
.ToString()
.Should().Be(q.ToString());
}
[Test]
public void SimpleOR()
{
QueryComplete target = ExpressionToQueryConverter.Convert(
d => d["name"] == "John" || d["name"] == "Mary"
);
QueryComplete q = Query.Or(
Query.EQ("name", "John"),
Query.EQ("name", "Mary")
);
target
.ToString()
.Should().Be(q.ToString());
}
[Test]
public void SimpleAND()
{
var target = ExpressionToQueryConverter.Convert(
d => d["age"] > 10 && d["age"] < 20
);
var q = Query.And(
Query.GT("age", 10),
Query.LT("age", 20)
);
target
.ToString()
.Should().Be(q.ToString());
}
[Test]
public void GTE()
{
var target = ExpressionToQueryConverter.Convert(
d => d["age"] >= 10
);
var q = Query.GTE("age", 10);
target
.ToString()
.Should().Be(q.ToString());
}
[Test]
public void LTE()
{
var target = ExpressionToQueryConverter.Convert(
d => d["age"] <= 10
);
var q = Query.LTE("age", 10);
target
.ToString()
.Should().Be(q.ToString());
}
Como você pode observar:
- A classe Query, do driver do MongoDB, possui diversos métodos para a construção de “critérios de consulta”;
- Em cada um dos testes, estabeleço uma condição de filtro utilizando expressões lambda;
- Em cada um dos testes, crio a consulta usando o modelo de objetos do Mongo;
- Utilizo o método Convert da classe ExpressionToQueryConverter (que vamos desenvolver), para converter nossas expressões lambda em objetos “mongo”.
Veja como ficou a implementação:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
namespace LinqToMongo
{
public static class ExpressionToQueryConverter
{
public static QueryComplete Convert(
Expression<Func<BsonDocument, bool>> expression
)
{
var visitor = new Visitor();
visitor.Visit(expression);
return (QueryComplete) visitor.ResultStack.Pop();
}
#region Nested type: Visitor
#endregion
}
}
Como pode perceber, o método Convert simplesmente “adapta” a chamada de um Visitor para a expressão fornecida. Graças ao conceito de expressions, podemos utilizar código como dado. Veja a implementação do Visitor:
private class Visitor : ExpressionVisitor
{
private readonly Stack resultStackField = new Stack();
public Stack ResultStack
{
get { return resultStackField; }
}
private object VisitAndProcess(Expression node)
{
Visit(node);
return resultStackField.Pop();
}
protected override Expression VisitBinary(BinaryExpression node)
{
resultStackField.Push(CreateQuery(
node.NodeType,
VisitAndProcess(node.Left),
VisitAndProcess(node.Right)
));
return node;
}
private IMongoQuery CreateQuery(ExpressionType type, object left, object right)
{
switch (type)
{
case ExpressionType.Equal:
return Query.EQ((string) left, BsonValue.Create(right));
case ExpressionType.GreaterThan:
return Query.GT((string) left, BsonValue.Create(right));
case ExpressionType.LessThan:
return Query.LT((string) left, BsonValue.Create(right));
case ExpressionType.GreaterThanOrEqual:
return Query.GTE((string) left, BsonValue.Create(right));
case ExpressionType.LessThanOrEqual:
return Query.LTE((string) left, BsonValue.Create(right));
case ExpressionType.AndAlso:
return Query.And((IMongoQuery) left, (IMongoQuery) right);
case ExpressionType.OrElse:
return Query.Or((IMongoQuery) left, (IMongoQuery) right);
}
throw new NotSupportedException(
string.Format("NodeType '{0}' is not supported!", type)
);
}
protected override Expression VisitConstant(ConstantExpression node)
{
resultStackField.Push(node.Value);
return base.VisitConstant(node);
}
}
O “pulo do gato” foi utilizar uma pilha enquanto percorro todos os nodos da expressão. Toda vez que encontro uma expressão binária, processo seus “lados esquerdo e direito” e componho um objeto de consulta (usando o empilhamento como lógica de composição).
Esse código é a “base” da implementação de hoje. Se não entendeu, recomendo que considere gastar algum tempo aqui.
Implementando Queryable pattern
Como já falei em diversos posts aqui do blog, considero o “Queryable pattern” um dos mais belos conceitos já aplicados ao framework. Quando criamos uma consulta em um objeto “Queryable”, ocorre um registro dos métodos em uma expressão no lugar de sua execução direta. Assim, podemos traduzir essa “expressão” em operações de consulta mais nobres como: banco de dados, web services, etc. No nosso exemplo, estaremos armazenando a expressão para conversão há uma consulta Mongo.
Vejamos alguns testes:
[TestFixture]
public class QueryableMongoTests
{
[Test]
public void QueryUsingWhereAsExtensionMethod()
{
var setup = QueryableMongo.Create(null).Where(item => item["age"] > 10);
var q =
Query.GT("age", 10)
;
((QueryableMongo)setup).GetQuery()
.ToString()
.Should().Be(q.ToString());
}
[Test]
public void QueryUsingWhereAsLinqSyntax()
{
var setup = from item in QueryableMongo.Create(null)
where item["age"] > 10
select item;
var q =
Query.GT("age", 10)
;
((QueryableMongo)setup).GetQuery()
.ToString()
.Should().Be(q.ToString());
}
[Test]
public void AndQueryUsingWhereAsLinqSyntax()
{
var setup = from item in QueryableMongo.Create(null)
where item["age"] > 10 && item["age"] <= 25
select item;
var q = Query.And(
Query.GT("age", 10),
Query.LTE("age", 25)
);
((QueryableMongo)setup).GetQuery()
.ToString()
.Should().Be(q.ToString());
}
[Test]
public void AndQueryUsingWhereAsChainedExtensionMethods()
{
var setup = QueryableMongo.Create(null)
.Where(item => item["age"] > 10)
.Where(item => item["age"] <= 25);
var q = Query.And(
Query.GT("age", 10),
Query.LTE("age", 25)
);
((QueryableMongo)setup).GetQuery()
.ToString()
.Should().Be(q.ToString());
}
[Test]
public void AndQueryUsingWhereAsChainedExtensionMethods2()
{
var setup =
Queryable.Where(
Queryable.Where(
QueryableMongo.Create(null),
item => item["age"] > 10),
item => item["age"] <= 25
);
var q = Query.And(
Query.GT("age", 10),
Query.LTE("age", 25)
);
((QueryableMongo)setup).GetQuery()
.ToString()
.Should().Be(q.ToString());
}
}
Perceba:
- Em todos os testes, instancio um “QueryableMongo” que é a classe que iremos desenvolver;
- Essa classe realiza (indiretamente) a interface IQueryable<T>. Logo, é “alvo” dos Extensions methods definidos na classe Queryable;
- O último teste mostra a aplicação dos “Extensions Methods” em sitaxe convencional;
- O compilador converte a “Linq Syntax” para a sintaxe convencional;
- Como nos testes verifico apenas a sintaxe da consulta gerada, não passo coleção alguma.
Comecemos com a implementação do tipo QueryableMongo:
public class QueryableMongo : IOrderedQueryable<BsonDocument>
{
public QueryableMongo(MongoCollection<BsonDocument> collection)
{
Collection = collection;
Provider = new MongoQueryProvider();
Expression = Expression.Constant(this);
}
public QueryableMongo(IQueryProvider provider, Expression expression)
{
if (provider == null)
{
throw new ArgumentNullException("provider");
}
if (expression == null)
{
throw new ArgumentNullException("expression");
}
if (!typeof(IQueryable<BsonDocument>).IsAssignableFrom(expression.Type))
{
throw new ArgumentOutOfRangeException("expression");
}
Provider = provider;
Expression = expression;
}
#region IOrderedQueryable<BsonDocument> Members
public MongoCollection<BsonDocument> Collection { get; private set; }
public IQueryProvider Provider { get; private set; }
public Expression Expression { get; private set; }
public Type ElementType
{
get { return typeof(BsonDocument); }
}
#endregion
#region Enumerators
public IEnumerator<BsonDocument> GetEnumerator()
{
return (Provider.Execute<IEnumerable<BsonDocument>>(Expression)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return (Provider.Execute<IEnumerable>(Expression)).GetEnumerator();
}
#endregion
public static QueryableMongo Create(MongoCollection<BsonDocument> collection)
{
return new QueryableMongo(collection);
}
public IMongoQuery GetQuery()
{
return ((MongoQueryProvider) Provider).GetMongoWhere(Expression);
}
}
Perceba:
- A interface IOrderedQueryable<T> herda de IQueryable<T>;
- Armazendo a coleção no objeto;
- Instancio um provider, conforme o pattern.
Agora, vejamos a implementação do provider:
using System;
using System.Linq;
using System.Linq.Expressions;
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver.Builders;
namespace LinqToMongo
{
public class MongoQueryProvider : IQueryProvider
{
#region IQueryProvider Members
public IQueryable CreateQuery(Expression expression)
{
return new QueryableMongo(this, expression);
}
public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
{
return new QueryableMongo(this, expression) as IQueryable<TResult>;
}
public object Execute(Expression expression)
{
throw new NotImplementedException();
}
public TResult Execute<TResult>(Expression expression)
{
var visitor = new Visitor();
visitor.Visit(expression);
return (TResult) (object) visitor.Collection.Find(visitor.Where);
}
#endregion
internal IMongoQuery GetMongoWhere(Expression expression)
{
var visitor = new Visitor();
visitor.Visit(expression);
return visitor.Where;
}
#region Nested type: Visitor
private class Visitor : ExpressionVisitor
{
public IMongoQuery Where { get; private set; }
public MongoCollection<BsonDocument> Collection { get; private set; }
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
)
{
if (node.Method.Name == "Where")
{
Visit(node.Arguments[0]);
var filter = ((UnaryExpression) node.Arguments[1]).Operand;
Where = Query.And(
Where,
ExpressionToQueryConverter.Convert((Expression<Func<BsonDocument, bool>>) filter)
);
return node;
}
throw new NotSupportedException(
string.Format("'{0}' method is not supported", node.Method.Name)
);
}
}
#endregion
}
}
Veja que:
- Novamente utilizo um Visitor para traduzir a consulta, quando necessário;
- Inspeciono especificamente o método Where (outros operadores ficam para a sequência da série);
Consumindo nosso Query Provider
Para concluir, mostrei como testar a montagem da expressão. Agora vejamos um exemplo prático de uso:
using System;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Driver;
namespace LinqToMongo.Demo
{
internal class Program
{
private static void Main(string[] args)
{
MongoServer server = MongoServer.Create("mongodb://localhost");
server.Connect();
MongoDatabase db = server.GetDatabase("demo");
MongoCollection<BsonDocument> collection = db.GetCollection("people");
collection.Insert(new BsonDocument
{
{"name", "Jonny"},
{"age", 32}
});
collection.Insert(new BsonDocument
{
{"name", "Mary"},
{"age", 35}
});
collection.Insert(new BsonDocument
{
{"name", "Kant"},
{"age", 13}
});
IQueryable<BsonDocument> q = from d in QueryableMongo.Create(collection)
where d["age"] > 30
select d;
foreach (BsonDocument p in q)
Console.WriteLine("{0} (Age={1})", p["name"], p["age"]);
server.DropDatabase("demo");
server.Disconnect();
Console.ReadLine();
}
}
}
Bonito, não?!
Palavras finais…
Como disse no início, esse post, na minha opinião, não tem código trivial (infelizmente). Além disso, sinto que não fui muito feliz explicando. Entretanto, acho que o código é bacana e bem compreensível. Se examinar o Github poderá ver “meu roteiro” de desenvolvimento.
Era isso.
![]()






Rafael Mueller
25/02/2012
Sobre o NoRM, foi um projeto legal, hoje já está ‘morto’.
http://groups.google.com/group/norm-mongodb/browse_thread/thread/ca41ac83b805188?pli=1