Saturday, September 19, 2009

A Custom linq query builder

In my opinion one the most interesting features of Linq is Expression Trees.They let us build structured queries over IQueryable type.
IQueryable ,as its name implies, is an interface that let us build a query over a provided object of type T.Although IQueryable and expression trees are mostly used in Linq to sql but since every IEnumerable can be converted to an IQueryable object (using System.Linq.AsQueryable method) these great tools are available to every kind of Linq (linq to Xml and linq to objects for example).
When we build an expression tree over an IQueryable object , the expression itself won't be executed until we really want to get the data.
For example if we write the following code:
(DataSource is an IQueryable object)

var result=DataSource.Where(post=>post.Title.IndexOf("Linq")>0);




result is yet another IQueryable that you can query again :

result=result.Where(post=>post.CreationDate>new DateTime(2009,1,1));




Now if we've added all we wanted to our query we can execute it either by converting the result to an ICollection (a List or an array for example) or by executing GetEnumrator method ( usually by using a for each loop).

Based on this introduction I designed a custom query builder that facilitate querying an object using collected search arguments (in a windows /web form for example)

Here's the scenario that I had in mind :
There's a web/win form that lets a user fill-in some information about what he wants to see.
Consider a blog application , a user can search post based on their titles and/or their creation date.Therefore a QueryPostArgs object is created and populated using the information provided by user.
Then this object is passed to a QueryBuilder to build a query from it.Query builder contains some register query methods and when is asked to build the query it will use them to build the final query.
Enough talking let write some code :

To collect and transfer the arguments information we have an AbstractQueryArgs class.

public abstract class AbstractQueryArgs<t> where T :class
{
}




To build queries over a post object we then derived from it and create a PostQueryArgs class.

public class PostQueryArgs : AbstractQueryArgs<post>
{
public PostQueryArgs()
{
CreationDateFrom = DateTime.MinValue;
CreationDateTo = DateTime.MaxValue;
}
public string Title { get; set; }
public DateTime CreationDateFrom { get; set; }
public DateTime CreationDateTo { get; set; }
public string[] Tags { get; set; }
}



To build query using an AbstractQueryArgs we have an AbstractQueryBuilder like this:

public abstract class AbstractQueryBuilder<t,tsearchargs> 
where
T :class where TSearchArgs :AbstractQueryArgs<t>
{
protected delegate IQueryable<t>
QueryMethodDelegate
(IQueryable<t> queryable,TSearchArgs searchArgs);

public IQueryable<t> DataSource { get; private set; }
private readonly List<QueryMethodDelegate>
_queryMethodDelegates = new List<QueryMethodDelegate>();


protected AbstractQueryBuilder(IQueryable<t> dataSource)
{
DataSource = dataSource;
}


protected void AddQueryMethod(QueryMethodDelegate queryMethodDelegate)
{
_queryMethodDelegates.Add(queryMethodDelegate);
}
public IQueryable<t> Build(TSearchArgs searchArgs)
{
var queryable = DataSource
foreach (var queryMethodDelegate in _queryMethodDelegates)
{
queryable=queryMethodDelegate(queryable,searchArgs);
}

return queryable;
}
}



It allow us to build custom query builders for any T class.As you can see it gets an
IQueryable as its data source via constructor.Then we can add some query methods to a list of query methods . A query method is any method that gets an IQueryable and a query arguments object and returns an IQueryable (It somehow a visitor method that visits an IQueryable object and do something on it based on provided query arguments and return it back ) To define a query method a protected delegate is defined.Every class that is derived from this abstract class can add its custom query methods to the query methods list using AddQueryMethod protected method.
Finally there's this Build method that build an IQueryable object using the provided datasource and passing it to each query method.As you can see this method is also returning an IQueryable object that lets us to perform any further queries over it if necessary.

Now we can create our custom query builder for our post object as follows:

public class PostQueryBuilder:AbstractQueryBuilder<post,postqueryargs>
{
public PostQueryBuilder(IQueryable<post> dataSource) : base(dataSource)
{
AddQueryMethod(AddTitleExpression);
AddQueryMethod(AddCreationDateExpression);
AddQueryMethod(ContainsAllTags);
}

private static IQueryable<post>
ContainsAllTags(IQueryable<post> queryable , PostQueryArgs queryArgs)
{
if (queryArgs.Tags == null || queryArgs.Tags.Length == 0) return queryable;
return queryable.Where
(x => x.Tags.Intersect(queryArgs.Tags).Count() == queryArgs.Tags.Length);
}

private static IQueryable<post>
AddTitleExpression(IQueryable<post> queryable,PostQueryArgs queryArgs)
{
return string.IsNullOrEmpty(queryArgs.Title) ? queryable :
queryable.Where(x=>x.Title.IndexOf(queryArgs.Title)!=0);
}

private static IQueryable<post>
AddCreationDateExpression(IQueryable<post> queryable,
PostQueryArgs queryArgs)
{

return queryable.Where(
x => x.CreationDate >= queryArgs.CreationDateFrom &&
x.CreationDate <= queryArgs.CreationDateTo); }


As you can see this class add three query methods to query methods list.One for title,one for creation date and one for tags.

Now that we have created these classes , all we need to do to build a query on a post datasource is like this:

var posts=new []{_post1,_post2,_post3};
var queryBuilder=new PostQueryBuilder(posts);

var queryArgs=new PostQueryArgs{Title="Intro",Tags=new []{"C#","Linq"}};
var query=queryBuilder.Buid(queryArgs);




Then we have a IQeryable object that when we try to enumerate it , post objects with "Intro" in their title and containing "C#" and "Linq" tags will be returned.

P.S. I have upload the source code in codeproject site you can fint it here.

1 comment:

RRave said...

Hi

Its great if you can share in codegain.com also.

Thank you