Making the complex trivial: Rich Domain Querying

time to read 4 min | 651 words

It is an extremely common issue and I talked about it in the past quite a few times. I have learned a lot since then, however, and I want to show you can create rich, complex, querying support with very little effort.

We will start with the following model:

image

And see how we can query it. We start by defining search filters, classes that look more or less like our domain. Here is a simple example:

public abstract class AbstractSearchFilter
{
	protected IList<Action<DetachedCriteria>> actions = new List<Action<DetachedCriteria>>();
	
	public void Apply(DetachedCriteria dc)
	{
		foreach(var action in actions)
		{
			action(dc);
		}
	}
}


public class PostSearchFilter : AbstractSearchFilter
{
	private string title;
	
	public string Title
	{
		get { return title; }
		set
		{
			title = value;
			actions.Add(dc => 
			{
				if(title.Empty())
					return;
				
				dc.Add(Restictions.Like("Title", title, MatchMode.Start));
			});
		}
	}
}

public class UserSearchFilter : AbstractSearchFilter
{
	private string username;
	private PostSearchFilter post;
	
	public string Username
	{
		get { return username; }
		set
		{
			username = value;
			actions.Add(dc =>
			{
				if(username.Empty())
					return;
			
				dc.Add(Restrictions.Like("Username", username, MatchMode.Start));
			});
		}
	}
	
	public PostSearchFilter Post
	{
		get { return post; }
		set
		{
			post = value;
			actions.Add(dc=>
			{
				if(post==null)
					return;
				
				var postDC = dc.Path("Posts"); // Path is an extension method for GetCriteriaByPath(name) ?? CreateCriteria(path)
				post.Apply(postDC);
			);
		}
	}
}

Now that we have the code in front of us, let us talk about it. The main idea here is that we move the responsibility of deciding what to query to the hands of the client. It can make decisions by just setting our properties. Not only that, but we support rich domain queries using this approach. Notice what we are doing in UserSearchFilter.Post.set, we create a sub criteria and pass it to the post search filter, to apply itself on that. Using this method, we completely abstract all the need to deal with our current position in the tree. We can query on posts directly, through users, through comments, etc. We don't care, we just run in the provided context and apply our conditions on it.

Let us take the example of wanting to search all the users who posts about NHibernate.  I can express this as:

usersRepository.FindAll(
  new UserSearchFilter
  {
    Post = new PostSearchFilter
        {
            Title = "NHibernate"
        }
  }
);

But that is only useful for static scenarios, and in those cases, it is easier to just write the query using the facilities NHibernate already gives us. Where does it shine?

There is a really good reason that I chose this design for the query mechanism. JSON.

I can ask the json serializer to serialize a JSON string into this object graph. Along the way, it will make all the property setting (and query building) that I need. On the client side, I just need to build the JSON string (an easy task, I think you would agree), and send it to the server. On the server side, I just need to build the filter classes (another very easy task). Done, I have a very rich, very complex, very complete solution.

Just to give you an idea, assuming that I had fully fleshed out the filters above, here is how I search for users name 'ayende', who posted about 'nhibernate' with the tug 'amazing' and has a comment saying 'help':

{ // root is user, in this case
	Name: 'ayende',
	Post:
	{
		Title: 'NHibernate',
		Tag:
		{
			Name: ['amazing']
		}
		Comment:
		{
			Comment: 'Help'
		}
	}
}
Deserializing that into our filter object graph gives us immediate results that we can pass the the repository to query with exactly zero hard work.