Skip to content

Use this LLM Friendly Docs as an MCP server for Marten.

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Marten and other JasperFx libraries. Please check our Support Plans for more details.

Compiled Queries and Query Plans

Marten has two different implementations for the "Specification" pattern that enable you to encapsulate all the filtering, ordering, and paging for a logically reusable data query into a single class:

  1. Compiled queries that help short circuit the LINQ processing with reusable "execution plans" for maximum performance, but are admittedly limited in terms of their ability to handle all possible LINQ queries or basically any dynamic querying whatsoever
  2. Query plans that can support anything that Marten itself can do, just without the magic LINQ query compilation optimization

Compiled Queries

WARNING

Don't use asynchronous Linq operators in the expression body of a compiled query. This will not impact your ability to use compiled queries in asynchronous querying.

WARNING

Compiled queries cannot use the recently added primary constructor feature in C#, and so far we don't even have a way to validate when you are using this feature in compiled query planning. Be warned.

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, 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:

cs
public interface ICompiledQuery<TDoc, TOut> : ICompiledQueryMarker where TDoc: notnull
{
    Expression<Func<IMartenQueryable<TDoc>, TOut>> QueryIs();
}

snippet source | anchor

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:

cs
public class FindByFirstName: ICompiledQuery<User, User>
{
    public string FirstName { get; set; }

    public Expression<Func<IMartenQueryable<User>, User>> QueryIs()
    {
        return q => q.FirstOrDefault(x => x.FirstName == FirstName);
    }
}

snippet source | anchor

TIP

There are many more example compiled query classes in the acceptance tests for compiled queries within the Marten codebase.

So a couple things to note in the class above:

  1. The QueryIs() method returns an Expression representing a Linq query
  2. FindByFirstName has a property (it could also be just a public field) called FirstName that is used to express the filter of the query

To use the FindByFirstName query, just use the code below:

cs
var justin = await session.QueryAsync(new FindByFirstName { FirstName = "Justin" });

var tamba = await session.QueryAsync(new FindByFirstName { FirstName = "Tamba" });

snippet source | anchor

Or to use it as part of a batched query, this syntax:

cs
var batch = session.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);

snippet source | anchor

Marten 9.0

The source-generated path described in this section ships in Marten 9.0 and is the supported way to use compiled queries. Queries the generator can't see at build time (no [JasperFxAssembly] marker, or shapes the generator skips) fall through to a reflection + FastExpressionCompiler-built descriptor at first use — no Roslyn, no pre-built artifacts.

Marten 9 ships a Roslyn source generator (Marten.SourceGenerator) that emits the per-ICompiledQuery<,> scaffolding at compile time. Opt-in is implicit — add two things to the project that declares your compiled query types and the runtime picks the source-generated path automatically:

xml
<!-- in your .csproj -->
<ItemGroup>
    <PackageReference Include="Marten.SourceGenerator" PrivateAssets="all" />
</ItemGroup>
cs
// in any file in the same assembly (e.g., AssemblyInfo.cs)
[assembly: JasperFx.JasperFxAssembly]

With both present, the generator emits a typed handler for every ICompiledQuery<TDoc, TOut> in the assembly and registers it with the Marten runtime via a [ModuleInitializer] that fires at assembly load.

What this gets you compared to the reflection-built fallback:

  • No FastExpressionCompiler for the per-query parameter binder. The generator emits a direct property-read switch — ~31% faster steady-state per call.
  • AOT-publishable for the common cases. No dynamic-codegen surface for queries the source-gen path covers.

The runtime builds the descriptor reflectively (no Roslyn — FastExpressionCompiler + a manual fallback inside RuntimeCompiledQueryDescriptorFactory) in three documented cases:

  • Plans whose SQL needs an ICompiledQueryAwareFilter (string Contains/StartsWith/EndsWith, HashSet<T>.Contains with JSONB containment, Dictionary<,>.ContainsKey, child-collection JsonPath counts).
  • Generic or nested ICompiledQuery<,> types — the generator skips both shapes for now.
  • Compiled queries declared in an assembly without [JasperFxAssembly].

The source generator is additive — adding it never breaks an existing compiled query; removing it just shifts that query to the reflection-built fallback at first use. Tracking issue: #4405.

How Does It Work?

The first time that Marten encounters a new type of ICompiledQuery, it builds a "plan" for the query by:

  1. Finding all public readable properties or fields on the compiled query type that would be potential parameters. Members marked with the [MartenIgnore] attribute are skipped.
  2. Either confirming that the query object you passed in has unique values across each parameter member, or constructing a new instance of the same type and assigning unique values itself.
  3. Parsing the expression returned from QueryIs() to derive the SQL command + the parameter slots Marten needs to fill at execution time.
  4. Matching the unique member values back to the command's parameter values to map query members to database parameters by index.

How Marten then dispatches the per-call hot path depends on whether the source generator is in play:

  • Source-generated dispatch (default when the Marten.SourceGenerator analyzer reference + [assembly: JasperFxAssembly] are present, see Recommended setup above). The generator emits a typed {Query}_CompiledQueryHandler class for every ICompiledQuery<,> in the assembly, plus a [ModuleInitializer] that registers the handler with Marten's runtime CompiledQueryHandlerRegistry at assembly load. When Marten sees the registered query type, it routes through the generator-emitted parameter binder (direct field/property reads — no reflection at runtime).
  • Reflection-built fallback. When the source generator hasn't registered a descriptor for the query type — assembly without [JasperFxAssembly], shape the generator skips, or runtime-registered query — Marten walks the freshly-built plan reflectively, compiles a per-query parameter binder via FastExpressionCompiler, and caches the resulting descriptor in the same registry. No Roslyn, no pre-built artifacts.

On subsequent calls to the same compiled query type, both paths just reuse the cached handler — the SQL command + parameter binder are remembered for the life of the DocumentStore.

You may need to help Marten out a little bit with the compiled query support in determining unique parameter values to use during query planning by implementing the new Marten.Linq.IQueryPlanning interface on your compiled query type. Consider this example query that uses paging:

cs
public class CompiledTimeline : ICompiledListQuery<TimelineItem>, IQueryPlanning
{
    public int PageSize { get; set; } = 20;

    [MartenIgnore] public int Page { private get; set; } = 1;
    public int SkipCount => (Page - 1) * PageSize;
    public string Type { get; set; }
    public Expression<Func<IMartenQueryable<TimelineItem>, IEnumerable<TimelineItem>>> QueryIs() =>
        query => query.Where(i => i.Event == Type).Skip(SkipCount).Take(PageSize);

    public void SetUniqueValuesForQueryPlanning()
    {
        Page = 3; // Setting Page to 3 forces the SkipCount and PageSize to be different values
        PageSize = 20; // This has to be a positive value, or the Take() operator has no effect
        Type = Guid.NewGuid().ToString();
    }

    // And hey, if you have a public QueryStatistics member on your compiled
    // query class, you'll get the total number of records
    public QueryStatistics Statistics { get; } = new QueryStatistics();
}

snippet source | anchor

Pay close attention to the SetUniqueValuesForQueryPlanning() method. That has absolutely no other purpose but to help Marten create a compiled query plan for the CompiledTimeline type.

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() transforms
  • First/FirstOrDefault()
  • Single/SingleOrDefault()
  • Where()
  • Include()
  • OrderBy/OrderByDescending etc.
  • Count()
  • Any()
  • AsJson()
  • ToJsonArray()
  • Skip(), Take() and Stats() for pagination

As for limitations,

  • You cannot use the Linq ToArray() or ToList() operators. See the next section for an explanation of how to query for multiple results with ICompiledListQuery.
  • The compiled query planning just cannot match Boolean fields or properties to command arguments, so Boolean flags cannot be used
  • You cannot use any asynchronous operators. So in all cases, use the synchronous operator equivalent. So FirstOrDefault(), but not FirstOrDefaultAsync(). This does not preclude you from using compiled queries in asynchronous querying

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:

cs
public interface ICompiledListQuery<TDoc>: ICompiledListQuery<TDoc, TDoc> where TDoc : notnull
{
}

snippet source | anchor

A sample usage of this type of query is shown below:

cs
public class UsersByFirstName: ICompiledListQuery<User>
{
    public static int Count;
    public string FirstName { get; set; }

    public Expression<Func<IMartenQueryable<User>, IEnumerable<User>>> QueryIs()
    {
        return query => query.Where(x => x.FirstName == FirstName);
    }
}

snippet source | anchor

If you do want to use a Select() transform, use this interface:

cs
public interface ICompiledListQuery<TDoc, TOut>: ICompiledQuery<TDoc, IEnumerable<TOut>> where TDoc : notnull
{
}

snippet source | anchor

A sample usage of this type of query is shown below:

cs
public class UserNamesForFirstName: ICompiledListQuery<User, string>
{
    public Expression<Func<IMartenQueryable<User>, IEnumerable<string>>> QueryIs()
    {
        return q => q
            .Where(x => x.FirstName == FirstName)
            .Select(x => x.UserName);
    }

    public string FirstName { get; set; }
}

snippet source | anchor

If you wish to use a compiled query for a document, using a JOIN so that the query will include another document, just as the Include() method does on a simple query, the compiled query would be constructed just like any other, using the Include() method on the query:

cs
[Fact]
public async Task simple_compiled_include_for_a_single_document()
{
    var user = new User();
    var issue = new Issue { AssigneeId = user.Id, Title = "Garage Door is busted" };

    using var session = theStore.IdentitySession();
    session.Store<object>(user, issue);
    await session.SaveChangesAsync();

    using var query = theStore.QuerySession();
    var issueQuery = new IssueByTitleWithAssignee { Title = issue.Title };
    var issue2 = await query.QueryAsync(issueQuery);

    issueQuery.Included.ShouldNotBeNull();
    issueQuery.Included.Single().Id.ShouldBe(user.Id);

    issue2.ShouldNotBeNull();
}

public class IssueByTitleWithAssignee: ICompiledQuery<Issue>
{
    public string Title { get; set; }
    public IList<User> Included { get; private set; } = new List<User>();

    public Expression<Func<IMartenQueryable<Issue>, Issue>> QueryIs()
    {
        return query => query
            .Include(x => x.AssigneeId, Included)
            .Single(x => x.Title == Title);
    }
}

snippet source | anchor

In this example, the query has an Included property which will receive the included Assignee / User. The 'resulting' included property can only be a property of the query, so that Marten would know how to assign the included result of the postgres query. The JoinType property here is just an example for overriding the default INNER JOIN. If you wish to force an INNER JOIN within the query you can simply remove the JoinType parameter like so: .Include<Issue, IssueByTitleWithAssignee>(x => x.AssigneeId, x => x.Included)

You can also chain Include methods if you need more than one JOINs.

Fetching "included" documents could also be done when you wish to include multiple documents. So picking up the same example, if you wish to get a list of Issues and for every Issue you wish to retrieve its' Assignee / User, in your compiled query you should have a list of Users like so:

cs
public class IssueWithUsers: ICompiledListQuery<Issue>
{
    public List<User> Users { get; set; } = new List<User>();
    // Can also work like that:
    //public List<User> Users => new List<User>();

    public Expression<Func<IMartenQueryable<Issue>, IEnumerable<Issue>>> QueryIs()
    {
        return query => query.Include(x => x.AssigneeId, Users);
    }
}

[Fact]
public async Task compiled_include_to_list()
{
    var user1 = new User();
    var user2 = new User();

    var issue1 = new Issue { AssigneeId = user1.Id, Title = "Garage Door is busted" };
    var issue2 = new Issue { AssigneeId = user2.Id, Title = "Garage Door is busted" };
    var issue3 = new Issue { AssigneeId = user2.Id, Title = "Garage Door is busted" };

    using var session = theStore.IdentitySession();
    session.Store(user1, user2);
    session.Store(issue1, issue2, issue3);
    await session.SaveChangesAsync();

    using var querySession = theStore.QuerySession();
    var compiledQuery = new IssueWithUsers();

    var issues = await querySession.QueryAsync(compiledQuery);

    compiledQuery.Users.Count.ShouldBe(2);
    issues.Count().ShouldBe(3);

    compiledQuery.Users.Any(x => x.Id == user1.Id);
    compiledQuery.Users.Any(x => x.Id == user2.Id);
}

snippet source | anchor

Note that you could either have the list instantiated or at least make sure the property has a setter as well as a getter (we've got your back).

As with the simple include queries, you could also use a Dictionary with a key type corresponding to the Id of the document- the dictionary value type:

cs
public class IssueWithUsersById: ICompiledListQuery<Issue>
{
    public IDictionary<Guid, User> UsersById { get; set; } = new Dictionary<Guid, User>();
    // Can also work like that:
    //public List<User> Users => new Dictionary<Guid,User>();

    public Expression<Func<IMartenQueryable<Issue>, IEnumerable<Issue>>> QueryIs()
    {
        return query => query.Include(x => x.AssigneeId, UsersById);
    }
}

[Fact]
public async Task compiled_include_to_dictionary()
{
    var user1 = new User();
    var user2 = new User();

    var issue1 = new Issue { AssigneeId = user1.Id, Title = "Garage Door is busted" };
    var issue2 = new Issue { AssigneeId = user2.Id, Title = "Garage Door is busted" };
    var issue3 = new Issue { AssigneeId = user2.Id, Title = "Garage Door is busted" };

    using var session = theStore.IdentitySession();
    session.Store(user1, user2);
    session.Store(issue1, issue2, issue3);
    await session.SaveChangesAsync();

    using var querySession = theStore.QuerySession();
    var compiledQuery = new IssueWithUsersById();

    var issues = await querySession.QueryAsync(compiledQuery);

    issues.ShouldNotBeEmpty();

    compiledQuery.UsersById.Count.ShouldBe(2);
    compiledQuery.UsersById.ContainsKey(user1.Id).ShouldBeTrue();
    compiledQuery.UsersById.ContainsKey(user2.Id).ShouldBeTrue();
}

snippet source | anchor

Querying for Paginated Results

Marten compiled queries also support queries for paginated results, where you could specify the page number and size, as well as getting the total count. A simple example of how this can be achieved as follows:

cs
public class TargetPaginationQuery: ICompiledListQuery<Target>
{
    public TargetPaginationQuery(int pageNumber, int pageSize)
    {
        PageNumber = pageNumber;
        PageSize = pageSize;
    }

    public int PageNumber { get; set; }
    public int PageSize { get; set; }

    public QueryStatistics Stats { get; } = new QueryStatistics();

    public Expression<Func<IMartenQueryable<Target>, IEnumerable<Target>>> QueryIs()
    {
        return query => query
            .Where(x => x.Number > 10)
            .Skip(PageNumber)
            .Take(PageSize);
    }
}

snippet source | anchor

Note that the way to get the QueryStatistics out is done by having a property on the query, which we specify in the Stats() method, similarly to the way we handle Include queries.

Querying for a Single Document

If you are querying for a single document with no transformation, you can use this interface as a convenience:

cs
public interface ICompiledQuery<TDoc>: ICompiledQuery<TDoc, TDoc> where TDoc : notnull
{
}

snippet source | anchor

And an example:

cs
public class FindUserByAllTheThings: ICompiledQuery<User>
{
    public string Username { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Expression<Func<IMartenQueryable<User>, User>> QueryIs()
    {
        return query =>
            query.Where(x => x.FirstName == FirstName && Username == x.UserName)
                .Where(x => x.LastName == LastName)
                .Single();
    }
}

snippet source | anchor

Querying for Multiple Results as JSON

To query for multiple results and have them returned as a Json string, you may run any query on your IQueryable<T> (be it ordering or filtering) and then simply finalize the query with ToJsonArray(); like so:

cs
public class FindJsonOrderedUsersByUsername: ICompiledListQuery<User>
{
    public string FirstName { get; set; }

    Expression<Func<IMartenQueryable<User>, IEnumerable<User>>> ICompiledQuery<User, IEnumerable<User>>.QueryIs()
    {
        return query =>
            query.Where(x => FirstName == x.FirstName)
                .OrderBy(x => x.UserName);
    }
}

snippet source | anchor

If you wish to do it asynchronously, you can use the ToJsonArrayAsync() method.

A sample usage of this type of query is shown below:

cs
public class FindJsonOrderedUsersByUsername: ICompiledListQuery<User>
{
    public string FirstName { get; set; }

    Expression<Func<IMartenQueryable<User>, IEnumerable<User>>> ICompiledQuery<User, IEnumerable<User>>.QueryIs()
    {
        return query =>
            query.Where(x => FirstName == x.FirstName)
                .OrderBy(x => x.UserName);
    }
}

snippet source | anchor

Note that the result has the documents comma separated and wrapped in angle brackets (as per the Json notation).

Querying for a Single Document as JSON

Finally, if you are querying for a single document as json, you will need to prepend your call to Single(), First() and so on with a call to AsJson():

cs
public class FindJsonUserByUsername: ICompiledQuery<User>
{
    public string Username { get; set; }

    Expression<Func<IMartenQueryable<User>, User>> ICompiledQuery<User, User>.QueryIs()
    {
        return query =>
            query.Where(x => Username == x.UserName).Single();
    }
}

snippet source | anchor

And an example:

cs
public class FindJsonUserByUsername: ICompiledQuery<User>
{
    public string Username { get; set; }

    Expression<Func<IMartenQueryable<User>, User>> ICompiledQuery<User, User>.QueryIs()
    {
        return query =>
            query.Where(x => Username == x.UserName).Single();
    }
}

snippet source | anchor

(our ToJson() method simply returns a string representation of the User instance in Json notation)

Using with QueryStatistics

Compiled queries can be used with the QueryStatistics paging helper. You just need to have a public member on your compiled query class of type QueryStatistics with a value. Marten will do the rest and use that object to collect the total number of rows in the database when the query is executed. Here's an example from the Marten tests:

cs
public class TargetsInOrder: ICompiledListQuery<Target>
{
    // This is all you need to do
    public QueryStatistics Statistics { get; } = new QueryStatistics();

    public int PageSize { get; set; } = 20;
    public int Start { get; set; } = 5;

    Expression<Func<IMartenQueryable<Target>, IEnumerable<Target>>> ICompiledQuery<Target, IEnumerable<Target>>.
        QueryIs()
    {
        return q => q
            .OrderBy(x => x.Id).Skip(Start).Take(PageSize);
    }
}

snippet source | anchor

And when used in the actual test:

cs
[Fact]
public async Task use_compiled_query_with_statistics()
{
    await theStore.Advanced.Clean.DeleteDocumentsByTypeAsync(typeof(Target));
    var targets = Target.GenerateRandomData(100).ToArray();
    await theStore.BulkInsertAsync(targets);

    var query = new TargetsInOrder { PageSize = 10, Start = 20 };

    var results = await theSession.QueryAsync(query);

    // Verifying that the total record count in the database matching
    // the query is determined when this is executed
    query.Statistics.TotalResults.ShouldBe(100);
}

snippet source | anchor

Query Plans 7.25

INFO

The query plan concept was created specifically to help a JasperFx client try to eliminate their custom repository wrappers around Marten and to better utilize batch querying.

TIP

Batch querying is a great way to improve the performance of your system~~~~

A query plan is another flavor of "Specification" for Marten that just enables you to bundle up query logic that can be reused within your codebase without having to create wrappers around Marten itself. To create a reusable query plan, implement the IQueryPlan<T> interface where T is the type of the result you want. Here's a simplistic sample from the tests:

cs
public class ColorTargets: QueryListPlan<Target>
{
    public Colors Color { get; }

    public ColorTargets(Colors color)
    {
        Color = color;
    }

    // All we're doing here is just turning around and querying against the session
    // All the same though, this approach lets you do much more runtime logic
    // than a compiled query can
    public override IQueryable<Target> Query(IQuerySession session)
    {
        return session.Query<Target>().Where(x => x.Color == Color).OrderBy(x => x.Number);
    }
}

// The above is short hand for:

public class LonghandColorTargets: IQueryPlan<IReadOnlyList<Target>>, IBatchQueryPlan<IReadOnlyList<Target>>
{
    public Colors Color { get; }

    public LonghandColorTargets(Colors color)
    {
        Color = color;
    }

    public Task<IReadOnlyList<Target>> Fetch(IQuerySession session, CancellationToken token)
    {
        return session
            .Query<Target>()
            .Where(x => x.Color == Color)
            .OrderBy(x => x.Number)
            .ToListAsync(token: token);
    }

    public Task<IReadOnlyList<Target>> Fetch(IBatchedQuery batch)
    {
        return batch
            .Query<Target>()
            .Where(x => x.Color == Color)
            .OrderBy(x => x.Number)
            .ToList();
    }
}

snippet source | anchor

And then use that like so:

cs
public static async Task use_query_plan(IQuerySession session, CancellationToken token)
{
    var targets = await session
        .QueryByPlanAsync(new ColorTargets(Colors.Blue), token);
}

snippet source | anchor

There is also a similar interface for usage with batch querying:

cs
/// <summary>
/// Marten's concept of the "Specification" pattern for reusable
/// queries within Marten batched queries. Use this for operations that cannot be supported by Marten compiled queries
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IBatchQueryPlan<T>
{
    Task<T> Fetch(IBatchedQuery query);
}

snippet source | anchor

And because we expect this to be very common, there is convenience base class named QueryListPlan<T> for querying lists of T data that can be used for both querying directly against an IQuerySession and for batch querying. The usage within a batched query is shown below from the Marten tests:

cs
[Fact]
public async Task use_as_batch()
{
    await theStore.Advanced.Clean.DeleteDocumentsByTypeAsync(typeof(Target));

    var targets = Target.GenerateRandomData(1000).ToArray();
    await theStore.BulkInsertDocumentsAsync(targets);

    // Start a batch query
    var batch = theSession.CreateBatchQuery();

    // Using the ColorTargets plan twice, once for "Blue" and once for "Green" target documents
    var blueFetcher = batch.QueryByPlan(new ColorTargets(Colors.Blue));
    var greenFetcher = batch.QueryByPlan(new ColorTargets(Colors.Green));

    // Execute the batch query
    await batch.Execute();

    // The batched querying in Marten is essentially registering a "future"
    // for each query, so we'll await each task from above to get at the actual
    // data returned from batch.Execute() above
    var blues = await blueFetcher;
    var greens = await greenFetcher;

    // And the assertion part of our arrange, act, assertion test
    blues.ShouldNotBeEmpty();
    greens.ShouldNotBeEmpty();

    var expectedBlues = targets.Where(x => x.Color == Colors.Blue).OrderBy(x => x.Number);
    var expectedReds = targets.Where(x => x.Color == Colors.Green).OrderBy(x => x.Number);

    blues.Select(x => x.Id).ShouldBe(expectedBlues.Select(x => x.Id));
    greens.Select(x => x.Id).ShouldBe(expectedReds.Select(x => x.Id));
}

snippet source | anchor

Released under the MIT License.