Starting from the existing code of the previous article, we experimented the caching feature by adding an outsider behavior into the existing data access layer. This strategy is very fast and it helps us to determine if the problem is addressed correctly and the technologies used are appropriate. Now, we should refactor it to fit elegantly the pre-existing design, without coupling too much the “caching” scope with the “data access” one.
Let’s start by decoupling a little bit the previous code. Instead of creating a concrete class DAL, let’s define an interface like this:
public interface IDataService
{
Order[] GetOrdersFor(int customer);
}
Consequently, the previous DAL
class, becomes an implementation of the IDataService
that goes to the Database to materialize data:
public class DAL:IDataService
{
public Order[] GetOrdersFor(int customer)
{
var db = new MyEntities();
return db.Orders.Where(p => p.CustomerID == customer).ToArray();
}
}
And we are at the starting point, where the consumer actor can now use the DAL implementation through the IDataService
interface:
IDataService dal = new DAL();
var orders = dal.GetOrdersFor(10);
Now we are in front of two options:
- Writing another implementation of
IDataService
, to perform both the Data Access logic and the Caching one. This option, however, leads to a duplication of code (the Data Access part). - Implementing something related only to caching and attaching it to the Data Access implementation in some way, keeping the two scope un-related.
It can be better first to implement something which relates to Caching and, second, the proper glue to attach it to the existing DAL implementation.
Here we have an example of caching implementation through the IDataService
interface:
public class DataCaching : IDataService
{
private IDataService service;
private MemoryCache cache = MemoryCache.Default;
public DataCaching(IDataService service)
{
this.service = service;
}
public Order[] GetOrdersFor(int customer)
{
var key = $"GetOrdersFor_{customer}";
var cachedItem = cache.Get(key);
if (cachedItem != null) return (Order[])cachedItem;
else
{
var result = service.GetOrdersFor(customer);
cache.Set($"GetOrdersFor_{customer}", result,
new CacheItemPolicy()
{
SlidingExpiration = TimeSpan.FromSeconds(10)
});
return result;
}
}
}
The DataCaching
class is an implementation of the IDataService
interface, providing a Caching Layer not on the specific DAL implementation but on a generic IDataService
underlying implementation, as a decorator.
This line:
var result = service.GetOrdersFor(customer);
is the chained call to an implementation which will return the Order[]
object.
Let’s think about this for a minute. We are not telling the DataCaching class which would be the actual implementation of the service reference.
The typical usage in the scenario we discussed, can be the following:
var dal = new DataCaching(new DAL());
var orders = dal.GetOrdersFor(10);
But we can, also, chain the same implementation two times:
var dal2 = new DataCaching(new DataCaching(new DAL()));
The result works too but, it’s useless. In case of a cache miss, it results in a two consecutive lookup calls to the Caching layer plus two consecutive write calls to it. It could make sense with different implementations, for instance where a first-level in-process cache can call an underlying remote, distributed, second-level cache service:
var dal3 = new DataCaching(new RedisCaching(new DAL()));
Finally, we can even prevent the DataCaching implementation to receive in the constructor another decorator, but only a final implementation, using C# generics contraints:
public class DataCaching<T> : IDataService where T:IDataService,new()
{
private T service;
private MemoryCache cache = MemoryCache.Default;
public DataCaching(T service)
{
this.service = service;
}
[…]
}
This new definition requires, during the object construction, to determine an actual implementation of IDataService
which has a parameter less constructor. We also need to change the instantiation code into this:
var dal = new DataCaching<DAL>(new DAL());
While the other one won’t be allowed anymore:
var dal2 = new DataCaching<DataCaching<DAL>>
(new DataCaching<DAL>(new DAL())); //Invalid
While this approach works, it can be defined as a workaround. We can later implement something more elegant in order to avoid unwanted behaviors while using these implementations.
Now we want to justify a little bit of refactoring, so let’s take a new member into the IDataService
interface:
public interface IDataService
{
Order[] GetOrdersFor(int customer);
Order GetOrder(int id);
}
We added the Order GetOrder(int id)
method definition that leads to a compilation error, until we’ve implemented it into all the non-abstract implementations currently using the interface.
This is a powerful feature of statically-typed languages, since we are embraced by the compiler who don’t let us go too far away without the proper code updates. In case of several implementations, we could be tempted to write a quick-and-dirty empty implementation in all the concrete classes, to avoid compilation errors and proceed working/debugging. However, it is better to isolate implementations or implement the methods by throwing the NotImplementedException
:
public Order GetOrder(int id)
{
throw new NotImplementedException();
}
This, as a last resort, will help you to identify (during runtime, however) the origin of the exception to consequently fix it with real code.
Refining and reducing duplicated code
Since an additional IDataService
method can generate duplicate code in the method bodies, we can now introduce further optimizations, as follows:
public class DAL : IDataService
{
private Func<MyEntities> contextFactory = null;
public DAL(Func<MyEntities> contextFactory)
{
this.contextFactory = contextFactory;
}
public Order GetOrder(int id)
{
return contextFactory().Orders
.FirstOrDefault(p => p.ID == id);
}
public Order[] GetOrdersFor(int customer)
{
return contextFactory().Orders
.Where(p => p.CustomerID == customer).ToArray();
}
}
Why a generator function instead of the actual instance?
The reader may notice we required a generator function in the constructor of the DAL class, instead of requiring directly theMyEntities
instance. We did this because we assumedMyEntities
is a DbContext-derived class of Entity Framework. In Entity Framework, theDbContext
object may have a long lifetime and can be shared across different queries. However, it is not recommended since the underlying implementation of EF already performs optimization with connection pools and clients. In addition, if we pass an instance we can face several potential issues due to code that disposes it after its “local” use. In the case, for example, theGetOrder
method disposes theMyEntities
object after retrieving the data, no other member of the instance can use it anymore. Worse, it would be a runtime exception.
We can also take advantage of the “using” keyword to get a Dispose()
on the Entity Framework object:
using (var ctx = contextFactory())
{
return ctx.Orders
.FirstOrDefault(p => p.ID == id);
}
And we finally inject the factory function in the instantiating code:
var dal = new DataCaching(new DAL(()=>new MyEntities()));
DataCaching
is not compiling, since we added a member in the IDataService
interface without the proper implementation in the concrete class. By injecting the lambda which executes the actual factory code, we can also define a generalization step that wraps all the caching-related code stripping it away from the single method implementation, as follows:
private K GetOrAdd<K>(string key,Func<K> eval)
{
var cachedItem = cache.Get(key);
if (cachedItem != null) return (K)cachedItem;
else
{
var result = eval();
cache.Set(key, result,
new CacheItemPolicy()
{
SlidingExpiration = TimeSpan.FromSeconds(10)
});
return result;
}
}
This is a trivial implementation since, for instance, the CacheItemPolicy
is the same for every method call. But the advantages in the methods implementation are great:
public Order[] GetOrdersFor(int customer)
{
var key = $"GetOrdersFor_{customer}";
return GetOrAdd<Order[]>(key,
() => service.GetOrdersFor(customer));
}
public Order GetOrder(int id)
{
var key = $"GetOrder_{id}";
return GetOrAdd<Order>(key,
() => service.GetOrder(id));
}
We increased the features of the application (by adding the new method definitions plus its implementations) while, at the same time, reducing the overall code, using a bit of generalization.
Introducing a bit of reflection
We started from a prototype which tested the code flow and the technology. In the previous section, we see instead how to apply some experiments to working code, to reduce duplicated code mainly. We continue of such trend even further, with another intervention which can reduce again the code written. In this case, however, the approach introduces reflection, which is discussed later in other chapters.
Let’s say our IDataService
interface will have tens of methods and, for each one, we need to implement:
- The EF high-valued code (in the DAL class): every Entity Framework related method to materialize data will be unique and potentially complex. It is normal to implement it manually.
- The low-valued code of the DataCaching class: where every method is a trivial repetition (or, worse, a copy/paste) of the same steps.
To be precise, it is a replicated pattern of:
- Define a key:
- we can assume the key will always be a string with the pattern “[MethodName]_{p1}_{p2}….{pn}”.
- Call the
GetOrAdd
method:- we assume the cached type is the same as the return type of the method.
- Specify an evaluating function:
- we know the underlying method to call has the same signature as the running method.
With the assumptions above, we can write code which reflects the IDataService
interface to get information about methods and properties useful to prepare a generated implementation.
var methods = typeof(IDataService).GetMethods()
.Select(p => new
{
Name=p.Name,
ReturnType=p.ReturnType.FullName==
"System.Void"?"void":p.ReturnType.FullName,
Parameters=p.GetParameters()
.Select(q => new
{
q.ParameterType,
q.Name
})
.ToArray()
})
.ToArray();
Here a bit of explanation:
- The
typeof(IDataService)
returns aSystem.Type
object, containing useful information about it.System.Type
is the starting point to reflect almost everything. - The
GetMethods()
method returns all the public methods of theIDataService
type (and, generally, of anySystem.Type
object) - Since the
GetMethods()
method returns an array ofMethodInfo
, we can iterate on that through LINQ (lambda syntax) to have a projection ReturnType
is aSystem.Type
too:FullName
is good except in some cases (System.Void
is one). We used the conditional operator to fix that case.- For every method, we ask
GetParameters
and iterate to map them inName
andParameterType
. - The two
ToArray()
calls are needed to materialize the results immediately (the default behavior of the iterator is to lazily materialize result upon request).
The listing above will produce an array of anonymous objects with all the relevant information we need to produce the code dynamically, through a final generation step, in the next article.
Companion code here:
https://github.com/childotg/iSolutionsLabs