Optimizing Read Performance with Entity Framework 4.0

by Larry Spencer Sunday, January 8, 2012 11:41 AM

I've been using the Entity Framework to access a database of users and their privileges. One important characteristic of this database is that its contents rarely change. New users may be added occasionally, or someone may change his password, but usually life just hums along. The database comes close to being read-only.

Our application was having performance problems, and we discovered that a lot of time was being spent accessing the user database. Here's the solution that I designed. 

Outline of the Solution

  1. Establish a cache of the authorization data.
  2. Add to the cache as data are retrieved.
  3. Get data from the cache when possible.
  4. Clear the cache when changes are made.

Details

1) Establish the cache.

In our application, all access to the database goes through a WCF service. I created a Lazy-initialized singleton of the cache as a member variable of the service (actually of the next layer down, but let's skip over those details). I've written before about how handy the System.Lazy class is for creating singletons in a thread-safe way.

The cache class happens to be my own ConcurrentCache class. You could also use .NET Framework's MemoryCache class. Whichever flavor of cache you use, it will have a Key and a Value.

My Key is a string that can represent the user's identity in any of several ways (not important for this article), and its Value is an array of the user's Claims (privileges).

 

// Cache of claims fetched for the user. 
static Lazy<ConcurrentCache<string,Claim[]>> _claimsCache 
    = new Lazy<ConcurrentCache<string,Claim[]>>(() =>
  {
    return new ConcurrentCache<string,Claim[]>(
        new TimeSpan(0,2,0),    // Objects expire after this long...
        new TimeSpan(0,1,0),    // ...or even if they have not been accessed for this long.
        120);                   // Clean up the cache every so many seconds.
  },
  true /*thread-safe*/);

 

2) Add data to the cache as they are retrieved.

3) Get data from the cache when possible.

(These two points were the same body of code for me.)

Although my application retrieved authorization data in several ways, all ended up in a common method at the lowest level. I modified that method so it first attempted to obtain data from the cache, and only if necessary did it go to the database.

4) Clear the cache when changes are made.

With Entity Framework, data changes in two stages. First, you change the data in the context. And then, you call SaveChanges. For my purpose, I needed to invalidate the cache at both events.

4.a) ...when changes are applied to the Entity Framework context.

This step would be easy to forget! If you change something in your context, and then later retrieve through the same context, you might not retrieve your changes. Instead, you might get what's in the cache. What you do about this depends on your situation. For mine, wherever I called ApplyChanges(), I had to clear the cache:

 

_claimsCache.Value.Clear();

 

4.b) ...when saving to the database.

What if I were to forget to clear the cache in one of the places of 4.a? As an extra precaution, I chose to override the Entity Framework's SaveChanges method and clear the cache there. This will absolutely guarantee that any changes to the database will clear the cache.

...or, I should say that any changes made with my WCF service will clear the cashe. Fortunately, I can be sure of that my WCF service is the only thing messing with my database. If I weren't sure, I could set up a SQL notification with the SqlDependency and SqlChangeMonitor classes, but that was overkill for me.

Here's how simple it is to override SaveChanges for this purpose. Note that the SaveChanges method is only virtual starting in Entity Framework 4.0. If you're using an earlier version, here's another reason to upgrade!

public override int SaveChanges(SaveOptions options)
{
    var ret = base.SaveChanges(options);
    _claimsCache.Value.Clear();
    return ret;
}

 

To clear the whole cache just because one little thing changed is very conservative, but it is foolproof and was adequate for my purpose.

Tags: ,

Entity Framework | WCF | All

About the Author

Larry Spencer

Larry Spencer develops software with the Microsoft .NET Framework for ScerIS, a document-management company in Sudbury, MA.