Building a MongoDB Filtering API in C#

One of the projects I’m working on uses MongoDB for data storage. What I found was that I often needed to do complex queries against this data, but didn’t want to have MongoDB driver code scattered throughout other layers of my project. The MongoDB-specific code should only exist in the layer responsible for actually querying the data. This would allow me to some day replace MongoDB with some other database without having to rewrite code all over my application.

Imagine I’m trying to find restaurants in my data store with specific tags and a location (Japanese bars in Seattle). What I wanted to be able to do, was write code that looked like this:

var results = new RestaurantAccess().Search(p => p  
                .Filter<TagFilter>(f => f.Tags.AddRange(new string[] { "sushi", "izakaya" }))
                .Filter<LocationFilter>(f => { f.Latitude = 47.6097; f.Longitude = -122.3331; }));

This code is completely agnostic from whatever my data storage is. I could replace MongoDB with SQL Server or Redis or RavenDB or whatever I want and not have to rewrite this code. I would have to write a new implementation of the filter API, specific to the new data storage platform, but that’s it.

Okay, so let’s do it. First, we want to define what a filter looks like:

    public abstract class Filter
    {
        public abstract IMongoQuery GetQuery();
    }

Easy enough. All filters (TagFilter, LocationFilter, etc) need to return a IMongoQuery instance. But now we need a way to chain filters together. We can do this with what I call a FilterPipe.

    public class FilterPipe
    {
        public FilterPipe()
        {
            this.Filters = new List<Filter>();
        }

        public List<Filter> Filters { get; set; }

        public FilterPipe Filter<T>(Action<T> action) where T : Filter, new()
        {
            T f = new T();
            action(f);
            this.Filters.Add(f);
            return this;
        }
    }

The FilterPipe is just an abstract class that acts as a container, or a fancy list, for all the filters we’re chaining together. The Filter method is what allows us to add new filters in a chained syntax, similar to using LINQ.

So, what does an actual filter implementation look like? Here’s our TagFilter:

    public class TagFilter : Filter
    {
        public TagFilter()
        {
            this.Tags = new List<string>();
        }

        public List<string> Tags { get; set; }

        public override MongoDB.Driver.IMongoQuery GetQuery()
        {
            if (this.Tags.Count > 0)
            {
                Query.In("Tags", new MongoDB.Bson.BsonArray(this.Tags));
            }
            return null;
        }
    }

Here’s our slightly more complex LocationFilter:

    public class LocationFilter : Filter
    {
        public LocationFilter()
        {
            this.Distance = 10;
        }

        public double Latitude { get; set; }

        public double Longitude { get; set; }

        /// <summary>
        /// radius distance in kilometers
        /// </summary>
        public double Distance { get; set; }

        public override MongoDB.Driver.IMongoQuery GetQuery()
        {
            return Query.Near("loc", this.Latitude, this.Longitude, (this.Distance / 111.12));
        }
    }

If you noticed in my first block of code, I have a class called RestaurantAccess. I won’t get into the details of this because it’s a pretty generic data-access class for connecting to a MongoDB instance and executing a query. You can find examples of doing that lots of other places.

What I will talk about, however, is the Search method on the RestaurantAccess class, which is the method that I call to get back my search results. It looks like this:

public List<T> Search(Action<FilterPipe> pipeAction)
{
    FilterPipe pipe = new FilterPipe();
    pipeAction(pipe);

    var collection = this.GetCollection();
    IMongoQuery query = new QueryDocument();
    foreach (var f in pipe.Filters)
    {
        var q = f.GetQuery();
        if(q != null)
            query = Query.And(query, q);
    }

    return collection.Find(query).ToList();
}

If you break it down, it’s actually pretty simple. We construct a new FilterPipe and call the Action delegate that the user passed in. This is where the calling user would be tacking on whatever filters he/she wants.

Next, we get call GetCollection(). This just creates a connection to our MongoDB server and returns the “Restaurants” collection.

After that, we loop through all of our filters and chain them together using a QueryDocument.

When we’re done, we call Find on our collection and pass in our whole query. Done!

I’m sure there are lots of other ways to accomplish this, but I chose to implement it this way because the syntax for calling the search felt very natural to me as a C# developer.