Skip to content

Extending Marten's Linq Support

INFO

The Linq parsing and translation to Postgresql JSONB queries, not to mention Marten's own helpers and model, are pretty involved and this guide isn't exhaustive. Please feel free to ask for help in Marten's Discord channel linked above if there's any Linq customization or extension that you need.

Marten allows you to add Linq parsing and querying support for your own custom methods. Using the (admittedly contrived) example from Marten's tests, say that you want to reuse a small part of a Where() clause across different queries for "IsBlue()." First, write the method you want to be recognized by Marten's Linq support:

cs
public class IsBlue: IMethodCallParser
{
    private static readonly PropertyInfo _property = ReflectionHelper.GetProperty<ColorTarget>(x => x.Color);

    public bool Matches(MethodCallExpression expression)
    {
        return expression.Method.Name == nameof(CustomExtensions.IsBlue);
    }

    public ISqlFragment Parse(IQueryableMemberCollection memberCollection, IReadOnlyStoreOptions options,
        MethodCallExpression expression)
    {
        var locator = memberCollection.MemberFor(expression).TypedLocator;

        return new WhereFragment($"{locator} = 'Blue'");
    }
}

snippet source | anchor

Note a couple things here:

  1. If you're only using the method for Linq queries, it technically doesn't have to be implemented and never actually runs
  2. The methods do not have to be extension methods, but we're guessing that will be the most common usage of this

Now, to create a custom Linq parser for the IsBlue() method, you need to create a custom implementation of the IMethodCallParser interface shown below:

cs
using System.Linq.Expressions;
using Marten.Linq.Members;
using Weasel.Postgresql.SqlGeneration;

namespace Marten.Linq.Parsing;

#region sample_IMethodCallParser

/// <summary>
///     Models the Sql generation for a method call
///     in a Linq query. For example, map an expression like Where(x => x.Property.StartsWith("prefix"))
///     to part of a Sql WHERE clause
/// </summary>
public interface IMethodCallParser
{
    /// <summary>
    ///     Can this parser create a Sql where clause
    ///     from part of a Linq expression that calls
    ///     a method
    /// </summary>
    /// <param name="expression"></param>
    /// <returns></returns>
    bool Matches(MethodCallExpression expression);

    /// <summary>
    ///     Creates an ISqlFragment object that Marten
    ///     uses to help construct the underlying Sql
    ///     command
    /// </summary>
    /// <param name="memberCollection"></param>
    /// <param name="options"></param>
    /// <param name="expression"></param>
    /// <param name="serializer"></param>
    /// <returns></returns>
    ISqlFragment Parse(IQueryableMemberCollection memberCollection, IReadOnlyStoreOptions options,
        MethodCallExpression expression);
}

#endregion

The IMethodCallParser interface needs to match on method expressions that it could parse, and be able to turn the Linq expression into part of a Postgresql "where" clause. The custom Linq parser for IsBlue() is shown below:

cs
public static bool IsBlue(this string value)
{
    return value == "Blue";
}

snippet source | anchor

Lastly, to plug in our new parser, we can add that to the StoreOptions object that we use to bootstrap a new DocumentStore as shown below:

cs
[Fact]
public async Task query_with_custom_parser()
{
    using var store = DocumentStore.For(opts =>
    {
        opts.Connection(ConnectionSource.ConnectionString);

        // IsBlue is a custom parser I used for testing this
        opts.Linq.MethodCallParsers.Add(new IsBlue());
        opts.AutoCreateSchemaObjects = AutoCreate.All;

        // This is just to isolate the test
        opts.DatabaseSchemaName = "isblue";
    });

    await store.Advanced.Clean.CompletelyRemoveAllAsync();

    var targets = new List<ColorTarget>();
    for (var i = 0; i < 25; i++)
    {
        targets.Add(new ColorTarget {Color = "Blue"});
        targets.Add(new ColorTarget {Color = "Green"});
        targets.Add(new ColorTarget {Color = "Red"});
    }

    var count = targets.Count(x => x.Color.IsBlue());

    targets.Each(x => x.Id = Guid.NewGuid());

    await store.BulkInsertAsync(targets.ToArray());

    using var session = store.QuerySession();
    session.Query<ColorTarget>().Count(x => x.Color.IsBlue())
        .ShouldBe(count);
}

snippet source | anchor

Released under the MIT License.