For performance efficiency, you may wish to cache an object within another object. That sounds easy enough to do. But what if the object cannot be found? You have to take special care to ensure the object is not fetched repeatedly.

An Example

As always, the best way to demonstrate this is with an example. Consider a “Company” object that has a GetEmployee method which retrieves an Employee object from a remote database, an operation that could take a few seconds. There is also a CEO property that returns the Employee object representing the company’s CEO:

public class Company
{
    public Employee CEO
    {
        get
        {
            // ...
        }
    }
    public Employee GetEmployee( Roles role )
    {
        // ...
    }
}

Cached Object

To improve performance, let’s say we want to cache the Employee object returned by the CEO property so that it can be accessed repeatedly with no delay after the first access. The easiest way to do this is simply include a private member that holds the CEO object once it has been loaded:

private Employee m_CEO;
public Employee CEO
{
    get
    {
        if (this.m_CEO == null)
            this.m_CEO = this.GetEmployee( Roles.CEO );
        return this.m_CEO;
    }
}

So far so good. But what if the CEO object cannot be found? As written above, the CEO property will call the GetEmployee method every time, resulting in a significant performance penalty.

Improved Cache

The solution is to create a blank Employee object and save it in a static member. As shown below, the Employee object stored in the s_EmployeeNotFound static member will serve as a “flag” to indicate that we’ve already attempted to get the CEO object but failed, hence do not attempt to get it again. If the CEO object could not be retrieved, instead of leaving the cached m_CEO reference as null, we set it equal to the s_EmployeeNotFound reference to indicate that we’ve already tried to load the CEO, so don’t bother doing it again.

public class Company
{
    static private Employee s_EmployeeNotFound = new Employee();
    private Employee m_CEO;
    public Employee CEO
    {
        get
        {
            // get the cached CEO
            Employee ceo = this.m_CEO;
            // if no CEO is cached
            if (ceo == null)
            {
                // try to load the CEO
                ceo = this.GetEmployee( Roles.CEO );
                // if could not load the CEO
                if (ceo == null)
                {
                    // indicate the CEO was not found
                    ceo = s_EmployeeNotFound;
                }
                // cache the new CEO
                this.m_CEO = ceo;
            }
            // if CEO was not found
            if (Object.ReferenceEquals( ceo, s_EmployeeNotFound ) )
            {
                // indicate there is no CEO to load
                ceo = null;
            }
            return ceo;
        }
    }

Note the use of Object.ReferenceEquals to compare objects. This ensures that we are truly checking for our special s_EmployeeNotFound object and prevents any accidental equality from an overridden Equals method in the Employee object.

Console Test Program

Using this example, here is a console test program that demonstrates how an object cache can really save time on multiple accesses. For simplicity, the GetEmployee method simply sleeps for 2 seconds to simulate a delay loading the object from a remote database.

using System;

namespace CachedObject
{
    class Program
    {
        static void Main( string[] args )
        {
            Company company = new Company();
            DateTime time1 = DateTime.Now;
            Console.WriteLine( "Accessing CEO for the first time..." );
            Employee ceo = company.CEO;
            DateTime time2 = DateTime.Now;
            Console.WriteLine( "Accessing cached CEO...n" );
            ceo = company.CEO;
            DateTime time3 = DateTime.Now;
            Console.WriteLine( "Time accessing CEO object:nNotCached...{0}nCached......{1}",
                time2.Subtract( time1 ), time3.Subtract( time2 ) );
            Console.ReadLine();
        }
        public class Company
        {
            static private Employee s_EmployeeNotFound = new Employee();
            private Employee m_CEO;
            public Employee CEO
            {
                get
                {
                    // get the cached CEO
                    Employee ceo = this.m_CEO;
                    // if no CEO is cached
                    if (ceo == null)
                    {
                        // try to load the CEO
                        ceo = this.GetEmployee( Roles.CEO );
                        // if could not load the CEO
                        if (ceo == null)
                        {
                            // indicate the CEO was not found
                            ceo = s_EmployeeNotFound;
                        }
                        // cache the new CEO
                        this.m_CEO = ceo;
                    }
                    // if CEO was not found
                    if (ceo == s_EmployeeNotFound)
                    {
                        // indicate there is no CEO to load
                        ceo = null;
                    }
                    return ceo;
                }
            }
            public Employee GetEmployee( Roles role )
            {
                // simulates a delay due to remote database access, for example
                System.Threading.Thread.Sleep( 2000 );
                // would return the employee if found; for this test, assume fail
                return null;
            }
        }
        public class Employee
        {
            public string Name;
            public int ID;
            public Roles Role;
            public DateTime BirthDate;
            public DateTime HireDate;
        }
        public enum Roles
        {
            CEO,
            Management,
            Sales,
            Marketing,
            Development,
            Staff,
        }
    }
}

Test Program Output

And here is the output to the test program. Notice how the first access of the CEO property took two seconds, but the second and all subsequent accesses will take no time at all, even if the CEO object could not be retrieved.

Accessing CEO for the first time…
Accessing cached CEO…
Time accessing CEO object:
NotCached…00:00:02.0156250
Cached……00:00:00