Cookie Consent by Free Privacy Policy Generator Distributed Caching and Domain Objects Caching Pattern for .NET - NCache

Domain Objects Caching Pattern for .NET

Author: Iqbal Khan

Abstract

Caching significantly improves application performance by reducing the need for expensive database trips. 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 to cache your domain objects and put caching code inside your persistence classes. Domain objects are central to any application, representing its core data and business validation rules. while domain objects may keep some read-only data, most of the data is transactional and changes frequently. Therefore, you cannot keep domain objects as "global variables" throughout your application, because the data in the database may change over time. This can cause those objects to become stale, leading to data integrity issues. 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 the 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 the 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 the 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, especially for N-Tier applications where each "user request" is very short.

This means that the 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, the caching logic for domain objects is centralized in one place. This ensures that whenever domain objects are loaded or saved, the application automatically interacts with the cache. Additionally, we must hide the caching code from the rest of the 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 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 are 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 approach also encapsulates all caching logic, keeping it hidden from the rest of the application and making it easy to replace or modify if needed.

Domain and Persistence Classes

In this sample, we will look at an Employee class from the Northwind database mapped to the "Employees" table in the database.

// Domain object "Employee" that holds your data
public sealed class Employee
{
    public int EmployeeId { get; set; }
    public string? Title { get; set; }
    public List<Employee> Subordinates { get; set; } = new();

    // helpful when copying from a cached instance
    public void CopyFrom(Employee other)
    {
        EmployeeId = other.EmployeeId;
        Title = other.Title;
        Subordinates = other.Subordinates;
    }
}

// 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
    List<Employee> FindByTitle(string title);
}

// Implementation of Employee persistence
public sealed class EmployeeFactory : IEmployeeFactory
{
    // All methods described in the article are implemented here
    // (see later sections for Load, LoadSubordinates, FindByTitle)
    public void Load(Employee emp)               => throw new NotImplementedException();
    public void Insert(Employee emp)             => throw new NotImplementedException();
    public void Update(Employee emp)             => throw new NotImplementedException();
    public void Delete(Employee emp)             => throw new NotImplementedException();
    public void LoadSubordinates(Employee emp)   => throw new NotImplementedException();
    public List<Employee> FindByTitle(string t)  => throw new NotImplementedException();
}

// A FactoryProvider to hide persistence implementation
public static class FactoryProvider
{
    // To abstract away the actual factory implementation
    public static IEmployeeFactory GetEmployeeFactory() => new EmployeeFactory();
}

Sample Application

Below is an example of how a client application will use this code.

public sealed class NorthwindApp
{
    static void Main(string[] args)
    {
        var 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
        List<Employee> empList = iEmpFactory.FindByTitle("Manager");
    }

    static void HandleEmployee(Employee emp) { /* ... */ }
    static void HandleSubordinates(List<Employee> subs) { /* ... */ }
}

The code above shows you the overall structure of your classes for handling domain object persistence and caching. As you can see, there is a clear-cut separation between the domain and persistence classes. And, there is an additional FactoryProvider class that lets you hide the persistence implementation from the rest of the application. However, the domain objects (Employee in this case) move 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 classes ("Customers", "Employees", "Orders", etc.). In this situation, an EmployeeId of 1000 may conflict with an OrderId of 1000 if your keys do not contain any type information. Therefore, you need to store some type of 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.

  1. Keys for individual objects: If you're only storing individual objects, you can make up your keys as follows:
    1. "Customers:PK:1000": This means an object of class Customers with a primary key of 1000.
  2. Keys for related objects: For each object, you may also want to keep related objects so you can easily find them. Here are the keys for that:
    1. "Customers:PK:1000:REL:Orders": This means an Orders collection for Customer with a primary key of 1000.
  3. Keys for query results: Sometimes, 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:
    1. "Employees:QRY:FindByTitleAndAge:Manager:40": This represents a query in the "Employees" class called "FindByTitleAndAge" which takes two run-time parameters. The first parameter is "Title" and the 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:

  1. Load Method: First, check the cache. If data is found, get it from there. Otherwise, load from the database and then put it in the cache.
  2. Insert Method: If you insert a row in the database, insert the corresponding object in the cache as well.
  3. Update Method: If you update a row in the database, update the corresponding object in the cache as well.
  4. Delete Method: If you delete a row from the database, delete the corresponding 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
public void Load(Employee emp)
{
    try
    { 
        // The cache key for the object will be like: Employees:PK:1000
        var objKey = CacheUtil.GetObjectKey("Employees", emp.EmployeeId.ToString());

        var cached = CacheUtil.Load<Employee>(objKey);
        if (cached is null)
        {
            // Item not found in the cache. Load from database and then store in the cache.
            _LoadFromDb(emp); // your DB call should populate 'emp'

            // For simplicity, assume this object does not depend on anything else
            List<string>? dependencyKeys = null;
            Action<string>? onRemoved = null;

            CacheUtil.Store(objKey, emp, dependencyKeys, onRemoved: onRemoved);

            // Now, load all its related subordinates
            LoadSubordinates(emp);
        }
        else
        {
            emp.CopyFrom(cached);
        }
    }
    catch (Exception)
    {
        // Handle exceptions here
    }
}

Please note a few things here.

  1. RemovedItemCallback: This is a delegate that allows your application to be notified asynchronously when the given item is removed from the cache.
  2. Expiration: You can specify an absolute time or an 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 the 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 a "dependency" between the object and all its related objects. The reason is that if you remove the object from the cache, you should also remove all its related objects, so there are no data integrity problems. Below is a code example of how to specify relationships in the cache.

// LoadSubordinates method (stores each subordinate individually, then the collection with dependencies)
public void LoadSubordinates(Employee emp)
{
    try
    {
        // 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("Employees", emp.EmployeeId.ToString());

        var cached = CacheUtil.Load<List<Employee>>(relKey);
        if (cached is null)
        {
            // Subordinates not found in the cache. Load from database and then store in the cache.
            _LoadSubordinatesFromDb(emp); // should populate emp.Subordinates (List<Employee>)
            List<Employee> subordList = emp.Subordinates;

            // Store each Employee separately in the cache and then store the collection
            // with a dependency on all individual Employee objects + the supervisor
            var dependencyKeys = new List<string>(subordList.Count + 1);

            foreach (var s in subordList)
            {
                string objKey = CacheUtil.GetObjectKey("Employees", s.EmployeeId.ToString());
                CacheUtil.Store(objKey, s, dependencyKeys: null);
                dependencyKeys.Add(objKey);
            }

            // Also depend on the supervisor
            dependencyKeys.Add(employeeKey);

            Action<string>? onRemoved = null;
            CacheUtil.Store(relKey, subordList, dependencyKeys, onRemoved: onRemoved);
        }
        else
        {
            // Subordinates already in the cache. Let's get them.
            emp.Subordinates = cached;
        }
    }
    catch (Exception)
    {
        // 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 of the individual objects are 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." This dependency also means that if the primary object is removed or updated in the cache, the collection will be removed 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 (cached by query key)
public List<Employee> FindByTitle(string title)
{
    try
    {
        // The cache-key for the query will be like this:
        // Employees:QRY:FindByTitle:Manager
        string queryKey = CacheUtil.GetQueryKey("Employees", "FindByTitle", title);

        var cached = CacheUtil.Load<List<Employee>>(queryKey);
        if (cached is null)
        {
            // No items found in the cache. Load from database and then store in the cache.
            List<Employee> empList = _FindByTitleFromDb(title);

            // Store each Employee separately and then the collection
            // with a dependency on all the individual Employee objects
            var dependencyKeys = new List<string>(empList.Count);

            foreach (var e in empList)
            {
                string objKey = CacheUtil.GetObjectKey("Employees", e.EmployeeId.ToString());
                CacheUtil.Store(objKey, e, dependencyKeys: null);
                dependencyKeys.Add(objKey);
            }

            Action<string>? onRemoved = null;
            CacheUtil.Store(queryKey, empList, dependencyKeys, onRemoved: onRemoved);

            return empList;
        }
        else
        {
            // Query results already in the cache. Let's get them.
            return cached;
        }
    }
    catch (Exception)
    {
        // Handle exceptions here
        return new List<Employee>();
    }
}

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 of the individual objects are 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 and 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 that provide you with 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 Khan works for Alachisoft, a leading software company providing .NET and Java distributed caching, O/R Mapping, and SharePoint Storage Optimization solutions. You can reach him at iqbal@alachisoft.com.

© Copyright Alachisoft 2002 - . All rights reserved. NCache is a registered trademark of Diyatech Corp.