Much like I typically wrap my DataReaders in the CSLA.NET's SafeDataReader to better handle DB Null's, I've taken to wrapping .NET's built-in Cache object in order to improve the API, decreasing my code's coupling and improve testability.
Three things always bug me about the Cache's API:
1 - Inserting null data throws an exception
2 - Never seems to have the right overload
3 - Repetitiveness of creating composite keys
First thing to do is create an ICacheManager interface, we’ll keep it simple:
namespace CodeBetter
{
public interface ICacheManager
{
void Insert(string key, object data, TimeSpan slidingExpiration, params object[] args);
void Insert(string key, object data, DateTime absoluteExpiration, params object[] args);
void Insert(string key, object data, CacheDependency dependency, DateTime absoluteExpiration, TimeSpan slidingExpiration, Priority priority, params object[] args);
T Get<T>(string key, params object[] args);
}
}
In my experience, I almost always reuse the same Insert overloads - I rarely ever set a priority (let .NET manage its own memory usage I say) or specify both an Absolute and Sliding expiry, and not too frequently specify a CacheDependency. So I create a couple Insert that'll get used 95% of the time without having to specify a bunch of null/defaults AND a one that lets me specify everything.
Internally I handle null data by simply returning out of the insert:
if (data == null)
{
return;
}
Finally, you probably noticed that in each case above I have a params object[] args. That's because I often find myself using composite keys, something like:
string key = string.Format("UserById:{0}", userId);
By letting my insert handle that, my API is a bit cleaner. (A nice thing about params is that you don't have to specify a property at all)
Here are a couple sample calls:
Insert("UserById:{0}", user, TimeSpan.FromMinutes(10), user.Id);
Insert("SystemConfiguration", configData, DateTime.Now.AddHours(2));
And here's the relevant code:
public class InMemoryCacheManager : ICacheManager
{
public void Insert(string key, object data, TimeSpan slidingExpiration, params object[] args)
{
//call overload...
}
public void Insert(string key, object data, DateTime absoluteExpiration, params object[] args)
{
//call overload...
}
public void Insert(string key, object data, CacheDependency dependency, DateTime absoluteExpiration, TimeSpan slidingExpiration, Priority priority, params object[] args)
{
if (key == null)
{
throw new ArgumentNullException("key");
}
if (data == null)
{
return;
}
if (args.Length > 0)
{
key = string.Format(key, args);
}
HttpRuntime.Cache.Insert(key, data, dependency, absoluteExpiration, slidingExpiration, priority);
}
public T Get<T>(string key, params object[] args)
{
if (args.Length > 0)
{
key = string.Format(key, args);
}
return (T)HttpRuntime.Cache.Get(key);
}
}
Although I find the syntactical improvements pleasant, the real benefit is with the introduction of the ICacheManager interface I've reduced my system's coupling and can easily create an IMemCachedMemoryManager. It's also easy to Mock up, and plays nice with tools like StructureMap:
MockRepository mock = new MockRepository();
ICacheManager cacheManager = _mock.CreateMock<ICacheManager>();
IDataStore dataStore = _mock.CreateMock<IDataStore>();
ObjectFactory.InjectStub(typeof(IDataStore), dataStore);
ObjectFactory.InjectStub(typeof(ICacheManager), cacheManager);
User user = new User { UserId = 34 };
using (_mock.Ordered())
{
Expect.Call(dataStore.LoadUserFromCredentials("username", "password")).Return(user);
_cacheManager.Insert("UseById:{0}", user, TimeSpan.FromMinutes(10), user.UserId);
}
mock.ReplayAll();
Assert.AreSame(user, new Repository().CreateUserFromCredentials("username", "password"));
mock.VerifyAll();
Posted
Sun, Jun 3 2007 4:36 PM
by
karl