Custom Filters — Transforming Expressions — Digitteck
Custom Filters — Transforming Expressions
dotnet·3 February 2020·4 min read

Custom Filters — Transforming Expressions

The MongoDB C# driver accepts Expression<Func<T, bool>> directly in collection find methods via ExpressionFilterDefinition. But sometimes the requirement is to accept a property selector separately from the value — for example, a reusable filter that receives x =>> x.FirstName and "John" as separate arguments, then builds the equality expression internally.

Expression trees make this possible without reflection strings. Two conversions cover the common cases: equality and list-contains.

ExpressionHelpers

A PropertyPathHelper.GetFullPropertyPath utility extracts the dotted property path from a selector expression (e.g.x =>> x.Book.Id Book.Id). From there, we rebuild the expression tree step by step:

csharp
public static class ExpressionHelpers
{
    // Converts x => x.Property.Ids  +  someValue  →  x => x.Property.Ids.Contains(someValue)
    public static Expression<Func<T, bool>> ConvertToListPropertyContains<T, TKey>(
        Expression<Func<T, List<TKey>>> expression, TKey value)
    {
        string propertyPath = PropertyPathHelper.GetFullPropertyPath(expression);
        if (string.IsNullOrWhiteSpace(propertyPath))
            throw new Exception(
quot;Could not get the property path from {expression}"
); string[] properties = propertyPath.Split('.'); ParameterExpression parameter = Expression.Parameter(typeof(T), "x"); MemberExpression memberExpression = null; foreach (string propName in properties) { memberExpression = memberExpression == null ? Expression.PropertyOrField(parameter, propName) : Expression.PropertyOrField(memberExpression, propName); } MethodInfo containsMethod = typeof(List<TKey>).GetMethod("Contains"); MethodCallExpression body = Expression.Call( memberExpression, containsMethod, Expression.Constant(value)); return Expression.Lambda<Func<T, bool>>(body, parameter); } // Converts x => x.Property + someValue → x => x.Property == someValue public static Expression<Func<T, bool>> ConvertToEqual<T, TKey>( Expression<Func<T, TKey>> expression, TKey value) { string propertyPath = PropertyPathHelper.GetFullPropertyPath(expression); if (string.IsNullOrWhiteSpace(propertyPath)) throw new Exception(
quot;Could not get the property path from {expression}"
); string[] properties = propertyPath.Split('.'); ParameterExpression parameter = Expression.Parameter(typeof(T), "x"); MemberExpression memberExpression = null; foreach (string propName in properties) { memberExpression = memberExpression == null ? Expression.PropertyOrField(parameter, propName) : Expression.PropertyOrField(memberExpression, propName); } BinaryExpression body = Expression.Equal(memberExpression, Expression.Constant(value)); return Expression.Lambda<Func<T, bool>>(body, parameter); } }

CustomFilter

Two concrete filter types — equality and list-contains — both delegate the expression construction to ExpressionHelpers. A static factory on the abstract base provides a clean call site:

csharp
public abstract class CustomFilter<TEntity>
{
    public abstract Expression<Func<TEntity, bool>> GetMongoFilter();

    public static CustomFilter<TEntity> Equal<TField>(
        Expression<Func<TEntity, TField>> property, TField value)
        => new EqualFilter<TEntity, TField>(property, value);

    public static CustomFilter<TEntity> ListContains<TField>(
        Expression<Func<TEntity, List<TField>>> property, TField value)
        => new ListContainsFilter<TEntity, TField>(property, value);
}

public class EqualFilter<TEntity, TField> : CustomFilter<TEntity>
{
    public EqualFilter(Expression<Func<TEntity, TField>> property, TField value)
    {
        Property = property;
        Value    = value;
    }

    public Expression<Func<TEntity, TField>> Property { get; }
    public TField Value { get; }

    public override Expression<Func<TEntity, bool>> GetMongoFilter()
        => ExpressionHelpers.ConvertToEqual(Property, Value);
}

public class ListContainsFilter<TEntity, TField> : CustomFilter<TEntity>
{
    public ListContainsFilter(Expression<Func<TEntity, List<TField>>> property, TField value)
    {
        Property = property;
        Value    = value;
    }

    public Expression<Func<TEntity, List<TField>>> Property { get; }
    public TField Value { get; }

    public override Expression<Func<TEntity, bool>> GetMongoFilter()
        => ExpressionHelpers.ConvertToListPropertyContains(Property, Value);
}

Models

csharp
public abstract class Entity
{
    [BsonId]
    [BsonRepresentation(BsonType.String)]
    public Guid Id { get; protected set; }
}

public class Student : Entity
{
    public string FirstName { get; private set; }
    public string LastName  { get; private set; }

    public Student(string firstName, string lastName, Guid id)
    {
        FirstName = firstName;
        LastName  = lastName;
        Id        = id;
    }
}

public class ClassRoom : Entity
{
    public string      ClassName { get; private set; }
    public List<Guid>  Enrolled  { get; private set; }

    public ClassRoom(string className, Guid id)
    {
        Enrolled  = new List<Guid>();
        ClassName = className;
        Id        = id;
    }

    public void Enroll(Student person)    => Enrolled.Add(person.Id);
    public void Disenroll(Student person) => Enrolled.Remove(person.Id);
}

Usage

The filters read exactly like typed lambda expressions. MongoDB translates the resulting LINQ expression into the correct query:

csharp
IMongoClient mongoClient = new MongoClient("mongodb://127.0.0.1:27017/admin");
IMongoDatabase db = mongoClient.GetDatabase("InformalSchool");

IMongoCollection<Student>  personsCollection = db.GetCollection<Student>("Persons");
IMongoCollection<ClassRoom> classCollection  = db.GetCollection<ClassRoom>("ClassRooms");

var student1 = new Student("John", "Doe", Guid.NewGuid());
var classRoom = new ClassRoom("Introduction in .NET", Guid.NewGuid());
classRoom.Enroll(student1);

personsCollection.InsertOne(student1);
classCollection.InsertOne(classRoom);

// Filter by equal field value
List<Student> johns = personsCollection
    .Find(CustomFilter<Student>
        .Equal(x => x.FirstName, "John")
        .GetMongoFilter())
    .ToList();

// Filter by list contains value
List<ClassRoom> rooms = classCollection
    .Find(CustomFilter<ClassRoom>
        .ListContains(x => x.Enrolled, student1.Id)
        .GetMongoFilter())
    .ToList();

Tags

.NETC#MongoDBLINQExpression Trees
digitteck

© 2026 Digitteck