C# – Alternatives to Singletons for caching lists of data

cdesign-patternsobject-orientedsingleton

In my project, I have an abstract Cache class that allows me to populate a series of lists that globally persist throughout my application. These cache objects are thread-safe and can be manipulated as necessary, and allow for me to cut-down on the massive overhead of querying external third-party APIs directly. I've seen some serious hate for singletons, so I'm a bit curious what other options I have when this is my current use case.

I've seen dependency injection mentioned quite a bit, but I don't know if it's quite adequate or useful in this scenario.

Here is an example of my Cache abstract class:

public abstract class Cache<TU, T>
    where TU : Cache<TU, T>, new()
    where T : class
{
    private static readonly TU Instance = new TU();
    private static volatile State _currentState = State.Empty;
    private static volatile object _stateLock = new object();
    private static volatile object _dataLock = new object();
    private static DateTime _refreshedOn = DateTime.MinValue;
    private static T InMemoryData { get; set; }

    public static T Data
    {
        get
        {
            switch (_currentState)
            {
                case State.OnLine:
                    var timeSpentInCache = (DateTime.UtcNow - _refreshedOn);
                    if (timeSpentInCache > Instance.GetLifetime())
                    {
                        lock (_stateLock)
                        {
                            if (_currentState == State.OnLine) _currentState = State.Expired;
                        }
                    }
                    break;

                case State.Empty:
                    lock (_dataLock)
                    {
                        lock (_stateLock)
                        {
                            if (_currentState == State.Empty)
                            {
                                InMemoryData = Instance.GetData();
                                _refreshedOn = DateTime.UtcNow;
                                _currentState = State.OnLine;
                            }
                        }
                    }
                    break;

                case State.Expired:
                    lock (_stateLock)
                    {
                        if (_currentState == State.Expired)
                        {
                            _currentState = State.Refreshing;
                            Task.Factory.StartNew(Refresh);
                        }
                    }
                    break;
            }

            lock (_dataLock)
            {
                if (InMemoryData != null) return InMemoryData;
            }

            return Data;
        }
    }

    public static T PopulateData()
    {
        return Data;
    }

    protected abstract T GetData();

    protected virtual TimeSpan GetLifetime()
    {
        return TimeSpan.FromMinutes(10);
    }

    private static void Refresh()
    {
        if (_currentState != State.Refreshing) return;
        var dt = Instance.GetData();
        lock (_stateLock)
        {
            lock (_dataLock)
            {
                _refreshedOn = DateTime.UtcNow;
                _currentState = State.OnLine;
                InMemoryData = dt;
            }
        }
    }

    public static void Invalidate()
    {
        lock (_stateLock)
        {
            _refreshedOn = DateTime.MinValue;
            _currentState = State.Expired;
        }
    }

    private enum State
    {
        Empty,
        OnLine,
        Expired,
        Refreshing
    }
}

And an example of its implementation.

public class SalesForceCache
{
    public class Users : Cache<Users, List<Contact>>
    {
        protected override List<Contact> GetData()
        {
            var sf = new SalesForce();
            var users = sf.GetAllUsers();

            sf.Dispose();

            return users;
        }

        protected override TimeSpan GetLifetime()
        {
            try
            {
                return TimeSpan.FromDays(1);
            }
            catch (StackOverflowException)
            {
                return TimeSpan.Zero;
            }
        }
    }

    public class Accounts : Cache<Accounts, List<Account>>
    {
        protected override List<Account> GetData()
        {
            var sf = new SalesForce();
            var accounts = sf.GetAllAccounts();

            sf.Dispose();

            return accounts;
        }

        protected override TimeSpan GetLifetime()
        {
            try
            {
                return TimeSpan.FromDays(1);
            }
            catch (StackOverflowException)
            {
                return TimeSpan.Zero;
            }
        }
    }
}

Best Answer

  1. You can create one cache instance per third-party API instance. If your third-party APIs do not come in instances, then encapsulate them into some instantiatable C# wrapper.

  2. You can make your caches members of your one great big root Application object. If you do not have one, come up with one. This is the only thing which may legitimately be a singleton. (But even then, it should only be a singleton in the sense that it will be newed once, not in the sense of consisting of static methods.)