Yet another article about MONGO!
This article is about retrieving data in mongo and tweaking the retrieval method to accommodate some project requirements. I am not an expert in working with Expression trees, but some methods are quite easy to use and provide a great deal of flexibility in our design.
The mongo driver offers alot in matters of getting data from the store, we can even pass in an Expression<Func<T, bool>> in the getter method of the DbCollection, but this may not also work for us.
The following is the snipped of the ExpressionFilterDefinition as defined in the mongo driver:
Well, what I want to achieve is to use the Expression syntax but i want to only pinpoint to a property, and pass the equaling value sepparatelly. basically making the following conversion:
To be able to make this happen, we need to create a new expression from scratch
- first we need to identity the property from the passed expression
- second we need to make a new expression that returns a boolean
The methods that we use to convert expressions are in code below, and in comments the steps are described:
public static class ExpressionHelpers { //the expression targets a property that is an enumerable. It convers to x=>x.Property.Contains(value) public static Expression<Func<T, bool>> ConvertToListPropertyContains<T, Tkey>(Expression<Func<T, List>> expression, Tkey value) { //a helper method that extracts the path typed : from x=> x.Book.Id it returns Book.Id string propertyPath = PropertyPathHelper.GetFullPropertyPath(expression); if (string.IsNullOrWhiteSpace(propertyPath)) { throw new Exception($"Could not get the property path from the expression {expression}"); } //list of nested properties like ["Book", "Id"] string[] properties = propertyPath.Split("."); //expression start : x=> ParameterExpression parameter = Expression.Parameter(typeof(T), "x"); MemberExpression memberExpression = null; //iterating through the properties list and creating the member expression foreach (string propName in properties) { if (memberExpression == null) { //composing : x=>x.Book. memberExpression = Expression.PropertyOrField(parameter, propName); } else { memberExpression = Expression.PropertyOrField(memberExpression, propName); } } MethodInfo methodInfo = typeof(List).GetMethod("Contains"); //creating the equal condition : x=>x.Book.Ids.Contains(somevalue) MethodCallExpression body = Expression.Call(memberExpression, methodInfo, Expression.Constant(value)); //wrapping up Expression<Func<T, bool>> lambaExpression = Expression.Lambda<Func<T, bool>>(body, parameter); return lambaExpression; } public static Expression<Func<T, bool>> ConvertToEqual<T, Tkey>(Expression<Func<T, Tkey>> expression, Tkey value) { //a helper method that extracts the path typed : from x=> x.Book.Id it returns Book.Id string propertyPath = PropertyPathHelper.GetFullPropertyPath(expression); if (string.IsNullOrWhiteSpace(propertyPath)) { throw new Exception($"Could not get the property path from the expression {expression}"); } //list of nested properties like ["Book", "Id"] string[] properties = propertyPath.Split("."); //expression start : x=> var parameter = Expression.Parameter(typeof(T), "x"); MemberExpression memberExpression = null; //iterating through the properties list and creating the member expression foreach (string propName in properties) { if (memberExpression == null) { //composing : x=>x.Book. memberExpression = Expression.PropertyOrField(parameter, propName); } else { memberExpression = Expression.PropertyOrField(memberExpression, propName); } } //creating the equal condition : x=>x.Book.Id == someValue BinaryExpression body = Expression.Equal(memberExpression, Expression.Constant(value)); //wrapping up Expression<Func<T, bool>> lambaExpression = Expression.Lambda<Func<T, bool>>(body, parameter); return lambaExpression; } }
Now that we have created our helper methods, we can start building our filter classes. They are pretty straight forward because most of the job is done by the helper methods defined above.
The filter classes will receive as input the expression which pin points to a specific property and the value which we are matching with:
public abstract class CustomFilter { public abstract Expression<Func<TEntity, bool>> GetMongoFilter(); public static CustomFilter Equal(Expression<Func<TEntity, TField>> property, TField value) => new EqualFilter<TEntity, TField>(property, value); public static CustomFilter ListContains(Expression<Func<TEntity, List>> property, TField value) => new ListContainsFilter<TEntity, TField>(property, value); } public class EqualFilter<TEntity, TField> : CustomFilter { 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 { public ListContainsFilter(Expression<Func<TEntity, List>> property, TField value) { Property = property; Value = value; } public Expression<Func<TEntity, List>> Property { get; } public TField Value { get; } public override Expression<Func<TEntity, bool>> GetMongoFilter() => ExpressionHelpers.ConvertToListPropertyContains(Property, Value); }
Now that we have everything setup. Let’s define some models and test our methods:
> 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) { this.FirstName = firstName; this.LastName = lastName; this.Id = id; } } public class ClassRoom : Entity { public string ClassName { get; private set; } public List Enrolled { get; private set; } public ClassRoom(string className, Guid id) { Enrolled = new List(); this.ClassName = className; this.Id = id; } public void Enroll(Student person) { this.Enrolled.Add(person.Id); } public void Disenroll(Student person) { this.Enrolled.Remove(person.Id); } }
Finally. In the console application I am inserting some data and using the custom filter to get it from the database. Note that I am using YamlDotNet to print out in a nicely way the information to the screen:
using MongoDB.Driver; using System; using System.Collections.Generic; using YamlDotNet.Serialization; namespace MongoExpressionConvert { class Program { static void Main(string[] args) { IMongoClient mongoClient = new MongoClient(@"mongodb://127.0.0.1:27017/admin"); IMongoDatabase db = mongoClient.GetDatabase("InformalSchool"); IMongoCollection personsCollection = db.GetCollection("Persons"); IMongoCollection classCollection = db.GetCollection("ClassRooms"); personsCollection.DeleteMany(x => true); classCollection.DeleteMany(x => true); Guid student1Id = Guid.Parse("313514c1-420a-4177-98a2-e6ca4b5d6693"); Guid student2Id = Guid.Parse("37b8b5a5-555c-4ec8-bb40-20a185de8624"); Guid student3Id = Guid.Parse("b03c062c-f5dd-4e4a-b668-f9c83c2b914d"); Guid classId = Guid.Parse("518a3918-22c7-4f46-839f-b40666d35c4f"); var classRoom = new ClassRoom("Introduction in .NET", classId); var student1 = new Student("John", "Doe", student1Id); var student2 = new Student("Jack", "Doe", student2Id); var student3 = new Student("Anne", "Doe", student3Id); classRoom.Enroll(student1); classRoom.Enroll(student2); classRoom.Enroll(student3); personsCollection.InsertMany(new List { student1, student2, student3 }); classCollection.InsertOne(classRoom); List persons = personsCollection .Find(CustomFilter.Equal(x => x.FirstName, "Jack").GetMongoFilter()) .ToList(); List classRooms = classCollection .Find(CustomFilter .ListContains(x => x.Enrolled, student1Id) .GetMongoFilter()) .ToList(); Print(persons); Print(classRooms); Console.ReadKey(); } static void Print(object graph) { Serializer serializer = new Serializer(); Console.WriteLine(serializer.Serialize(graph)); } } }