The conception, birth, and first steps of an application named Charlie

Subscribe: Atom or RSS

The EntityCache Object

by Alister Jones (SomeNewKid)

When I first designed the Entity System for Charlie, I gave the EntityManager properties for IsCacheable, CacheDependency, CacheItemPriority, CacheSlidingExpiration, and so on. With these properties, each concete Manager object can specify how caching should be applied to any Entity or EntityCollection objects it returns. For example, the RoleManager can specify whether the Role objects it returns should be cached and, if so, how they should be cached.

To start with, I set the IsCacheable property to false for every Manager. By effectively disabling the cache, I could concentrate on the functionality of the business objects, leaving caching as a final optimisation step. As I explained in my earlier weblog entry, I brought forward the plan to implement caching of the business objects. I left all of the caching properties in the base EntityManager class, but moved the actual caching work to a new EntityCache class. That way, I can change the caching mechanism without affecting the EntityManager object, which doesn’t really care how caching is implemented.

Let’s take a quick look at how caching works in the context of the Entity System. Specifically, it is the job of the base EntityManager object to test whether the requested Entity already exists in cache. Here is a cut-down version of the relevant code:

public abstract class EntityManager
{
    protected Entity LoadEntity(EntityCriteria criteria)
    {
        String cacheKey = // to be discussed
        Object cached = this.GetEntityFromCache(cacheKey);
        
        if (cached != null && cached is Entity)
        {
            Object clone = ((Entity)cached).Clone();
            entity = (Entity)clone;
        }
        else
        {
            entity = this.Mapper.Retrieve(entity, criteria);
            this.AddEntityToCache(cacheKey, entity);
        }
        return entity;
    }
}

To see why the cached Entity is cloned before being returned to the calling class, please refer to my previous weblog entry.

The very first problem to be solved is what key value can we use to determine whether the cache contains the Entity being requested? It is not enough that we simply use the ID value of the Entity being requested. After all, an ArticleEntity with an ID value of 32 is not the same as a WeblogEntity with an ID value of 32. Further, it is not enough to use a key representing the entity type and the ID value (such as “Charlie.Articles.Article.32”), since localisation means that the cached entity may be the English version, when the incoming request is for the French version. This suggests that we could use a key that is a composite of the entity type, the culture, and the ID (such as “Charlie.Articles.Article.32.en-US”). The problem here is that the concrete ArticleManager may require more fine-grained caching, such as caching one version of the Article together with comments by visitors, and another version of the Article without comments by visitors. But the base EntityManager class cannot possibly know of all the caching variations required by the concrete manager classes. So, how can the base EntityManager class implement caching, when it cannot know of the variations required by the concrete manager classes?

Fortunately, this was a very easy problem to solve. We need only look closely at the LoadEntity method of the base EntityManager class to see that there is something that uniquely describes the Entity being requested, and that that something properly reflects all of the caching variations needed:

public abstract class EntityManager
{
    protected Entity LoadEntity(EntityCriteria criteria)
    {
        String cacheKey = criteria.ToString();
        // rest of the class
    }
}

To understand how the criteria reflects all of the caching variations needed, here is a pretend ForumCriteria class:

public class ForumCriteria
{
    public Int32   ID;
    public Boolean LoadById;
    
    public String  Name;
    public Boolean LoadByName;

    public Culture Culture;
    public Boolean LoadByCulture;

    public Boolean LoadReplies;

    public Boolean LoadAvatars;
    
    public override String ToString()
    {
        return
            "Charlie.Forums.Forum" +
            ID + LoadById +
            Name + LoadByName +
            Culture + LoadByCulture +
            LoadReplies +
            LoadAvatars;
    }
}

When the base EntityManager class calls criteria.ToString(), it will end up with a unique cache key, such as:

Charlie.Forums.Forum.32.True..False.en-US.True.True.False

This means that the base EntityManager will always cache entities in a way that precisely reflects how that entity was requested by the incoming criteria. Problem solved. We could actually use reflection instead of forcing the developer to implement a custom ToString() method. However, in the comments in my code, I have said the following:

//  Yeah yeah, we could use reflection to inspect this criteria class.
//  But, what's the point of using Cache for speed if we use slow old
//  reflection to get the cache key?

The second problem to be solved was getting the EntityCache class to return lazy-loaded entity objects, and not fully-loaded entity objects. My last two weblog entries described the problems I faced here, and the solution provided by cloning entities.

The final problem to be solved was whether to use the intrinsic ASP.NET cache, or implement a custom caching mechanism. Initially the plan was to use the intrinsic cache, which is why the EntityManager class provides the following properties (simplified as fields):

public abstract class EntityManager
{
    protected Boolean                  IsCacheable;
    protected CacheDependency          CacheDependency;
    protected CacheItemPriority        CacheItemPriority;
    protected CacheItemRemovedCallback CacheItemRemovedCallback;
    protected DateTime                 CacheAbsoluteExpiration;
    protected TimeSpan                 CacheSlidingExpiration;
}

This allows each concrete manager (such as the RoleManager) to precisely describe whether the entity handled by this manager (the Role entity) should be cached and, if so, how it should be cached. If you consider that the CacheDependency property can be a SqlCacheDependency, you will appreciate just how flexible is the intrinsic ASP.NET cache.

I then considered using a custom cache, based on that used by Paul Wilson in his WebPortal project. But I eventually decided against this approach. To start with, my recent foray into caching proved that I don’t have a great understanding of reference types, and I had also learned that I don’t have a solid understanding of static classes. More importantly, I felt that the flexibility of the intrinsic ASP.NET cache was too beneficial to give up. I decided that Charlie would use the built-in cache, but with two enhancements.

The first enhancement was to use the idea provided by the Wilson WebPortal, even though I would not use its implementation. Specifically, if the concrete manager specified that the CacheItemPriority was High (which is the highest setting available), then the EntityCache would not only add a clone to the built-in cache, it would also add the same clone to a custom HighPriorityCache. This custom HighPriorityCache was nothing more than a static class that exposed a Hashtable. Because the cached item was being referenced by a static class, that item would not be removed from the cache. Without that reference, the item could be removed from the cache. All this really means is that when the EntityCache sees that an entity has a CacheItemPriority of High, it performs a little trick that moves the priority from high to permanent—that item simply will not be expired from cache within the lifetime of the Charlie application.

The second enhancement was to come up with a way to immediately expire objects from the cache. With the intrinsic ASP.NET cache, it is not possible to forcibly expire an item from the cache. You cannot expire the item, nor can you set it to null. However, I desperately wanted to give Charlie the ability to instantly expire any cached Entity or EntityCollection. The reason I wanted to do this is because experience on other websites has shown that it is a pain in the ass when you make changes to the website, but those changes are not reflected for up to 15 minutes (or whenever the cache expires). I don’t want to subject Charlie’s website owners to that same frustrating experience.

The solution was very simple. The EntityCache class works only with the EntityManager class, which in turn works only with Entity and EntityCollection objects. So, I gave both the Entity and the EntityCollection classes an internal IsExpired property.

internal Boolean IsExpired
{
    get
    {
        return this.isExpired;
    }
    set
    {
        this.isExpired = value;
    }
}
private Boolean isExpired;

Then, when the EntityCache class receives a request to remove an item from its cache, it looks first to see whether that item is in cache. If that item is in cache, it cannot actually delete that item—the ASP.NET cache does not allow this. Instead, the EntityCache class marks the cached Entity or EntityCollection as being expired.

internal void Remove(String cacheKey)
{
    Object cached = HttpContext.Current.Cache[thisKey];
    if (cached != null)
    {
        if (cached is Entity)
        {
            ((Entity)cached).IsExpired = true;
        }
        else if (cached is EntityCollection)
        {
            ((EntityCollection)cached).IsExpired = true;
        }
    }
}

Then, when the EntityCache receives a request for a cached Entity or EntityCollection, it will look to see whether there is a cached item and, if so, whether that item is marked as expired. If there is no cache item, or if there is a cached item but it is marked as expired, the EntityCache will return a null value.

internal Object Get(String cacheKey)
{
    Object cached = HttpContext.Current.Cache[cacheKey];
    if (cached == null)
        return null;
    if (cached is Entity)
    {
        if (((Entity)cached).IsExpired)
        {
            return null;
        }
        else
        {
            return cached;
        }
    }
    else if (cached is EntityCollection)
    {
        if (((EntityCollection)cached).IsExpired)
        {
            return null;
        }
        else
        {
            return cached;
        }
    }
    return null;
}

When the EntityManager class receives a request to Save an Entity or EntityCollection, the manager will automatically expired any cached copies. When the Entity or EntityCollection is next requested, it will be drawn from the database, which means it will always reflect the most recently committed changes. This was the problem to be solved, and it was solved with minimal code changes. (By the way, I know that the above code could be compressed. However, I feel no motivation to compress code if it would make it harder to understand what is going on. A lot of my code will look relatively verbose, but that’s fine by me. It’s fine by the compiler, too.)

After a few bumbling steps, Charlie’s Entity System is now pretty darn fast. It now makes use of an enhanced cache that stops high priority items from expiring, and that allows for immediate expiration of updated items. And there is no O/R mapping or reflection to slow down the Entity System.

by Alister Jones | Next up: Charlie’s Alarm Clock

0 comments

----