Reusing predicates in EntityFramework

Warning: some knowledge of Expressions is required.

Entity Framework simplifies a great number of things; for starters, I don't have to write a lot of SQL which makes development a lot faster - I'm a lot more productive in C# than SQL. One thing that works really well in EF, is the ability to reuse predicates - how often do you find yourself typing:
posts.Where(p => !p.IsDeleted);
In progamming we often get taught to DRY out the code - i.e. Don't Repeat Yourself. I think that extends to the expressions we're pass in to EF. The expressions are the specification language of EF (ignore that part if you don't do DDD). I'll look at a couple of ways of storing that expression - first up: extension methods.

Set up

Let's a assume we have a blog post entity like:
public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
    public string AuthorName { get; set; }
    public bool IsPublished { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime DateCreated { get; set; }
}
and a context:
public class BlogContext : DbContext
{
    public BlogContext() : base("Blogs") {}

    public DbSet<BlogPost> BlogPosts { get; set; }
}
Now we're ready to start playing with those extension methods.

Extension methods

The simplest, and the approach that I first used, is to create an extension method:

public static class BlogPostQueryExtensions
{
    public IQueryable<BlogPost> WhichAreNotDeleted(this IQueryable<BlogPost> posts)
    {
        return posts.Where(p => !p.IsDeleted);
    }
}
That makes it super easy to dry out the code:
context.BlogPosts.WhichAreNotDeleted();
As long as you have the correct using statements you'll even get great intellisense support. It's also easy to extend by chaining calls:
context.BlogPosts.WhichAreNotDeleted()
     .WhichContainText("Foo")
     .WhichAreByAuthor("Bar");
That' all non deleted posts by "Bar" containing the text "Foo". It's a compelling pattern and has worked really well. I still use this for my projections e.g. ProjectToBlogPostWithComments();.

Expression fields and factories

Using expressions looks like this:
Expression<Func<BlogPost, bool>> isDeleted = p => !p.IsDeleted;
Expression<Func<BlogPost, bool>> isByBar = p => p.AuthorName == "Bar";
Expression<Func<BlogPost, bool>> containsFoo = p => p.Body.Contains("Foo");

context.BlogPosts.Where(isDeleted)
    .Where(isByBar)
    .Where(containsFoo);
Now, this doesn't look very different at all - and actually, at this point there is absolutely no compelling reason to favour the expressions over the extension methods. The real power comes when you start combining the filters into a single expression and only calling Where once.
context.BlogPosts.Where(
    isDeleted
    .And(isByBar)
    .And(containsFoo));
You'll have to ignore the magic "And" method for now - that's what I'm going to talk about in the next blog post.
Combining the expressions into a single expression produces less nested SQL and results in zero duplication of predicates. The extension methods (or multiple wheres) result in the earlier predicates being repeated multiple times. As the expression approach doesn't it has cut the cost of some queries I have tested by more than 33%.

In the next post I'll talk about the And() method and it's sibling the Or() method.

Comments

Popular posts from this blog

Trimming strings in action parameters in ASP.Net Web API

Full text search in Entity Framework 6 using command interception

Composing Expressions in C#