Expose redis cache to other layers

I have the following project structure and have a query with how best to expose the registered cache provider to them.

  • Data Projects
    – Data.Project.1
  • Cache Projects
    – CacheManager
  • Web Service Projects
    – Web.Service.Project.1
    The Data project is responsible for retrieving information from 3rd parties. These responses are then cached (in redis).

Web service and Data projects have a dependency on the Cache Manager project.

In the web service project, the cache provider is registered:

               // cache
                container.Register<IRedisClientsManager>(c => new PooledRedisClientManager("connection string"));
                container.Register(c => c.Resolve<IRedisClientsManager>().GetCacheClient());

I then set a reference to the container in the cache manager project so anyone else who needs access to the cache can:

                // set the container for cache to use
                CacheManager.SetContainer(container);

The cache manager then looks something as follows:

    public static class CacheManager
    {
        private static ICacheClient _client;
        private static ICacheClient Client
        {
            get
            {
                if (_client == null)
                {
                    _client = Container == null ? new MemoryCacheClient() : Container.Resolve<ICacheClient>();
                }
                
                return _client;
            }
        }

        private static Container Container;

        public static void SetContainer(Container container)
        {
            Container = container;
        }
	}

Now any projects that reference the cache manager project can go:

CacheManager.Client.Add("test", new List<int>());

What I want to find out is if this is the right way to go about this?

That looks a bit backwards, I wouldn’t be accessing the IOC in the cache manager, just register the ICacheClient it uses when you register the Cache manager, e.g:

container.Register(c => new CacheManager(c.Resolve<ICacheClient>()));

Then anything you register in the IOC can be resolved with the singleton:

var cacheManager = HostContext.TryResolve<CacheManager>();

Which will be naturally populated with the registered caching provider.

Since it’s relevant we have specific API’s to access the registered ICacheClient with HostContext.Cache as well as the alternative in-memory cache with HostContext.LocalCache.

Alternative solution using Extension methods

I personally like to avoid wrappers if I can, so if this wrapper class doesn’t hold any state and is just a bag of functionality, I’d prefer to just add extension methods over the dependency you’re adding functionality over, e.g:

pubilc static class CacheClientExtensions
{
    public static void Add<T>(this ICacheClient cache, string key, T value)
    {
        cache.Set(key,value);
    }
}

This way you don’t have to concern yourself with resolving instances from the IOC or instance scopes etc and anyone will be able to access that functionality directly with:

HostContext.LocalCache.Add(key,value);
HostContext.Cache.Add(key,value);

That also has the advantage of letting you use the enhanced functionality with any ICacheClient instance. We’re heavy proponents of extension methods which we believe allows for more intuitive, flexible and easier to reason about API’s whilst requiring minimal abstractions, for these reasons OrmLite, HTTP Utils and lots of ServiceStack makes heavy usage of extension methods.

That all makes sense - thanks for the insight. I did not know that HostContext was available globally. Let me see what I can put together.

I’ve gone down the road of still keeping the CacheManager as extension methods wouldn’t work in this scenario.

However how would I access the MemoryCacheClient at registration time?

    public class CacheManager
    {
        private readonly ICacheClient _remoteCache;
        private readonly ICacheClient _localCache;

        public CacheManager(ICacheClient remoteCache, ICacheClient localCache)
        {
            _remoteCache = remoteCache;
            _localCache = localCache;
        }
    }
                // cache
                container.Register<IRedisClientsManager>(c => new PooledRedisClientManager("connection string"));
                container.Register(c => c.Resolve<IRedisClientsManager>().GetCacheClient());
                container.Register(c => new CacheManager(c.Resolve<ICacheClient>(), ????));

To access MemoryCacheClient was registered before, you can use

 container.Register(c => new CacheManager(c.Resolve<ICacheClient>(), HostContext.LocalCache));

If you did not register MemoryCacheClient yet, you have to register it first:

 container.Register(c => new MemoryCacheClient());
 container.Register(c => new CacheManager(c.Resolve<ICacheClient>(), HostContext.LocalCache));

I’ll also point out that just because you register the CacheManager, doesn’t mean the dependency needs to be available at that time, so if you’re not resolving the CacheManager on Start up you could just have:

container.Register(c => new CacheManager(
    c.Resolve<ICacheClient>(), c.Resolve<MemoryCacheClient>()));

Where after your AppHost is initialized if you haven’t registered a MemoryCacheClient ServiceStack will automatically register one for you so it will be available at runtime. But if you wanted to resolve an instance of CacheManager immediately at Startup, e.g:

var cacheManager = container.Resolve<CacheManager>();

Then you would need to register the MemoryCacheClient yourself, e.g:

container.Register(c => new MemoryCacheClient());

Which is all that HttpContext.LocalCache resolves.