EF Caching with Jarek Kowalski's Provider


The information in this post is out of date.

Visit msdn.com/data/ef for the latest information on current and past releases of EF.


Jarek Kowalski built a great caching and tracing toolkit that makes it easy to add these two features onto the Entity Framework. His code sample plugs in as a provider that wraps the original store provider you intend to use. This guide will help you get started using Jarek’s tools to enable caching and/or tracing in your application.

Caching can be especially valuable in an application that repeatedly retrieves the same data, such as in a web application that reads the list of products from a database on each page load. Tracing allows you to see what SQL queries are being generated by the Entity Framework and determine when these queries are being executed by writing them to the console or a log file. When these two features are used together, you can use the tracing output to determine how caching has changed your application’s pattern of database access.

Although the Entity Framework currently does not ship with caching or tracing “in the box,” you can add support for these features by following this walkthrough. These instructions cover only the essentials of enabling caching and tracing using Jarek’s provider samples. More advanced details about how to configure these samples are available in the resources Jarek provides online.

Below is a diagram that represents the change we will be making to the query execution pathway in order to plug in the caching and tracing features. The extensibility point we will be using is at the store provider level of the entity framework.

  In addition, below is a diagram that should elucidate why it is referred to as a “wrapping” provider and what it looks like when there is a cache hit versus a cache miss during query execution. As you can see below, the providers conceptually wrap around your existing provider.

 

Gathering Resources and Setting Up

Let’s get started.  Here are the new resources you will need to follow along with this walkthrough:

· Download “Tracing and Caching Provider Wrappers for Entity Framework” from MSDN to a folder where you keep your visual studio projects

· Download the extended context code generator, “ExtendedContext.vsix,” from the link at the bottom of this post

First, install the visual studio extension my colleague Matt Luebbert created that will automatically generate certain boilerplate code for you.  A vsix file like this is an installable visual studio extension that adds functionality to your IDE.  This particular vsix adds a T4 template that generates a class that extends your existing entity container so that it can work with the new caching and tracing features.  Note, however, that it also has dependencies on adding both of Jarek’s caching and tracing resources to your project.  This does not necessarily mean that to use caching you must use tracing, or vice versa, but you do have to provide your project with references to both resources in order to use this extra tool we have provided for convenience.  If you do not want to use this tool, you can also generate the necessary code by hand according to Jarek’s instructions here.

 

To install the extension, double-click the “ExtendedContext.vsix” file you downloaded to your desktop.  Then click “Install.”  

 

 

Click "Close" after the installation completes successfully.

Now open your project in Visual Studio. If you would like a sample project to get started, you can use the same one I am using, which I created in a previous walkthrough the Absolute Beginner’s Guide to Entity Framework. For this particular sample, I am using the same models as in that walkthrough but a simpler version of the executable code which also demonstrates the benefits of caching more obviously. Here is the code in the file Program.cs that I am starting with this time.

 

using System;

using System.Linq;

namespace ConsoleApplication2

{

    class Program

    {

        static void Main(string[] args)

        {

            using (Model1Container context = new Model1Container())

            {

                //Add an entity

                context.Users.AddObject(new User { Username = "User1", Password = "password" });

                context.SaveChanges();

                User findUser = context.Users.FirstOrDefault(user => user.Username == "User1");

                Console.WriteLine("Found user: " + findUser.Username);

                Console.WriteLine("User is attending " + findUser.ConfirmedEvents.Count() +

                    " events, including: ");

            }

            using (Model1Container newContext = new Model1Container())

            {

                User findUser = newContext.Users.FirstOrDefault(user => user.Username == "User1");

                Console.WriteLine("Found user: " + findUser.Username);

                Console.WriteLine("User is attending " + findUser.ConfirmedEvents.Count() +

                    " events, including: ");

            }

            Console.Read(); //Pause execution

        }

    }

}

 

 

 Now we need to add the proper references to Jarek’s code. To do this, from the File menu of Visual Studio click “Add” >> “Existing Project…”

 

First, add the project named EFProviderWrapperToolkit.csproj which is in the EFProviderWrapperToolkit folder of Jarek’s download. Now, add the reference to that project from the project in which we are enabling caching. To do this, right-click on “References” for your project in Solution Explorer, and then select “Add Reference…” Under the “Projects” tab select the EFProviderWrapperToolkit and click “OK” to add a reference for this code into your project.

 

The EFProviderWrapperToolkit is a common dependency for both caching and tracing, so regardless of whether you wish to use one or both of those features, this is a necessary inclusion.

Enabling Tracing

To add tracing functionality, follow the same steps as above to add the EFTracingProvider.csproj file which is located in the EFTracingProvider folder within the downloaded code.

 

 

After you have added the project, right-click on “References” in the Solution Explorer and click “Add Reference…” to add the necessary reference for the EFTracingProvider components.

If you would like to follow along with this walkthrough and use the Extended Context generator, which depends on also adding the EFCachingProvider code, repeat that same procedure once more to add the “EFCachingProvider” project, and then add a reference to that project as above.  This is required to satisfy a dependency of the class the Extended Context generator writes for you, since that class is designed to work with both caching and tracing and therefore depends on both.

Now we will make use of that convenient extended context generator that came in the VSIX file.  Right-click on the design surface of your project’s .edmx file and click “Add Code Generation Item.”  You should see “EF Caching and Tracing Content Code Generator” as an option.  Select that generator and click “Add.”  You will also need to come back to this screen to add the “ADO.NET Entity Object Generator,” but you can only add one code generator at a time.

 

 

 

 

You will likely see a security warning as below and perhaps a few other times while using this add-on code.  Click “OK” to proceed whenever this warning appears.

 

 

 

This will add the ExtendedContext file to your project, like this:

 

Here is an example of what this generator automatically writes for you. This is what is contained in the ExtendedContext1.cs file listed above.

  

//------------------------------------------------------------------------------

// <auto-generated>

// This code was generated from a template.

//

// Changes to this file may cause incorrect behavior and will be lost if

// the code is regenerated.

// </auto-generated>

//------------------------------------------------------------------------------

using System;

using System.IO;

using EFCachingProvider;

using EFCachingProvider.Caching;

using EFProviderWrapperToolkit;

using EFTracingProvider;

namespace ConsoleApplication2

{

    public partial class ExtendedModel1Container : Model1Container

    {

       private TextWriter logOutput;

   

        public ExtendedModel1Container()

            : this("name=Model1Container")

        {

        }

   

        public ExtendedModel1Container(string connectionString)

            : base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(

                    connectionString,

                    "EFTracingProvider",

                    "EFCachingProvider"

            ))

        {

        }

   

        #region Tracing Extensions

   

        private EFTracingConnection TracingConnection

        {

            get { return this.UnwrapConnection<EFTracingConnection>(); }

        }

   

        public event EventHandler<CommandExecutionEventArgs> CommandExecuting

        {

            add { this.TracingConnection.CommandExecuting += value; }

            remove { this.TracingConnection.CommandExecuting -= value; }

        }

   

        public event EventHandler<CommandExecutionEventArgs> CommandFinished

        {

            add { this.TracingConnection.CommandFinished += value; }

            remove { this.TracingConnection.CommandFinished -= value; }

        }

   

        public event EventHandler<CommandExecutionEventArgs> CommandFailed

        {

            add { this.TracingConnection.CommandFailed += value; }

            remove { this.TracingConnection.CommandFailed -= value; }

        }

   

        private void AppendToLog(object sender, CommandExecutionEventArgs e)

        {

            if (this.logOutput != null)

            {

                this.logOutput.WriteLine(e.ToTraceString().TrimEnd());

                this.logOutput.WriteLine();

            }

        }

   

        public TextWriter Log

        {

            get { return this.logOutput; }

            set

            {

                if ((this.logOutput != null) != (value != null))

                {

                    if (value == null)

                    {

                        CommandExecuting -= AppendToLog;

                    }

                    else

                    {

                        CommandExecuting += AppendToLog;

                    }

                }

   

                this.logOutput = value;

            }

        }

   

    

        #endregion

   

        #region Caching Extensions

   

        private EFCachingConnection CachingConnection

        {

            get { return this.UnwrapConnection<EFCachingConnection>(); }

        }

   

        public ICache Cache

        {

            get { return CachingConnection.Cache; }

            set { CachingConnection.Cache = value; }

        }

   

        public CachingPolicy CachingPolicy

        {

            get { return CachingConnection.CachingPolicy; }

            set { CachingConnection.CachingPolicy = value; }

        }

   

        #endregion

   

    }

}

 

 

Repeat this same procedure for adding a code generation item by right-clicking the design surface and  adding the” ADO.NET Entity Object Generator,” a generator that comes as part of the Entity Framework.

Now add the necessary “using” directives at the top of the file(s) in which you’ll be using tracing and/or caching.  In other words, add them wherever you construct contexts and manipulate your entities.  If you are following along, add this to the top of the “Program.cs” file.  As with adding the projects and references, the EFProviderWrapperToolkit is required for either tracing or caching.  However, here you can add only the references for the features you intend to use.  For example, if you only want to use tracing, you could reference only EFProviderWrapperToolkit and EFTracingProvider to just get the resources for tracing.  If you only wanted to use caching, reference EFProviderWrapperToolkit and EFCachingProvider.   Since I plan to use both caching and tracing in this walkthrough, I will include all three references right now.

using EFProviderWrapperToolkit;

using EFTracingProvider;

using EFCachingProvider;

 

 

You also need to register the provider for whichever of the features you wish to use, which is done separately for each the EFTracingProvider and the EFCachingProvider.  This is the crucial step of plugging in to the extensibility point at the store provider level.  This can be accomplished very conveniently through the RegisterProvider method as demonstrated below.  For this walkthrough I will register both, but you could also register just the feature you plan to use.  This registration needs to be called only once during execution, so place this configuration code somewhere in your application that will be called only once and at the beginning of execution before you would like to use either caching or tracing. Note the difference between lines below – their identical length and subtle difference can be misleading. 

 

EFTracingProviderConfiguration.RegisterProvider(); //Add this line if you plan to use tracing

EFCachingProviderConfiguration.RegisterProvider(); //...This one is for caching

 

In the same section of your code where you placed the above provider registration, you can specify the configuration settings.  A more complete list of settings can be found online, but for now I will just turn on tracing to see that it is working.  Add this line right below the RegisterProvider lines above, potentially within an “if defined” block so you can toggle it on and off easily.  If you do use an “if defined” block, don’t forget to add “#define LOGGING” at the top of the file or at compilation time.

 

 

#if LOGGING

            EFTracingProviderConfiguration.LogToConsole = true; //Optional: for sending tracing to console

#endif

 

 

Finally, for any context in your application in which you would like to use caching or tracing, prepend “Extended” to the name of the context you construct. For example, my entity container was called “Model1Container” before installing these features, and the extended context code generator named the class it created as “ExtendedModel1Container.” You can automate this replacement by using find and replace. Select from the top menu bar: “Edit” >> “Find and Replace” >> “Quick Replace.”

 

Here is what my code looks like now (things I changed since the beginning of this walkthrough are highlighted in gray):

#define LOGGING

using System;

using System.Linq;

using EFProviderWrapperToolkit; //Required for caching and/or tracing

using EFTracingProvider; //Required for tracing

using EFCachingProvider; //Required for caching

namespace ConsoleApplication2

{

    class Program

    {

        static void Main(string[] args)

        {

            EFCachingProviderConfiguration.RegisterProvider();

            EFTracingProviderConfiguration.RegisterProvider();

#if LOGGING

            EFTracingProviderConfiguration.LogToConsole = true; //Optional: for sending tracing to console

#endif

            using (ExtendedModel1Container context = new ExtendedModel1Container())

            {

                //Add an entity

                context.Users.AddObject(new User { Username = "User1", Password = "password" });

             context.SaveChanges();

                User findUser = context.Users.FirstOrDefault(user => user.Username == "User1");

                Console.WriteLine("Found user: " + findUser.Username);

                Console.WriteLine("User is attending " + findUser.ConfirmedEvents.Count() +

                    " events, including: ");

            }

            using (ExtendedModel1Container newContext = new ExtendedModel1Container())

            {

                User findUser = newContext.Users.FirstOrDefault(user => user.Username == "User1");

                Console.WriteLine("Found user: " + findUser.Username);

                Console.WriteLine("User is attending " + findUser.ConfirmedEvents.Count() +

                    " events, including: ");

       }

            Console.Read(); //Pause execution

        }

    }

}

 

 

 

Now you are ready to test if tracing is working. Build and run your application. Whenever the database is queried, you should see output to the console like below. Each logging output consists of the following:

First line (white): The query counter, starting at 1 from the beginning of execution.
Next lines (gray): The actual query executed against the database
Last line (green): An indication that the query was complete and the amount of time it took.

 

 

Enabling Caching

Now that we have tracing working, let’s enable some the caching functionality. Begin by adding a new directive at the top of the file in which we added the other “using” statements.

using EFCachingProvider.Caching;

Then, define the cache (where the items will be stored) and the caching policy (which items will be stored). It makes the most sense to define these beyond the scope of any one context so that multiple contexts can use the same cache, perhaps directly below the registration and configuration lines added earlier. That ability to share the same cache among multiple contexts in your application is one of the primary benefits of a second-level cache.

For example, you could use a basic in-memory cache and a policy that caches all of your entities.

ICache IMCache = new InMemoryCache();

CachingPolicy cachingPolicy = CachingPolicy.CacheAll;

 

In the above code, ICache is a public interface for a cache store, created by Jarek for his toolkit, and InMemoryCache is a class he provides that implements that interface.

Lastly, you have to inform each context explicitly which cache it will use and how it will cache entities. You do this by telling the context about your cache as follows, referencing the cache and policy just created:

context.Cache = IMCache;

context.CachingPolicy = cachingPolicy;

Now the context will use the cache you defined. Here’s how my code looks with the new additions.

#define LOGGING

using System;

using System.Linq;

using EFProviderWrapperToolkit; //Required for caching and/or tracing

using EFTracingProvider; //Required for tracing

using EFCachingProvider; //Required for caching

using EFCachingProvider.Caching; //Required for using the InMemoryCache

namespace ConsoleApplication2

{

    class Program

    {

        static void Main(string[] args)

        {

            EFCachingProviderConfiguration.RegisterProvider();

            EFTracingProviderConfiguration.RegisterProvider();

#if LOGGING

            EFTracingProviderConfiguration.LogToConsole = true; //Optional: for sending tracing to console

#endif

            ICache IMCache = new InMemoryCache();

            CachingPolicy cachingPolicy = CachingPolicy.CacheAll;

            using (ExtendedModel1Container context = new ExtendedModel1Container())

            {

                context.Cache = IMCache; //Required at the beginning of the context

                context.CachingPolicy = cachingPolicy; //Also required at the beginning of the context

                //Add an entity

                context.Users.AddObject(new User { Username = "User1", Password = "password" });

                context.SaveChanges();

                User findUser = context.Users.FirstOrDefault(user => user.Username == "User1");

                Console.WriteLine("Found user: " + findUser.Username);

                Console.WriteLine("User is attending " + findUser.ConfirmedEvents.Count() +

                    " events, including: ");

            }

            using (ExtendedModel1Container newContext = new ExtendedModel1Container())

            {

                newContext.Cache = IMCache; //Required at beginning of every context - references ICache from above

                newContext.CachingPolicy = cachingPolicy; //Also required for every cache - references CachingPolicy from above

                User findUser = newContext.Users.FirstOrDefault(user => user.Username == "User1");

                Console.WriteLine("Found user: " + findUser.Username);

                Console.WriteLine("User is attending " + findUser.ConfirmedEvents.Count() +

                    " events, including: ");

          }

            Console.Read(); //Pause execution

        }

    }

}

 

Build and run your program again to see what changes. You should be able to see some difference in the number of database calls, likely fewer than before when only tracing was enabled. You might not see any changes in the number of calls if you primarily write to the database or read items only once during execution, since the cache is only useful during repetitive reads. However, in applications with redundant reads from the database, such as this sample I have been constructing, caching can provide some great performance and load benefits. The table below shows a side-by-side comparison that visually demonstrates the result of caching. Notice how when using caching each query is sent to the database only once.

 

 

 

Voila! That’s basic caching at work.

If you would like to learn more about this toolkit, you can visit Jarek’s blog and read through the blog post where he describes caching in more advanced detail. And if you have any feedback regarding the provider, you can reply in the comments below or reach out to Jarek via his blog contact form.

 

ExtendedContext.vsix.txt