Currently Entity Framework does not natively support second level caching. For pre-EF6 versions you could use EF Caching Provider Wrapper but due to changes to the EF provider model in EF6 it does not work with newest versions of EF. In theory it would be possible to recompile the old caching provider against EF6 but, unfortunately, this would not be sufficient. A number of new features breaking some assumptions made in the old caching provider (e.g. support for passing an open connection when instantiating a new context) have been introduced in EF6 resulting in the old caching provider not working correctly in these scenarios. Also dependency injection and Code-based Configuration introduced in EF6 simplify registering wrapping providers which was rather cumbersome (especially for Code First) in the previous versions. To fill the gap I created the Second Level Cache for EF 6.1 project. It’s a combination of a wrapping provider and a transaction handler which work together on caching query results and keeping the cache in a consistent state. In a typical scenario query results are cached by the wrapping caching provider and are invalidated by the transaction handler if an entity set on which cached query results depend was modified (note that data modification in EF always happens in a transaction hence the need of using transaction handler).
Using the cache is easy but requires a couple steps. First you need to install the EntityFramework.Cache NuGet package. There are two ways to do this from Visual Studio. One way to do this is to use the UI – right click the References node in the Solution Explorer and select the Manage NuGet Packages option. This will open a dialog you use to install NuGet packages. Since the project is currently in the alpha stage you need to select “Include Prelease” in the drop down at the top. Then enter “EntityFramework.Cache” in the search window and, once the package appears, click the “Install” button.
You can also install the package from the Package Manager Console. Open the Package Manager Console (Tools → NuGet Package Manager → Package Manager Console) and execute the following command:
Install-Package EntityFramework.Cache –Pre
(
-Pre
allows installing pre-release packages).Note that the package depends on Entity Framework 6.1. If you don’t have Entity Framework 6.1 in your project it will be automatically installed when you install the EntityFramework.Cache package.
Once the package is installed you need to tell EF to use caching by configuring the caching provider and the transaction handler. You do this by creating a configuration class derived from the
DbConfiguration
class. In the constructor of your DbConfiguration
derived class you need to set the transaction interceptor and the Loaded
event handler which will be responsible for replacing the provider. Here is an example of how to setup caching which uses the built-in in-memory cache implementation and the default caching policy.
public class Configuration : DbConfiguration { public Configuration() { var transactionHandler = new CacheTransactionHandler(new InMmemoryCache()); AddInterceptor(transactionHandler); Loaded += (sender, args) => args.ReplaceService<DbProviderServices>( (s, _) => new CachingProviderServices(s, transactionHandler, new DefaultCachingPolicy())); } }
The default caching policy used above is part of the project and allows caching all query results regardless of their size or entity sets used to obtain the results. Both, sliding and absolute, expiration times in the default caching policy are set to maximum values therefore items will be cached until an entity set used to obtain the results depended on is modified. If the default caching policy is not suitable for your needs you can create a custom caching policy in which you can limit what will be cached. To create a custom policy you just need to derive a class from the CachingPolicy
class, implement the abstract methods, and pass the policy to the CachingProviderServices
during registration. When implementing a custom caching policy there is one thing to be aware of – the expired entries are removed from the cache lazily. It means that an entry will be removed by the caching provider only when the caching provider tries to read the entry and finds it is expired. This is not extremely helpful (especially because since the item is expired the provider will query the database to get fresh results which will be then put to the cache – so effectively the expired entry will be replaced with a new entry) but lets the user decide what the best strategy of cleaning the cache in their case is. For instance, in the InMemoryCache implementation included in the project I created a Purge
method. This method could be periodically called to remove stale cache entries.
The project also includes a simple implementation of a cache which caches query results in memory. This is just a sample implementation and if you would like to use a different caching mechanism you are free to do so – you just need to implement the ICache interface. This interface is a slightly modified version of the interface that shipped with the original EF Caching Provider Wrapper which should make moving existing apps using the old caching solution to EF6 easier.
As the old saying goes “A program is worth a 1000 words“, so let’s take a look at the second level cache in action. Here is a complete sample app which is using the cache:
public class Airline { [Key] public string Code { get; set; } public string Name { get; set; } public virtual ICollection<Aircraft> Aircraft { get; set; } } public class Aircraft { public int Id { get; set; } public string EquipmentCode { get; set; } public virtual Airline Airline { get; set; } } public class AirTravelContext : DbContext { static AirTravelContext() { Database.SetInitializer(new DropCreateDatabaseIfModelChanges<AirTravelContext>()); } public DbSet<Airline> Airlines { get; set; } public DbSet<Aircraft> Aircraft { get; set; } } public class Configuration : DbConfiguration { public Configuration() { var transactionHandler = new CacheTransactionHandler(Program.Cache); AddInterceptor(transactionHandler); Loaded += (sender, args) => args.ReplaceService<DbProviderServices>( (s, _) => new CachingProviderServices(s, transactionHandler)); } } class Program { internal static readonly InMemoryCache Cache = new InMemoryCache(); private static void Seed() { using (var ctx = new AirTravelContext()) { ctx.Airlines.Add( new Airline { Code = "UA", Name = "United Airlines", Aircraft = new List<Aircraft> { new Aircraft {EquipmentCode = "788"}, new Aircraft {EquipmentCode = "763"} } }); ctx.Airlines.Add( new Airline { Code = "FR", Name = "Ryan Air", Aircraft = new List<Aircraft> { new Aircraft {EquipmentCode = "738"}, } }); ctx.SaveChanges(); } } private static void RemoveData() { using (var ctx = new AirTravelContext()) { ctx.Database.ExecuteSqlCommand("DELETE FROM Aircraft"); ctx.Database.ExecuteSqlCommand("DELETE FROM Airlines"); } } private static void PrintAirlinesAndAircraft() { using (var ctx = new AirTravelContext()) { foreach (var airline in ctx.Airlines.Include(a => a.Aircraft)) { Console.WriteLine("{0}: {1}", airline.Code, string.Join(", ", airline.Aircraft.Select(a => a.EquipmentCode))); } } } private static void PrintAirlineCount() { using (var ctx = new AirTravelContext()) { Console.WriteLine("Airline Count: {0}", ctx.Airlines.Count()); } } static void Main(string[] args) { // populate and print data Console.WriteLine("Entries in cache: {0}", Cache.Count); RemoveData(); Seed(); PrintAirlinesAndAircraft(); Console.WriteLine("\nEntries in cache: {0}", Cache.Count); // remove data bypassing cache RemoveData(); // not cached - goes to the database and counts airlines PrintAirlineCount(); // prints results from cache PrintAirlinesAndAircraft(); Console.WriteLine("\nEntries in cache: {0}", Cache.Count); // repopulate data - invalidates cache Seed(); Console.WriteLine("\nEntries in cache: {0}", Cache.Count); // print data PrintAirlineCount(); PrintAirlinesAndAircraft(); Console.WriteLine("\nEntries in cache: {0}", Cache.Count); } }
The app is pretty simple – a couple of entities, a context class, code base configuration (should look familiar), a few methods and the Main()
method which drives the execution. One method that is worth mentioning is the RemoveData()
method. It removes the data from the tables using the SqlExecuteMethod()
therefore bypassing the entire Entity Framework update pipeline including the caching provider. This is to show that the cache really works but is at the same type a kind of warning – if you bypass EF you will need to make sure the cache is in a consistent state or you can get incorrect results. Running the sample app results in the following output:
Entries in cache: 0 FR: 738 UA: 788, 763 Entries in cache: 3 Removing data directly from the database Airline Count: 0 FR: 738 UA: 788, 763 Entries in cache: 4 Entries in cache: 2 Airline Count: 2 FR: 738 UA: 788, 763 Entries in cache: 4 Press any key to continue . . .
Let’s analyze what’s happening here. On line 1 we just report that the cache is empty (no entries in the cache). Seems correct – no queries have been sent to the database so far. Then we remove any stale data from the database, seed the database, and print the contents of the database (lines 2 and 3). Printing the content of the database requires querying the database which should result in some entries in the cache. Indeed on line 4 three entries are reported to be in the cache. Why three if we sent just one query? If you peek at the cache with the debugger you will see that two of these entries are for the HistoryContext so three seems correct. Now (line 6) we delete all the data in the database so when we query the database for the airline count (line 7) the result is 0. However, even though we don’t have any data in the database, we can still print data on lines 8 and 9. This data comes from the cache – note that when we deleted data we used ExecuteSqlCommand()
method which bypassed the EF update pipeline and, as a result, no cache entries were invalidated. On line 11 we again report the number of items in the cache. This time the cache contains four entries – two entries for the HistoryContext related queries, one entry for the data that was initially cached and one entry for the result of the query where we asked for the number of airlines in the database. Now we add the data to the database again and again print the number of items in the cache on line 13. This time the number of entries is two. This is because inserting the data to the database invalidated cached results for queries that used either Airlines or Aircraft entity sets and as a result only the results related to the HistoryContext remained in the cache. We again query the database for the number of airlines (line 14) and print all the data from the database. The results for both queries are added to the cache and we end up having four entries in the cache again (line 18).
That’s pretty much it. Try it out and let me what you think (or report bugs). You can also look at and play with the code – it is available on the project website on codeplex.
Note that this version is an alpha version and there is still some work remainig before it can be called done – most notably support for async methods, adding some missing overrides for the ADO.NET wrapping classes. I am also considering adding support for TransactionScope
.