Transparently Caching Queries
July 31, 2012 Leave a comment
The How and the What are often discussed when it comes to caching. As always, there is no Silver Bullet that solves all issues at once.
TecX offers one solution for the following scenario: Say you have a datasource that provides access to your data via a set of IQueryable<T> properties. The interface might look like this:
public interface ICustomerRepository { IQueryable<Customer> Customers { get; } }
Now you want to add caching but you don’t want to have to change how the consumers work with that repository. So you need some transparent caching mechanism that isolates your consumers (and your developers) from the actual implementation of caching. You are then able to run your application without caching or you can use the new features from System.Runtime.Caching, the AppFabric Cache or something completely different.
Two classes from TecX.Caching (namely QueryInterceptor and QueryInterceptorProvider) allow for transparent interception of queries against an IQueryable<T>. They are wrappers for IQueryable and IQueryProvider that use the Expression property of the IQueryable to generate a unique cache key. A simple ToString() operation won’t give you a key that is “uniqe enough” so a technique presented by Pete Montgomery is used to partially evaluate the expression tree of the query.
The sample CachingCustomerRepository demonstrates how this interception can be used to introduce a transparent caching layer into your application and swap it out for arbitrary implementations using different caching technologies at any time.
public class CachingCustomerRepository : ICustomerRepository { private readonly ICustomerRepository inner; private readonly ObjectCache cache; private readonly QueryInterceptor<Customer> customers; private readonly ExpirationToken expirationToken; public CachingCustomerRepository(ICustomerRepository inner) { this.inner = inner; this.cache = new MemoryCache(typeof(CachingCustomerRepository).Name); this.customers = new QueryInterceptor<Customer>(this.inner.Customers); this.customers.QueryProvider.Executing += this.OnQueryExecuting; this.customers.QueryProvider.Executed += this.OnQueryExecuted; this.expirationToken = new ExpirationToken(); } public IQueryable<Customer> Customers { get { return this.customers; } } public void Add(Customer customer) { this.inner.Add(customer); this.expirationToken.Expire(); } private void OnQueryExecuted(object sender, ExpressionExecuteEventArgs e) { IQueryable<Customer> cachedResult = this.cache[e.CacheKey] as IQueryable<Customer>; if (cachedResult == null) { var evaluatedQueryable = ((IEnumerable<Customer>)e.Result).ToList().AsQueryable(); CacheItem cacheItem = new CacheItem(e.CacheKey, evaluatedQueryable); CacheItemPolicy policy = new CacheItemPolicy { SlidingExpiration = 1.Minutes() }; ExternallyControlledChangeMonitor monitor = new ExternallyControlledChangeMonitor { ExpirationToken = this.expirationToken }; policy.ChangeMonitors.Add(monitor); this.cache.Add(cacheItem, policy); } } private void OnQueryExecuting(object sender, ExpressionExecuteEventArgs e) { IQueryable<Customer> cachedResult = this.cache[e.CacheKey] as IQueryable<Customer>; if (cachedResult != null) { e.Handled = true; e.Result = cachedResult; } } }
Using a framework like Moq you can easily mock your actual data access in order to run tests against your cache.
var mock = new Mock<ICustomerRepository>(); mock.SetupGet(r => r.Customers).Returns( new[] { new Customer { Id = 1, Name = "1" }, new Customer { Id = 2, Name = "2" }, new Customer { Id = 3, Name = "3" } }.AsQueryable()); var cache = new CachingCustomerRepository(mock.Object); // Actual testing code mock.VerifyGet(r => r.Customers, Times.Once());
At the heart of the CachingCustomerRepository are the QueryInterceptor (comes in a generic and a non-generic version)
public class QueryInterceptor<T> : QueryInterceptor, IQueryable<T> { public QueryInterceptor(IQueryable<T> wrapped) : this(wrapped, new QueryInterceptorProvider(wrapped.Provider)) { } public QueryInterceptor(IQueryable<T> wrapped, QueryInterceptorProvider provider) : base(wrapped, provider) { } public IEnumerator<T> GetEnumerator() { var enumerable = this.Provider.Execute<IEnumerable<T>>(this.Expression); var enumerator = enumerable.GetEnumerator(); return enumerator; } } public class QueryInterceptor : IQueryable { private readonly IQueryable wrapped; private readonly QueryInterceptorProvider queryProvider; public QueryInterceptor(IQueryable wrapped, QueryInterceptorProvider provider) { this.wrapped = wrapped; this.queryProvider = provider; } public Type ElementType { get { return this.wrapped.ElementType; } } public Expression Expression { get { return this.wrapped.Expression; } } public IQueryProvider Provider { get { return this.queryProvider; } } public QueryInterceptorProvider QueryProvider { get { return this.queryProvider; } } IEnumerator IEnumerable.GetEnumerator() { var enumerable = (IEnumerable)this.Provider.Execute(this.Expression); var enumerator = enumerable.GetEnumerator(); return enumerator; } }
and the QueryInterceptorProvider.
public class QueryInterceptorProvider : IQueryProvider { private readonly IQueryProvider wrapped; public QueryInterceptorProvider(IQueryProvider wrapped) { this.wrapped = wrapped; } public event EventHandler<ExpressionExecuteEventArgs> Executing = delegate { }; public event EventHandler<ExpressionExecuteEventArgs> Executed = delegate { }; public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { var rawQuery = this.wrapped.CreateQuery<TElement>(expression); var interceptor = new QueryInterceptor<TElement>(rawQuery, this); return interceptor; } public IQueryable CreateQuery(Expression expression) { var rawQuery = this.wrapped.CreateQuery(expression); var interceptor = new QueryInterceptor(rawQuery, this); return interceptor; } public TResult Execute<TResult>(Expression expression) { string cacheKey = expression.GetCacheKey(); object value; bool handled = this.NotifyExecuting(expression, cacheKey, out value); TResult result = !handled ? this.wrapped.Execute<TResult>(expression) : (TResult)value; this.NotifyExecuted(expression, cacheKey, result); return result; } public object Execute(Expression expression) { string cacheKey = expression.GetCacheKey(); object value; bool handled = this.NotifyExecuting(expression, cacheKey, out value); object result = !handled ? this.wrapped.Execute(expression) : value; this.NotifyExecuted(expression, cacheKey, result); return result; } private bool NotifyExecuting(Expression expression, string cacheKey, out object result) { var e = new ExpressionExecuteEventArgs { Expression = expression, CacheKey = cacheKey }; this.Executing(this, e); if (e.Handled) { result = e.Result; return true; } result = null; return false; } private void NotifyExecuted(Expression expression, string cacheKey, object result) { var e = new ExpressionExecuteEventArgs { Expression = expression, CacheKey = cacheKey, Result = result }; this.Executed(this, e); } }
The code above illustrates one possible solution for one problem: How can I introduce a completely transparent caching layer around my data access?
Explicit caching on the contrary allows you to optimize what you put in the cache and when. This video by the guys behind StackOverflow shows which performance improvements and hardware savings are possible when you make caching very explicit.
Whenever you introduce caching to your application there are some questions that need to be answered:
- Do I really have to cache every query?
- Can I reuse parts of my results?
- Will I run this exact query often enough to justify caching?
- What happens when data changes (e.g. by calling UpdateCustomer)?
- How much does performance really improve by introducing caching?
- Does caching have an influence on the consistency of my results?
- …
Don’t use caching if you can’t reason that the benefits outweigh the costs. Caching can cause a lot of trouble if it’s not done the right way…
Get the source code for transparent caching here (project TecX.Caching and the test suite that shows how to use it in TecX.Caching.Test).