The Singleton is a creational design pattern that allows us to create a single instance of an object and to share that instance with all the users that require it. There is a common opinion that the Singleton pattern is not recommended because it presents a code smell, but there are some cases where it fits perfectly.
For example, some components have no reason to be instanced more than once in a project. Take a logger for example. It is quite common to register logger class as a singleton component because all we have to do is to provide a string to be logged and the logger is going to write it to the file. Then multiple classes may require to write in the same file at the same time from different threads, so having one centralized place for that purpose is always a good solution.
If you want to see a logger in action in ASP.NET Core Web API, you can read this article: ASP.NET Core – Logging with NLog.
Or maybe sometimes we have a task to read some data from a file and use them through our project. If we know for sure that file won’t change while we read it, we can create a single instance of the object which will read that file and share it through the project to the consumer classes.
In this article, we are going to show you how to properly implement a Singleton pattern in your project. When we say properly we mean that our singleton class is going to be a thread-safe which is a crucial requirement when implementing a Singleton pattern.
So, let’s begin.
The source code is available at the Singleton Design Pattern – Source Code.
For the complete list of articles from this series check out C# Design Patterns.
Initial Project
We are going to start with a simple console application in which we are going to read all the data from a file (which consists of cities with their population) and then use that data. So, to start off, let’s create a single interface:
public interface ISingletonContainer { int GetPopulation(string name); }
After that, we have to create a class to implement the ISingletonContainer interface. We are going to call it SingletonDataContainer:
public class SingletonDataContainer: ISingletonContainer { private Dictionary<string, int> _capitals = new Dictionary<string, int>(); public SingletonDataContainer() { Console.WriteLine("Initializing singleton object"); var elements = File.ReadAllLines("capitals.txt"); for (int i = 0; i < elements.Length; i+=2) { _capitals.Add(elements[i], int.Parse(elements[i + 1])); } } public int GetPopulation(string name) { return _capitals[name]; } }
So, we have a dictionary in which we store the capital names and their population from our file. As we can see, we are reading from a file in our constructor. And that is all good. Now we are ready to use this class in any consumer by simply instantiating it. But is this really what we need to do, to instantiate the class which reads from a file which never changes (in this particular project. Population of the cities is changing daily). Of course not, so obviously using a Singleton pattern would be very useful here.
So, let’s implement it.
Singleton Implementation
To implement the Singleton pattern, let’s change the SingletonDataContainer
class:
public class SingletonDataContainer: ISingletonContainer { private Dictionary<string, int> _capitals = new Dictionary<string, int>(); private SingletonDataContainer() { Console.WriteLine("Initializing singleton object"); var elements = File.ReadAllLines("capitals.txt"); for (int i = 0; i < elements.Length; i+=2) { _capitals.Add(elements[i], int.Parse(elements[i + 1])); } } public int GetPopulation(string name) { return _capitals[name]; } private static SingletonDataContainer instance = new SingletonDataContainer(); public static SingletonDataContainer Instance => instance; }
So, what we’ve done here is that we hid our constructor from the consumer classes by making it private. Then, we’ve created a single instance of our class and exposed it through the Instance property.
At this point, we can call the Instance property as many times as we want, but our object is going to be instantiated only once and shared for every other call.
Let’s check that theory:
class Program { static void Main(string[] args) { var db = SingletonDataContainer.Instance; var db2 = SingletonDataContainer.Instance; var db3 = SingletonDataContainer.Instance; var db4 = SingletonDataContainer.Instance; } }
If we start our app, we are going to see this:
We can see that we are calling our instance four times but it is initialized only once, which is exactly what we want.
But our implementation is not ideal. Let’s construct our object the lazy way.
Implementing a Thread-Safe Singleton
Let’s modify our class to implement a thread-safe Singleton by using the Lazy
type:
public class SingletonDataContainer : ISingletonContainer { private Dictionary<string, int> _capitals = new Dictionary<string, int>(); private SingletonDataContainer() { Console.WriteLine("Initializing singleton object"); var elements = File.ReadAllLines("capitals.txt"); for (int i = 0; i < elements.Length; i+=2) { _capitals.Add(elements[i], int.Parse(elements[i + 1])); } } public int GetPopulation(string name) { return _capitals[name]; } private static Lazy<SingletonDataContainer> instance = new Lazy<SingletonDataContainer>(() => new SingletonDataContainer()); public static SingletonDataContainer Instance => instance.Value; }
Right now, our class is completely thread-safe. It is loaded in a lazy way which means that our instance is going to be created only when it is actually needed. We can even check if our object is created with the IsValueCreated
property if we need to.
Excellent, we have finished our Singleton implementation.
Now we can fully consume it in our consumer class:
class Program { static void Main(string[] args) { var db = SingletonDataContainer.Instance; Console.WriteLine(db.GetPopulation("Washington, D.C.")); var db2 = SingletonDataContainer.Instance; Console.WriteLine(db2.GetPopulation("London")); } }
Great job.
Conclusion
We have seen that even though the Singleton pattern isn’t that much appreciated, it can be helpful in some cases. So, it is always up to us to decide whether we are going to use it or not. Bottom line is that if we need to apply a Singleton pattern in our project, this is a good way to do it.