Ayende @ Rahien

Refunds available at head office

Making the complex trivial: Rich Domain Querying

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.
 

Comments

Brian Chavez
10/15/2008 10:48 PM by
Brian Chavez

I did something similar to this in a recent project using JSON to serialize search criterias.

Except, I didn't put the criteria building in the property setters. They're like SearchDTO objects. The DAOs themselves take care of translating the filter objects into their respective DetachedCriterias to avoid SearchObjects and UI dependent on NHibernate.dll.

Ken Egozi
10/16/2008 08:18 AM by
Ken Egozi

Cool thing. Never used JSON serialisation for that, usually I'll send the query in Form/Querystring and use the [DataBind] power to get the filter.

My only problem with it, is that the filters (which are domain related) become way too aware of NH imo. I don't like my IReopsitory methods to accept NH based things as parameters

What I do is to have the Filter as a an anaemic NH-free class, located within the Domain.

in the domain implementation assembly (where the NH stuff goes) I'd declare a type inheriting from DetachedCriteria, that accepts the filter in the constructor.

public class ModuleSearchCriteria : DetachedCriteria

    {

        private readonly ModuleSpecification specification;

        /// 

<summary
/// Creates a new <see out of a

        /// 

<see
///

    public ModuleSearchCriteria(ModuleSpecification specification) 
        : base(typeof(Module), "module")

{

// manipulating the criteria by the filter

}

That way it's also easy to de-serialise a filter to a view (say for the sake of pre-populating a search screen), using the filter, keeping the view free from NH dependant objects

Demis
10/16/2008 10:02 AM by
Demis

hmmm. This just seems like a lot of effort for something Linq would excel at.

Ayende Rahien
10/16/2008 04:11 PM by
Ayende Rahien

Dennis,

You missed the point of complexity, it is not in the actual querying. It is the query building part that is hard.

pete w
10/16/2008 06:24 PM by
pete w

I've done this kind of thing before, both in and out of NHibernate. I have found to to be quite useful.

What I would love to see is a technology that provides a sql server reporting services interface, but works with business objects and not sql, to me this would be a killer app.

All too often, we create applications with rich business objects, only to write sql-based reports, to me, it makes more sens to re-use these object for reporting

MD
10/17/2008 06:26 PM by
MD

It is cool indeed, but is it safe to build the search criteria on the client?

Ayende Rahien
10/17/2008 08:07 PM by
Ayende Rahien

MD,

What do you mean safe?

From SQL injection perspective, NH ensure that this is not a problem.

From optimization perspective, your filter model ensure that you don't allow truly bad things happening.

Grimace of Despair
10/17/2008 11:34 PM by
Grimace of Despair

I smell a NHQG update + revival :)

(I know, I know, ... I'm free to send a patch :P )

JP
10/19/2008 11:07 AM by
JP

Sorry my ignorance but how do you pass the list of actions to a single detachedcriteria? In other words, if you have a repository which accepts a single detachedcriteria, how do you merge them all to a single one?

Ayende Rahien
10/19/2008 11:22 AM by
Ayende Rahien

JP,

You are using CreateCriteria to do this

JP
10/19/2008 11:27 AM by
JP

Can you please show an example using the code above?

Ayende Rahien
10/19/2008 11:52 AM by
Ayende Rahien

Please ask in nh users group

JP
10/19/2008 02:16 PM by
JP

I'm just not getting what you intend to do in the FindAll method. You receive an AbstractSearchFilter as a parameter but I cannot understand what you do in the method's body :)

Can you please reply how you think this method should behave?

Thanks

Ayende Rahien
10/19/2008 02:45 PM by
Ayende Rahien

It is something like:

public T[] FindAll(AbstractSearchFilter filter)

{

        var dc = DetachedCriteria.For

<t();

        filter.Apply(dc);

       return        dc.GetExecutableSession(session).List

<t();

}

JP
10/21/2008 10:32 AM by
JP

Thanks!

You're the man.

Neil
10/23/2008 10:39 AM by
Neil

Hi Oren,

I'm sure I saw some code like this in a pub recently :)

Thanks for putting it up on the blog.

Chris Tavares
10/27/2008 05:16 AM by
Chris Tavares

Just a minor nit to pick - your JSON is invalid. You're required to quote all strings, including the ones on the left of the ':' characters.

Chris Tavares
10/30/2008 09:01 PM by
Chris Tavares

Then the JSON serializer is busted.

Comments have been closed on this topic.