On a current project we’re working on in our company, we are developing a web application which accesses the back-end through some web services. Nothing special really, except that certain web services provide pretty static information like lookup tables, which don’t change very often, so it’s not really necessary to refetch them all the time.

Since we are heavily relying on the dependency injection and all the back-end services are exposed through C# interfaces, it wasn’t really hard to develop a new implementation of such an interface which calls the back-end service and then caches the results in the HTTP cache to be used by any subsequent calls.

Here’s a simple example of a service interface:

public interface IBackendService
{
    List<string> ListSomeStrings(string parameter1, int parameter2);
}

I won’t bore you with an implementation of this, since it’s really a dumb one. What’s interesting is how the cached implementation looks like:

public class BackendServiceCachingProxy : CachingProxyBase<IBackendService>, IBackendService
{
    public BackendServiceCachingProxy(
        IBackendService wrappedService, 
        Cache cache,
        TimeSpan slidingCacheExpiration)
        : base(wrappedService, cache, slidingCacheExpiration)
    {
    }

    public List<string> ListSomeStrings(string parameter1, int parameter2)
    {
        return CallServiceMethod<string>(
            () => ConstructCacheKey("SomeStrings", parameter1, parameter2),
            s => s.ListSomeStrings(parameter1, parameter2));
    }
}

Our caching wrapper inherits from a base class CachingProxyBase. Before showing you the code for CachingProxyBase, let me just quickly explain what the BackendServiceCachingProxy code does.

We’re using HTTP cache, so we have to supply it to the class in the constructor. We also specify how long the cache should be valid (slidingCacheExpiration parameter).

Each service method in the caching wrapper now uses CallServiceMethod method to implement the cached service method calls. You specify two parameters:

  1. A function delegate for constructing the cache key. This key should be unique for each unique combination of method’s input parameters. The CachingProxyBase offers a helper method ConstructCacheKey to help you with this task.
  2. A function delegate for calling the method on the service. This delegate will be used for calling the actual service implementation which will contact the back-end.

 

CachingProxyBase

And now the implementation of the CachingProxyBase:

public abstract class CachingProxyBase<TService>
{
    public void ExpireAllCachedData()
    {
        foreach (System.Collections.DictionaryEntry entry in cache)
            cache.Remove(entry.Key.ToString());
    }

    protected CachingProxyBase(
        TService wrappedService,
        Cache cache,
        TimeSpan slidingCacheExpiration)
    {
        this.wrappedService = wrappedService;
        this.cache = cache;
        this.slidingCacheExpiration = slidingCacheExpiration;
    }

    protected Cache Cache
    {
        get { return cache; }
    }

    protected TimeSpan SlidingCacheExpiration
    {
        get { return slidingCacheExpiration; }
    }

    protected TService WrappedService
    {
        get { return wrappedService; }
    }

    protected TValue CallServiceMethod<TValue>(
        Func<string> constructCacheKeyFunc,
        Func<TService, TValue> serviceMethodFunc)
        where TValue : class
    {
        string cacheKey = constructCacheKeyFunc();

        TValue cachedValue = GetCachedValue<TValue>(cacheKey);

        if (cachedValue == null)
        {
            cachedValue = serviceMethodFunc(WrappedService);
            CacheValue(cacheKey, cachedValue);
        }

        return cachedValue;
    }

    protected string ConstructCacheKey(
        string function, 
        string environmentId, 
        params object[] args)
    {
        StringBuilder cacheKeyBuilder = new StringBuilder();
        cacheKeyBuilder.AppendFormat(
            CultureInfo.InvariantCulture,
            "Proxy-{0}-{1}",
            function,
            environmentId);

        foreach (object arg in args)
            cacheKeyBuilder.AppendFormat(CultureInfo.InvariantCulture, "-{0}", arg);

        return cacheKeyBuilder.ToString();
    }

    private void CacheValue(string cacheKey, object value)
    {
        Cache.Insert(
            cacheKey,
            value,
            null,
            Cache.NoAbsoluteExpiration,
            SlidingCacheExpiration);
    }

    [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
    private T GetCachedValue<T>(string cacheKey) 
    {
        return (T)cache.Get(cacheKey);
    }

    private readonly TService wrappedService;
    private readonly Cache cache;
    private readonly TimeSpan slidingCacheExpiration;
}

TService is a generic parameter representing the service interface the proxy is enhancing with the caching. I think the code is pretty self-explanatory. The ExpireAllCachedData method is exposed to be used in unit tests to make sure the cache is empty before doing any work.

Using It With Windsor Castle

I’ve made a helper method for configuring in Windsor Castle a specific back-end service with or without caching:

[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
protected void RegisterServiceWithOptionalCache<TService, TImplementation, TCachedImplementation>(
    IWindsorContainer container,
    TimeSpan slidingCacheExpiration)
    where TImplementation : TService
    where TCachedImplementation : TService
{
    string serviceName = typeof(TService).Name;

    if (IsCachingUsed)
    {
        container.Register(
            Component.For<TService>().ImplementedBy<TCachedImplementation>()
                .ServiceOverrides(ServiceOverride.ForKey("wrappedService").Eq(serviceName))
                .Parameters(Parameter.ForKey("slidingCacheExpiration").Eq(slidingCacheExpiration.ToString())));

        if (log.IsDebugEnabled)
            log.DebugFormat("Registered {0}", typeof(TCachedImplementation).Name);
    }

    container.Register(
        Component.For<TService>().ImplementedBy<TImplementation>()
            .Named(serviceName));            
}

 

Conclusion

There are probably more elegant ways of doing this (probably using Windsor’s interceptors and reflection), but I think this code does its job well if you have a small number of service methods to cover.