Domain Objects Caching Pattern for .NET...
By: iqbal@alachisoft.com
Date: April 25, 2006
Printer Friendly Version
Abstract
Caching greatly improves application performance because it reduces expensive trips to the database. But, if you
want to use caching in your application, you must decide what to cache and where to put your caching code. The
answer is simple. Cache your domain objects and put caching code inside your persistence classes.
Domain objects are central to any application and represent its core data and business validation rules. And,
while domain objects may keep some read-only data, most of the data is transactional and changes frequently.
Therefore, you cannot simply keep domain objects as "global variables" for the entirety of your
application because the data will change in the database and your domain objects will become stale, thereby
causing data integrity problems. You'll have to use a proper caching solution for this. And, your options are
ASP.NET Cache, Caching Application Block in Microsoft Enterprise Library, or some commercial solution like
NCache from Alachisoft. Personally, I would advise against using ASP.NET Cache since it forces you to cache from
presentation layer (ASP.NET pages) which is bad.
The best place to embed caching in your application is your domain objects persistence classes. In this article,
I am extending an earlier design pattern I wrote called
Domain Objects Persistence Pattern for
.NET. I am going to show you how you can incorporate intelligent caching into your application to boost its
performance and what considerations you should keep in mind while doing that.
Domain Objects Caching Pattern attempts to provide a solution for domain object caching. The domain objects in
this pattern are unaware of the classes that persist them or whether they're being cached or not, because the
dependency is only one-way. This makes the domain object design much simpler and easier to understand. It also
hides the caching and persistence code from other subsystems that are using the domain objects. This also works
in distributed systems where only the domain objects are passed around.
Scope
Domain Objects, Domain Objects Caching.
Problem Definition
Domain objects form the backbone of any application. They capture data model from the database and also the
business rules that apply to this data. It is very typical for most subsystems of an application to rely on these
common domain objects. And, usually applications spend most of their time in either loading or saving these domain
objects to the database. The actual "processing time" of these objects is very small specially for
N-Tier applications where each "user request" is very short.
This means that performance of the application depends greatly on how quickly these domain objects can be made
available to the application. If the application has to make numerous database trips, the performance is usually
bad. But, if the application caches these objects close-by, the performance improves greatly.
At the same time, it is very important that domain object caching code is kept in such a central place that no
matter who loads or saves the domain objects, the application automatically interacts with the cache.
Additionally, we must hide the caching code from the rest of application so we can take it out easily if needed.
Solution
As described above, the solution is an extension of an existing design pattern called Domain Objects Persistence
Pattern for .NET. That pattern already achieves the goal of separating domain objects from persistence code and
from the rest of the application as well. This double-decoupling provides a great deal of flexibility in the
design. The domain objects and the rest of the application is totally unaffected whether the data is coming from
a relational database or any other source (e.g. XML, flat files, or Active Directory/LDAP).
Therefore, the best place to embed caching code is in the persistence classes. This ensures that no matter which
part of the application issues the load or save call to domain objects, caching is appropriately referenced first.
This also hides all the caching code from rest of the application and lets you replace it with something else
should you choose to do so.
Domain and Persistence Classes
In this sample, we will look at an Employee class from Northwind database mapped to the "Employees"
table in the database.
// Domain object "Employee" that holds your data
public class Employee {
// Some of the private data members
// ...
public Employee() {}
// Properties for Employee object
public
String
EmployeeId { get
{return
_employeeId;} set
{_employeeId = value;}}
public
String Title {
get {return _title;}
set
{_title = value;}}
public
ArrayList
Subordinates { get
{return
_subordinates;} set
{_subordinates = value;}}
}
//
Interface for the Employee persistence
public
interface IEmployeeFactory
{
// Standard transactional methods
for single-row operations
void
Load(Employee
emp);
void
Insert(Employee
emp);
void
Update(Employee
emp);
void
Delete(Employee
emp);
// Load the related Employees (Subordinates) for this Employee
void
LoadSubordinates(Employee
emp);
// Query method to return a collection of Employee objects
ArrayList
FindByTitle(String
title);
}
// Implementation of Employee persistence
public class EmployeeFactory : IEmployeeFactory
{
// all methods described in interface above are implemented here }
//
A FactoryProvider to hide persistence implementation
public
class FactoryProvider
{
// To abstract away the actual factory
implementation
public
static IEmployeeFactory
GetEmployeeFactory() { return new
EmployeeFactory();
}
}
|
Sample Application
Below is an example of how a client application will use this code.
public class NorthwindApp
{
static
void Main (string[]
args) {
Employee
emp = new
Employee();
IEmployeeFactory
iEmpFactory = FactoryProvider.GetEmployeeFactory();
//
Let's load an employee from Northwind
database.
emp.EmployeeId
= 2;
iEmpFactory.load(emp);
//
Pass on the Employee object
HandleEmployee(emp);
HandleSubordinates(emp.Subordinates);
//
empList is a collection of Employee
objects
ArrayList
empList = iEmpFactory.FindByTitle("Manager");
}
}
|
The code above shows you the overall structure of your classes for handling domain objects persistence and
caching. As you can see, there is clear-cut separation between the domain and persistence classes. And, there is
an additional FactoryProvider class that lets you hide the persistence implementation from rest of the
application. However, the domain objects (Employee in this case) moves around throughout the application.
Creating Cache Keys
Most cache systems provide you with a string-based key. At the same time, the data that you cache consists of
various different classes ("Customers", "Employees", "Orders", etc.). In this
situation, an EmployeeId of 1000 may conflict with an OrderId of 1000 if you keys do not contain any
type information. Therefore, you need to store some type information as part of the key as well. Below are some
suggested key structures. You can make up your own based on the same principles.
- Keys for individual objects: If you're only storing individual objects, you can make up your keys as
following:
- "Customers:PK:1000". This means Customers object with primary key of 1000.
- Keys for related objects: For each individual object, you may also want to keep related objects so you
can easily find them. Here are keys for that:
- "Customers:PK:1000:REL:Orders". This means an Orders collection for Customer with primary key of
1000
- Keys for query results: Sometime, you run queries that return a collection of objects. And, these
queries may also take different run-time parameters each time. You want to store these query results so the next
time you don't have to run the query. Here are the keys for that. Please note that these keys also include
run-time parameter values:
- "Employees:QRY:FindByTitleAndAge:Manager:40". This represents a query in "Employees"
class called "FindByTitleAndAge" which takes two run-time parameters. The first parameter is
"Title" and second is "Age". And, their runtime parameter values are specified.
Caching in Transactional Operations
Most transactional data contains single-row operations (load, insert, update, and delete). These methods are all
based on primary key values of the object and are the ideal place to start putting caching code. Here is how to
handle each method:
- Load
Method: First check the cache. If
data found, get it from there. Otherwise,
load from the database and then put in
the cache.
- Insert
Method: After successfully adding
a row in the database, add its object
to the cache as well.
- Update
Method: After successfully updating
a row in the database, update its object
in the cache as well.
- Delete
Method: After successfully removing
a row from the database, remove its object
from the cache as well.
Below is a sample Load method with caching logic included. Remember, you're only loading a single object
(single row) from the database.
//
Check the cache before going to the
database
void
Load(Employee emp)
{
try
{
// Construct a cache-key to lookup
in the cache first
// The cache-key for the object will
be like this: Employees:PK:1000
string
objKey = CacheUtil.GetObjectKey("Employee",
emp.EmployeeId.ToString());
object
obj = CacheUtil.Load(objKey);
if (obj == null)
{
// item not
found in the cache. Load from database
and then store in the cache
_LoadFromDb(emp);
// For simplicity, let's assume this
object does not depend on anything
else
ArrayList
dependencyKeys =
null;
CacheItemRemovedCallback
onRemoved = null;
CacheUtil.Store(objKey,
emp, dependencyKeys, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
onRemoved );
//
Now, load all its related subordinates
LoadSubordinates(emp);
}
else
{
emp.Copy((Employee)obj);
}
}
catch
(Exception
e)
{
// Handle exceptions here
}
}
|
Please note a few things here.
- RemovedItemCallback:
This is a delegate that allows your application to be notified asynchronously when the given item is
removed from the cache.
- Expiration:
You can specify an absolute time or idle time expiration. Although, we did not specify any expiration
above, you could have specified two types of expirations. One is a fixed-time expiration (e.g. 10 minutes
from now) and the second is an idle-time expiration (e.g. if item is idle for 2 minutes).
Caching Relationships
Domain objects usually represent relational data coming from a relational database. Therefore, when you cache
them, you have to keep in mind their relationships and cache the related objects as well. And, you also have to
create "dependency" between the object and all its related objects. The reason being that if you remove
the object from the cache, you should also remove all its related objects so there is not data integrity problems.
Below is a code example of how to specify relationships in the cache.
// LoadSubordinates method
void
LoadSubordinates(Employee emp)
{
try
{
// Construct a cache-key to lookup
related items in the cache first
// The cache-key for related collection
will be like this: Employees:PK:1000:REL:Subordinates
string
relKey = CacheUtil.GetRelationKey("Employees",
"Subordinates",
emp.EmployeeId.ToString());
string
employeeKey =
CacheUtil.GetObjectKey("Employee",
emp.EmployeeId.ToString());
object
obj = CacheUtil.Load(relKey);
if (obj == null)
{
// Subordinates
not found in the cache. Load from
database and then store in the cache
_LoadSubordinatesFromDb(emp);
ArrayList
subordList = emp.Subordinates;
// Result is a collection of Employee.
Let's store each Employee separately
in
// the cache and then store the collection
also but with a dependency on all
the
// individual Employee objects. Then,
if any Employee is removed, the collection
will also be
// Count + 1 is so we can also put
a dependency on the Supervisor
ArrayList
dependencyKeys = new ArrayList(subordList.Count
+ 1);
for (int
index = 0; index , subordList.Count;
index++)
{
string
objKey=CacheUtil.GetObjectKey("Employee",subordList[index].EmployeeId.ToString());
CacheUtil.Store(objKey, subordList[index],
null, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
null );
dependencyKeys[index] = objKey;
}
dependencyKeys[subordList.Count] =
employeeKey;
CacheItemRemovedCallback
onRemoved = null;
CacheUtil.Store(relKey, subordinateList,
dependencyKeys, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
onRemoved );
}
else
{
// Subordinates already in the cache.
Let's get them
emp.Subordinates = (ArrayList)obj;
}
}
catch
(Exception
e)
{
// Handle exceptions here
}
}
|
In the above example, you'll notice that a collection is being returned from the database and each object inside
the collection is stored individually in the cache. Then, the collection is being cached as a single-item but with
a cache dependency on all the individual objects in the collection. This means that if any the individual objects
is updated or removed in the cache, the collection is automatically removed by the cache. This allows you to
maintain data integrity in caching collections.
You'll also notice in the above example that the collection has a cache dependency on the "primary
object" whose related objects the collection contains. This dependency also means that if the primary object
is removed or updated in the cache, the collection will be removed in order to maintain data integrity.
Caching in Query Methods
A query method returns a collection of objects based on the search criteria specified in it. It may or may not
take any runtime parameters. In our example, we have a FindByTitle that takes "title" as a parameter.
Below is an example of how caching is embedded inside a query method.
|
//
Query method to return a collection
ArrayList
FindByTitle(String
title)
{
try
{
// Construct a cache-key to lookup
items in the cache first
// The cache-key for the query will
be like this: Employees:PK:1000:QRY:FindByTitle:Manager
string
queryKey = CacheUtil.GetQueryKey("Employees",
"Query",
title);
object
obj = CacheUtil.Load(queryKey);
if (obj == null)
{
// No items
found in the cache. Load from database
and then store in the cache
ArrayList
empList = _FindByTitleFromDb(title);
//
Result is a collection of Employee.
Let's store each Employee separately
in
// the cache and then store the
collection also but with a dependency
on all the
// individual Employee objects.
Then, if any Employee is removed,
the collection will also be
ArrayList
dependencyKeys = new ArrayList(empList.Count);
for
(int
index = 0; index , empList.Count;
index++)
{
string
objKey = CacheUtil.GetObjectKey("Employee",
empList[index].EmployeeId.ToString());
CacheUtil.Store(objKey, empList[index],
null, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
null );
dependencyKeys[index] = objKey;
}
CacheItemRemovedCallback
onRemoved = null;
CacheUtil.Store(queryKey,
empList, dependencyKeys, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
onRemoved );
}
else
{
// Query results already in the
cache. Let's get them
return (ArrayList)
obj;
}
}
catch
(Exception
e)
{
// Handle exceptions here
}
}
|
In the above example, just like the relationship method, you'll notice that a collection is being returned from
the database and each object inside the collection is stored individually in the cache. Then, the collection is
being cached as a single-item but with a cache dependency on all the individual objects in the collection. This
means that if any the individual objects is updated or removed in the cache, the collection is automatically
removed by the cache. This allows you to maintain data integrity in caching collections.
Applications in Server Farms
The above pattern works for both single-server or server-farm deployment environments. The only thing that must
change is the underlying caching solution. Most caching solutions are for single-server environments (e.g.
ASP.NET Cache and Caching Application Block). But, there are some commercial solutions like Alachisoft NCache
(http://www.alachisoft.com) that provide you a distributed cache that
works in a server farm configuration. This way, your application can use a cache from any server in the farm and
all cache updates are immediately propagated to the entire server farm.
Conclusion
Using the Domain Objects Caching Pattern, we have demonstrated how you should embed caching code into your
persistence classes. And, we've covered the most commonly used situations of Load, Queries, and Relationships
with respect to caching. This should give you a good starting point to determine how you should use caching in
your application.
Author: Iqbal M. Khan works for Alachisoft, a leading software company providing O/R Mapping and Clustered
Object Caching solutions for .NET. You can reach him at
iqbal@alachisoft.com or visit Alachisoft at www.alachisoft.com.