Using Expression Trees and Lambda Expressions to perform CAML queries – Part 2

In my last post I explored some of the basics around Expression Tree parsing and CAML to translate a quite simple expression into CAML to then query SharePoint for all items of a certain content type, a book content type that I had previously provisioned.

Here is a list of the other posts in the series:

This time I promised to get a little more advanced, so let’s go at it.
First of all, I have decided to construct a Book entity and a BookRepository in interest of domain driven design. Lets look at the Book entity:

public class Book
{
    public const string ContentTypeId = "0x010101008103BAF1CBB541CB9F9A6DF623AEFEDD";
    public string ISBN { get; set; }
    public DateTime? ValidFrom { get; set; }
    public DateTime? ValidTo { get; set; }
    public string Author { get; set; }
    public string Publisher { get; set; }
    public string Title { get; set; }
    public string Id { get; private set; }
}

 

 

In SharePoint I have a list of books and I have created four books with some different data. Three of them have me (“Johan Leino”) as author and they all have different published dates.  

 

  

 

 

Query 1: Books by me

var BooksByMe = FindAll(book => book.Author == "Johan Leino");

 

 

My repository has a method (FindAll) that takes an expression of the same type as in my first post but it works with books. Calling that method to get all books with me as the author look like the code above.
So, of course we have to make a couple of changes to the parser to make it work for this type of query. We´ll start with VisitMemberAccess 

 

    protected virtual XElement VisitMemberAccess(MemberExpression member)
    {
        var expr = member.Expression; if (expr.NodeType == ExpressionType.Constant)
        {
            LambdaExpression lambda = Expression.Lambda(member);
            Delegate fn = lambda.Compile();
            return VisitConstant(Expression.Constant(fn.DynamicInvoke(null), member.Type));
        }
        else
        {
            return new XElement("FieldRef", ParseMemberName(member.Member.Name));
        }
    }
   
    protected virtual XAttribute ParseMemberName(string memberName)
    {
        return new XAttribute("Name", memberName);
    }

 

 

The only real difference here is that I have constructed a new method, ParseMemberName,  which does the same thing as in the code from the first post.
The thing is, I have decided to make a special (new subclass) parser that works with my Book entity.
I need to have a parser that knows how to translate between a C# book (the properties) and a SharePoint book (the fields on the content type).

class BookExpressionToCamlParser : ExpressionToCamlParser
{  
    protected override XAttribute ParseMemberName(string memberName) 
    {   
        switch (memberName)   
        {     
            case "Author":       
                return base.ParseMemberName("BookAuthor");     
            default:       
                return base.ParseMemberName(memberName);   
        } 
    }
}

The  BookExpressionToCamlParser class overrides the ParseMemberName method because it knows how to translate between “Author” and “BookAuthor

 

So in summary, the code in FindAll now would look something like this:

    public static IList<Book> FindAll(Expression<Func<Book, bool>> expression)
    {  
        List<Book> books = new List<Book>(); 
        ExpressionToCamlParser parser = new BookExpressionToCamlParser(); 
        string caml = parser.Translate(expression.Body); 
       
        SPSiteDataQuery query = new SPSiteDataQuery 
        {   
            Query = caml,  
            Lists = string.Format("<Lists MaxListLimit=\"0\" ServerTemplate=\"{0}\" />",                         
                    "10005"),   
            Webs = "<Webs Scope=\"SiteCollection\" />",   
            ViewFields = "<FieldRef Name='BookTitle' />" 
        };
        ...

 
Note: I´m using the new BookExpressionToCamlParser instead of ExpressionToCamlParser among other things

 

And the output:

 Three books found which is correct because I am the author of three of the four books.
Now let’s make it more difficult…

 

Query 2: Find My Active Books

The book called “My third book” is not to be published until the 1st of December, so the query I´m looking for is supposed to find the books that are “active” and have me as the author (which is supposed to be two)

var BooksByMeThatAreActive = FindAll(book => book.Author == "Johan Leino" && book.ValidFrom <= DateTime.Today);

The querying code is seen above and is still readable, I think. Imagine (or see below) how that code would look in CAML instead. 

 

You can see that the parser has now nested the Eq and Leq inside the And and also taken into account that I wanted <= DateTime.Today. The query has found two books which is correct.
Let´s walk through the changes needed to make the parser do that.
I have made some changes in the parser to make it support the DateTime (<Today />) for example which is kind of special in CAML.

To start with some changes to the VisitMemberAccess method:

    protected virtual XElement VisitMemberAccess(MemberExpression member)
    { 
        if (member.Expression != null && member.Expression.NodeType == ExpressionType.Constant) 
        {   
            LambdaExpression lambda = Expression.Lambda(member);   
            Delegate fn = lambda.Compile();   
            return VisitConstant(Expression.Constant(fn.DynamicInvoke(null), member.Type)); 
        } 
        else 
        {   
            if (member.Member.DeclaringType == typeof(DateTime) || member.Member.DeclaringType == typeof(Nullable<DateTime>))   
            {     
                switch (member.Member.Name)     
                {       
                    case "Now":       
                    case "Today":         
                        return new XElement("Value", new XAttribute("Type", "DateTime"), new XElement("Today")); // Now doesn´t work in CAML       
                    default:         
                        LambdaExpression lambda = Expression.Lambda(member);         
                        Delegate fn = lambda.Compile();         
                        return VisitConstant(Expression.Constant(fn.DynamicInvoke(null), member.Type));     
                }   
            }
       
            if (IsNullable(member.Type)) 
            {   
                return new XElement("FieldRef", new XAttribute("Nullable", "TRUE"), ParseMemberName(member.Member.Name)); 
            }    
           
            return new XElement("FieldRef", ParseMemberName(member.Member.Name)); 
        }
    }

The parser now understands that if the caller has included a comparison with DateTime.Now or DateTime.Today it is supposed to translate that to <Today /> which is supported in CAML.

 

There is a method for checking for nullable types that look like this: (probably could have been an extension method) 

    private bool IsNullable(Type type)
    { 
        return type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>));
    }

 

 

The parser also has to support Unary expressions, so this new method is necessary together with a new switch clause in the Visit method: 

    private XElement VisitUnary(UnaryExpression unary)
    {
        return Visit(unary.Operand);
    }
   
    // and in the Visit method
   
    case ExpressionType.Convert: 
        return VisitUnary(expression as UnaryExpression);

 

 

 

 The ParseMemberName method also knows how to translate between “ValidFrom” and “Published“, not going to show that method again though.
In the next post it will be even more difficult but I think I´m getting places already.

Stay tuned…

Advertisements

, , , , ,

  1. #1 by Polprav on October 23, 2009 - 12:56

    Hello from Russia!
    Can I quote a post in your blog with the link to you?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: