The “compiled query” feature is a brand new addition to Marten (as of v0.8.9), and one we think will have some very positive impact on the performance of our systems. I’m also hopeful that using this feature will make some of our internal code easier to read and understand. Down the line, the combination of compiled queries with the batch querying support should be the foundation of a decent dynamic, aggregated query mechanism to support our React/Redux based client architectures (with a server side implementation of Falcor maybe?). Big thanks and a shoutout to Corey Kaylor for adding this feature.
Linq is easily one of the most popular features in .Net and arguably the one thing that other platforms strive to copy. We generally like being able to express document queries in compiler-safe manner, but there is a non-trivial cost in parsing the resulting Expression trees and then using plenty of string concatenation to build up the matching SQL query. Fortunately, as of v0.8.10, Marten supports the concept of a Compiled Query that you can use to reuse the SQL template for a given Linq query and bypass the performance cost of continuously parsing Linq expressions.
All compiled queries are classes that implement the ICompiledQuery<TDoc, TResult> interface shown below:
public interface ICompiledQuery<TDoc, TOut>
{
Expression<Func<IQueryable<TDoc>, TOut>> QueryIs();
}
In its simplest usage, let’s say that we want to find the first user document with a certain first name. That class would look like this:
public class FindByFirstName : ICompiledQuery<User, User>
{
public string FirstName { get; set; }
public Expression<Func<IQueryable<User>, User>> QueryIs()
{
return q => q.FirstOrDefault(x => x.FirstName == FirstName);
}
}
So a couple things to note in the class above:
- The
QueryIs()method returns an Expression representing a Linq query FindByFirstNamehas a property (it could also be just a public field) calledFirstNamethat is used to express the filter of the query
To use the FindByFirstName query, just use the code below:
var justin = theSession.Query(new FindByFirstName {FirstName = "Justin"});
var tamba = await theSession.QueryAsync(new FindByFirstName {FirstName = "Tamba"});
Or to use it as part of a batched query, this syntax:
var batch = theSession.CreateBatchQuery();
var justin = batch.Query(new FindByFirstName {FirstName = "Justin"});
var tamba = batch.Query(new FindByFirstName {FirstName = "Tamba"});
await batch.Execute();
(await justin).Id.ShouldBe(user1.Id);
(await tamba).Id.ShouldBe(user2.Id);
How does it work?
The first time that Marten encounters a new type of ICompiledQuery, it executes the QueryIs() method and:
- Parses the Expression just to find which property getters or fields are used within the expression as input parameters
- Parses the Expression with our standard Linq support and to create a template database command and the internal query handler
- Builds up an object with compiled Func’s that “knows” how to read a query model object and set the command parameters for the query
- Caches the resulting “plan” for how to execute a compiled query
On subsequent usages, Marten will just reuse the existing SQL command and remembered handlers to execute the query.
What is supported?
To the best of our knowledge and testing, you may use any Linq feature that Marten supports within a compiled query. So any combination of:
Select()transformsFirst/FirstOrDefault()Single/SingleOrDefault()Where()OrderBy/OrderByDescendingetc.Count()Any()
At this point (v0.9), the only limitations are:
- You cannot yet incorporate the Include’s feature with compiled queries, but there is an open GitHub issue you can use to track progress on adding this feature.
- You cannot use the Linq
ToArray()orToList()operators. See the next section for an explanation of how to query for multiple results
Querying for multiple results
To query for multiple results, you need to just return the raw IQueryable<T> as IEnumerable<T> as the result type. You cannot use the ToArray() or ToList() operators (it’ll throw exceptions from the Relinq library if you try). As a convenience mechanism, Marten supplies these helper interfaces:
If you are selecting the whole document without any kind of Select() transform, you can use this interface:
public interface ICompiledListQuery<TDoc> : ICompiledListQuery<TDoc, TDoc>
{
}
A sample usage of this type of query is shown below:
public class UsersByFirstName : ICompiledListQuery<User>
{
public static int Count;
public string FirstName { get; set; }
public Expression<Func<IQueryable<User>, IEnumerable<User>>> QueryIs()
{
// Ignore this line, it's from a unit test;)
Count++;
return query => query.Where(x => x.FirstName == FirstName);
}
}
If you do want to use a Select() transform, use this interface:
public interface ICompiledListQuery<TDoc, TOut> : ICompiledQuery<TDoc, IEnumerable<TOut>>
{
}
A sample usage of this type of query is shown below:
public class UserNamesForFirstName : ICompiledListQuery<User, string>
{
public Expression<Func<IQueryable<User>, IEnumerable<string>>> QueryIs()
{
return q => q
.Where(x => x.FirstName == FirstName)
.Select(x => x.UserName);
}
public string FirstName { get; set; }
}
Querying for a single document
Finally, if you are querying for a single document with no transformation, you can use this interface as a convenience:
public interface ICompiledQuery<TDoc> : ICompiledQuery<TDoc, TDoc>
{
}
And an example:
public class FindUserByAllTheThings : ICompiledQuery<User>
{
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Expression<Func<IQueryable<User>, User>> QueryIs()
{
return query =>
query.Where(x => x.FirstName == FirstName && Username == x.UserName)
.Where(x => x.LastName == LastName)
.Single();
}
}