NHibernate Tutorial 4: Second-level Caching with Entity Framework & NHibernate

in #utopian-io8 years ago (edited)

What Will I Learn?

Using a Second-Level Cache with Entity Framework & NHibernate

Requirements

  • NHibernate
  • Entity Framework

Difficulty

Intermediate

Tutorial Contents

  • Caching in O/RM Tools
  • Second-level Caching with Entity Framework
  • Second-level Caching with NHibernate

Curriculum

Second-level Caching with Entity Framework & NHibernate

Nowadays no form of data access makes any longer sense without some robust form of caching. The idea behind caching is surprisingly simple and effective: make frequently read data accessible in a way that is faster than getting it from the physical location. The .NET infrastructure offers a few options for caching. In ASP.NET applications the first option you run across is the built-in Cache object. It is fairly rich, serves most of the typical needs, but it is limited to a single server scenario. In .NET, the caching API has been moved out of the ASP.NET space and made available to just any applications. In addition, you have the Caching Application Block in the Enterprise Library. It goes beyond the basic caching API in that it offers a provider-based mechanism to cache data in various stores: in-memory, XML files, nearer databases.

The real problem to address, however, is yet another. Where would you place caching in your layered solution? Two main patterns exist for caching in applications: Cache Aside and Cache Through.

The Cache Aside pattern entails the developer manages the contents of the cache manually. This means that the caching layer is different from the data access layer (DAL) and it is checked before placing a call to the DAL. Invoked from the application logic, the code in the caching layer checks the data in the cache before invoking the DAL. The DAL has no awareness of the caching layer. The caching layer is responsible for evicting and updating the cache whenever the data source is updated.

In the Cache Through pattern the cache is embedded in the DAL. The application logic just calls into the DAL and the DAL reads and writes data hiding the cache from the rest of the application. When the DAL is based on an O/RM tool, the theme of caching inevitably touches on O/RM tools. Let's find out more.  

Caching in O/RM Tools

All O/RM tools provide some caching capabilities. Normally, caching in O/RM tools is articulated in two levels. The first level is usually a transaction-level cache of persistent data that lasts for the duration of the context object. The context object caches any data it happens to retrieve so that the same record won't be retrieved twice in the same session. The first-level cache is also known as the "identity map".

The second-level caching operates down the stack below the first level of cache you typically have in the O/RM context object. The second level cache is intended to provide a cached database view so that queries can be resolved quickly on the client without hitting the database server at all. Whenever a piece of data is missing and the database server is contacted, the returned data is inserted into the cache too. Items are commonly removed from the cache based on some policies to keep the size under control.

In the end, the first level caching works on a per session basis whereas the second-level caching stores data shared across multiple sessions.

NHibernate and Entity Framework both support first-level caching out of the box. Second- level caching is usually offered through external cache providers created by third-party companies when not by the same product development group. Architecturally speaking, NHibernate and Entity Framework limit to provide injection points for adding a second-level cache. Entity Framework, however, doesn’t ship with any second-level caching provider; NHibernate, instead, ships with a few of them and others can be obtained via the community.

Second-level Caching with Entity Framework

As mentioned, you won't find a second-level cache integrated in Entity Framework. Does this mean that there's nothing you can do about it? Is Cache Aside the only way to go when you use Entity Framework in your DAL and business layer? The answer is yes, if you only want to rely on out-of-the-box and fully supported solutions. Instead, if you're ready to deal with some sample code not officially supported by Microsoft, or if you're excited about writing a custom wrapping provider for Entity Framework, well you have an alternate option.

The basic fact is that Entity Framework doesn't currently offer a simple and easy injection point for you to plug in your cache provider. This indeed represents one of the major differences with NHibernate. It rather offers a generic mechanism to extend the capabilities of an existing data provider. You could write a wrapper for an existing data provider (for a given database) and add some ad hoc capabilities including caching, tracing, and whatever else you may think of. Figure 1 shows where such wrapping providers fit in the Entity Framework pipeline.

Wrapping providers in Entity Framework.

The user code calls into the object context which, in turn, creates an Entity Framework connection. At this time, you can kick in and override the data provider configured in the EDMX script with your own. It has to be anyway an ADO.NET data provider extended with some additional capabilities.

In doing so, you take advantage of the public provider model which makes it possible for provider writers to support 3rd-party databases, such as Oracle, MySQL, PostgreSQL, and so forth. The provider interface used by Entity Framework is stackable meaning that your provider may intercept communication between Entity Framework and the original provider. In this way, the wrapper provider gets a chance do things like implementing the Cache-Through pattern, logging SQL commands, and more. 

The first step entails registering the wrapping provider with Entity Framework. This is typically done through manual intervention in the configuration file. Here's an example: 

<system.data>
   <DbProviderFactories>
       <add name="SampleCacheProvider"
            invariant="SampleCacheProvider"
            description="Sample Wrapping Provider for caching"
            type="SampleCacheProvider.EFCachingProviderFactory,
                  SampleCacheProvider" />
   </DbProviderFactories>
</system.data>

If you want to try this out, just use the sample from the previous link. If you don't like working with configuration, you can use the following fluent code:

EFCachingProviderConfiguration.RegisterProvider();

It goes without saying that EFCachingProviderConfiguration is a class in the wrapping provider we're using as an example. Here's an excerpt that shows the source code of method  

public static void RegisterProvider()
{
   DbProviderFactoryBase.RegisterProvider("SampleCacheProvider",
            "SampleCacheProvider", typeof(EFCachingProviderFactory));
}

It should be clear that a wrapping provider is simply the container of extensions. If you want caching, you have to provide it. The aforementioned sample code defines an ICache interface and implements it in an in-memory provider using a dictionary to store data. The next step consists in extending the object context with a reference to the cache provider you plan to use. 

public class ExtendedSimpleNorthwindEntities : SimpleNorthwindEntities
{
   public ExtendedSimpleNorthwindEntities()
       : this("name=SimpleNorthwindEntities")
   {
   }
   public ExtendedSimpleNorthwindEntities(String connectionString)
       : base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
                              connectionString, "SampleCacheProvider"))
   {
   }
   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; }
   }
}

At this point, we're almost done and just need to write the user-level code. Here's a sample console application: 

static 

void Main()
{
   // Register and configure  
   EFCachingProviderConfiguration.RegisterProvider();
   var cache = new InMemoryCache();
   // Do some work here
   for(var i=1; i<4; i++)
   {
       Console.WriteLine("Attempt #{0}\n==============================", i);
       var context = new ExtendedSimpleNorthwindEntities
                           {
                               Cache = cache,
                               CachingPolicy = CachingPolicy.CacheAll
                           };
       var query = from c in context.Customers where c.Country == "Italy" select c;
       // Display
       foreach(var c in query.ToList())
       {
           Console.WriteLine("{0}\n\tCITY: {1}\n\n", c.CompanyName, c.City);
       }
   }
   PressAnyKey();
}

You create a global instance of the cache provider and attach it to object context. The sample uses three different instances of the object context but only the first time a query hits the database.

SQL Profiler and the caching provider within EF.

Second-level Caching with NHibernate

The life time of the second level cache is tied to the session factory and not to an individual session. Most of the times, this means the lifetime of the application. Once an entity is loaded by its unique id and the second level cache is active the entity is available for all other sessions obtained through the same session factory. Thus once the entity is in the second level cache NHibernate won't load the entity from the database until it is removed from the cache. You enable and configure the second level cache through the configuration file where, among other things, you choose which cache provider to use. Some cache providers are natively included with NHibernate. You can configure the NHibernate cache in various ways. For example, it can be limited to load a single entity, entity sets via lazy loading, or just any query that returns a collection of entities.

In addition, third-party vendors of distributed caching systems have their own provider for NHibernate. This is the case with NCache and ScaleOut.To start out, you tell NHibernate which cache provider to use by using the following script in the hibernate.cfg.xml file as follows: 

<property name="cache.use_second_level_cache">true</property>
<property name="cache.provider_class">
     NHibernate.Caches.SysCache.SysCacheProvider, NHibernate.Caches.SysCache
</property>

If the selected cache provider does require its own settings (it will usually do), you are expected to list them all following the specific provider configuration syntax.

You must be aware that a NHibernate cache provider maintains three lists: cached entities, cached queries, timestamps. Every entity that is retrieved is cached as a sequence of primitive values (associations must be cached separately) and is identified by ID. This means that if you request a specific entity by ID you'll be served a new instance of the entity rehydrated with the stored list of values. This simplifies the internal management of data as the cache is an array of identifiers rather than a graph of objects. For entities to be cached, however, it is essential that they support caching. Here's how you enable caching on individual entities: 

<class name="Product" table="Products">
   :
   <cache usage="read-write|read-only" />
</class>

General queries that return a collection of objects can be cached as well. To enable this feature, you define the following setting:

<property name="cache.use_query_cache">true</property>

Not all queries are automatically cached because of this setting. It simply means that the feature is enabled, but only queries that explicitly claim to be cacheable are actually cached. Here's an example: 

var customers = session.CreateCriteria(typeof(Customer))
                   .SetCacheable(true)
                   .Add(Restrictions.Eq("Country", "Italy"))
                   .List<Customer>();

The SetCacheable method does the trick.The timestamp cache stores the last time a table was written to and is updated only upon the commit of a transaction. The timestamp cache is used internally by the cache provider to avoid serving stale data to users.

Finally, it is worth noting that caches don't detect any changes made to the database by another process. If this is just what happens in your scenario you might want to expire data regularly. As mentioned, a cache is bound to SessionFactory as far as its lifetime is concerned. Everything is lost as soon as the session factory is gone.



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Upvote is the only thing I can support you.

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @haig I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Coin Marketplace

STEEM 0.13
TRX 0.33
JST 0.034
BTC 110762.15
ETH 4299.54
USDT 1.00
SBD 0.82