CompiledQueryReplicator Tutorial

This tutorial aims to teach you how to use the CompiledQueryReplicator, why it exists and why it is useful. It assumes you already know C# and Entity Framework. It also assumes that you know about compiled queries (more info: MSDN, Blog).

The CompiledQueryReplicator is a class that allows you to define your LINQ query once, but have multiple compiled instances of it available at any one time.

Why do I need this?

The reason CompiledQueryReplicator was created was to make working with compiled queries in Entity Framework easier when you want to change an ObjectQuery's MergeOptions. Unfortunately, when a compiled LINQ to Entities query is first run (and it actually compiles), the MergeOption that you've set on the ObjectQuery in the ObjectContext you've passed in is "baked" into the compiled query. So if the first time you run your compiled query and your ObjectContext's ObjectQuery.MergeOption is set to MergeOption.NoTracking, the compiled query will forevermore return objects that are not attached to your ObjectContext, even if you call the query a second time with a different ObjectContext with a different ObjectQuery.MergeOption!

This issue is discussed in more detail in this blog. It's nasty because depending on what method calls your compiled query first, a different MergeOption can be baked in, leading to weird crashes and data strangely not being saved.

The only way (that I can see, anyway) to get around this issue of the MergeOption being baked into the compiled query is to have a different compiled query per MergeOption. So you'd have to have code like this:

private static class CompiledQueries
{
    public static readonly Func<AdventureWorksEntities, int, Product>
        GetProductWithAppendOnly = CompiledQuery.Compile(
            (AdventureWorksEntities context, int id) =>
                (from licence in context.Product
                 where licence.ProductID == id
                 select licence).FirstOrDefault());

    public static readonly Func<AdventureWorksEntities, int, Product>
        GetProductWithNoTracking = CompiledQuery.Compile(
            (AdventureWorksEntities context, int id) =>
                (from product in context.Product
                 where product.ProductID == id
                 select product).FirstOrDefault());
}

But that's a rubbish solution, because when you want to modify that query, you need to change it multiple places: a maintenance nightmare. This is where the CompiledQueryReplicator comes it. It lets you define that query once, and then it will transparently give you different compiled query instances.

How do I use it?

The CompiledQueryReplicator is basically a very simple Dictionary (although it does not implement IDictionary, since the semantic is slightly different). Instances of a compiled query are indexed by a key. This key type is generic so you can choose what it will be. You simply request a particular instance using a key object (like a Dictionary), and that compiled query instance will be returned to you. (If it didn't exist already it will be automatically created; this means you don't need to worry about adding entries to this "Dictionary". You just ask for what you want, and it just gives it to you, creating it if it doesn't exist.)

If you're using the Entity Framework, it is recommended that you use the EfCompiledQueryReplicatorFactory class to construct instances of CompiledQueryReplicator. By using its static methods you can get a simpler syntax since we can make use of compiler type inference (which you don't get when calling a constructor).

Here's how the previous (bad) example is simplified using the CompiledQueryReplicator:

private static class CompiledQueries
{
    public static readonly CompiledQueryReplicator<Func<AdventureWorksEntities, int, Product>, MergeOption>
        GetProduct = EfCompiledQueryReplicatorFactory<MergeOption>.Create(
            (AdventureWorksEntities context, int id) =>
                (from product in context.Product
                 where product.ProductID == id
                 select product).FirstOrDefault());
}

It looks pretty much the same as creating a normal compiled query, which is nice. We're specifying that the key type should be MergeOption, so we can have a different compiled query per MergeOption.

This is how we access and use the compiled queries:

public Product GetProductById(int id)
{
    using (AdvWorksEntities context = new AdvWorksEntities())
    {
        context.Product.MergeOption = MergeOption.NoTracking;
        return CompiledQueries.GetProduct[MergeOption.NoTracking](context, id);
    }
}

public void ChangeProductName(int id, string newName)
{
    using (AdvWorksEntities context = new AdvWorksEntities())
    {
        Product product = CompiledQueries.GetProduct[MergeOption.AppendOnly](context, id);
        product.Name = newName;
        context.SaveChanges();
    }
}

And that's it! CompiledQueryReplicator is thread-safe, so you can use it in your multithreaded apps (eg. ASP.NET Forms/MVC) without fear.

Last edited Mar 24, 2010 at 1:19 PM by dchambers, version 4

Comments

No comments yet.