The LINQ Provider for MongoDB does not
currently take into account
data projections efficiently when returning data. This could mean that you’re
unnecessarily returning more data from the database than is needed.
So, I’m going to show you the pattern I applied as a replacement for the LINQ
queries when I need to use a projection.
Given the following simple LINQ statement:
var query =
(from r in DataLayer.Database
.GetCollection<Research>()
.AsQueryable<Research>()
where !r.Deleted
select new
{
Id = r.Id,
Title = r.Title,
Created = r.Created
}).Skip(PageSize * page).Take(PageSize);
it can be converted to a MongoCursor style search like this:
var cursor = DataLayer.Database.GetCollection<Research>()
.FindAs<Research>(Query<Research>.NE(r => r.Deleted, true))
.SetFields(
Fields<Research>.Include(
r => r.Id,
r => r.Title,
r => r.Created))
.SetLimit(PageSize)
.SetSkip(PageSize * page);
I’ve attempted to format it in a similar way mostly for my own sanity. As you see,
both queries first get access to the database collection. But, instead of using
AsQueryable<T>, you’ll use the
FindAs<T> method. The query is more verbose in the second
example, although not overly so. I chose to keep it strongly typed by using the
generic version Query<Research>. By doing so, it meant that I
could use an Expression to set the field/property that was being queried, rather
than rely on a string (I could have just passed “Deleted” as a string in the code).
By strongly typing the parameters in this way, it meant that the compile process can
catch things like type mismatches (verifying that the value being compared is a
Boolean for example as is the Deleted property).
Secondly, and this addresses the requirement of a
result projection, I’ve included just those fields that are required by the UI rather than all of
the fields in the document. One of the fields is potentially quite long (up to 1MB
of text), and in this case, unnecessary in a summary list display in my web
application. Here, I used the SetFields method of the MongoCursor.
The C# driver includes a static class called Fields (in a generic
and non-generic form) which can be used to express the set of fields to be
included/excluded. I’ll point out that there is an option to just pass in a list of
strings to SetFields, but it’s not strongly typed. So again, no
compile-time checking that I’ve got the property names correct. I’m going for safety
here, so I chose the strongly-typed generic implementation of
Fields<Research>. Then, using the
Expression syntax again, I’ve selected the fields I needed as a
parameter list.
Finally, I added some code to limit the result size and set the skip equivalent to
the original LINQ query.
There are a number of other Query methods that you can use to build more complex
operations.
For example:
var q = Query.And(
Query<Research>.Exists(r => r.Title),
Query<Research>.Matches(
r => r.Title, BsonRegularExpression.Create(new Regex("^R"))));
The above maps to the following MongoDB query:
{ "$and" : [{ "Title" : { "$exists" : true } }, { "Title" : /^R/ }] }
Title field exists and the Title field starts with an uppercase “R”.
While the Query style syntax is more verbose than the equivalent LINQ statement, the
result still is expressive and very readable and maintainable.
FYI: If there’s an index on Title, then the /^R/ syntax returns the results the most
efficiently
in MongoDB (as it stops searching after the first character).