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:
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:
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
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:
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();