Tutorials / Build a REST API with C# and .NET · Lesson 5 of 15

On this page

    Service Lifetimes: Singleton

    In the last lesson you learned the core idea of Dependency Injection: a class should not choose its own implementation. It should be handed one, and the decision should live in one place.

    This lesson takes the same idea one step further. We start with a new version of the same bad habit: same root problem, different shape.


    The bad way: every class loads its own settings

    Say two classes both need some app settings, such as a database connection string. With nothing shared, each one loads the settings itself:

    public class OrderProcessor
    {
        public void Process()
        {
            // Pretend this is expensive: read a file, parse it, maybe call a remote service
            Console.WriteLine("Loading configuration... (pretend this is expensive)");
            var connectionString = "Server=localhost;Database=BookShop;";
    
            Console.WriteLine($"Processing orders using: {connectionString}");
        }
    }
    
    public class ReportGenerator
    {
        public void GenerateReport()
        {
            // Pretend this is expensive: read a file, parse it, maybe call a remote service
            Console.WriteLine("Loading configuration... (pretend this is expensive)");
            var connectionString = "Server=localhost;Database=BookShop;";
    
            Console.WriteLine($"Generating report using: {connectionString}");
        }
    }
    

    That is not two different pieces of logic. It is the same lines typed twice in two files. Programmers call this breaking DRY, which stands for "Don't Repeat Yourself". And it is not just untidy. It costs something real, twice over:

    • Time. The expensive step runs again from scratch for every class that needs it.
    • Memory. Each class holds its own copy of the same connection string.

    Two classes here, but nothing stops it from growing to ten or twenty, each paying the same cost separately.

    It is the same pattern as the dice games from the last lesson, which each wrote new ClockNumberGenerator() themselves. Only now it is loading settings instead of making random numbers.


    The fix, step one: put it in one class

    The usual fix: do not repeat the logic. Write it once, and let everything else point back to that one place.

    public class AppOptions
    {
        public Guid InstanceId { get; } = Guid.NewGuid();
        public string ConnectionString { get; }
    
        public AppOptions()
        {
            // Pretend this is expensive: read a file, parse it, maybe call a remote service
            Console.WriteLine("Loading configuration... (pretend this is expensive)");
            ConnectionString = "Server=localhost;Database=BookShop;";
        }
    }
    

    InstanceId is not a real setting. It is a trick so we can see whether two classes ended up with the same AppOptions object or two different ones.

    Now OrderProcessor and ReportGenerator load nothing themselves. They ask for an AppOptions through the constructor, the same pattern as INumberGenerator in the last lesson:

    public class OrderProcessor
    {
        private readonly AppOptions _options;
    
        public OrderProcessor(AppOptions options)
        {
            _options = options;
        }
    
        public void Process()
        {
            Console.WriteLine($"OrderProcessor using AppOptions {_options.InstanceId}");
        }
    }
    
    public class ReportGenerator
    {
        private readonly AppOptions _options;
    
        public ReportGenerator(AppOptions options)
        {
            _options = options;
        }
    
        public void GenerateReport()
        {
            Console.WriteLine($"ReportGenerator using AppOptions {_options.InstanceId}");
        }
    }
    

    The repeated code is gone, so DRY is satisfied. But is the problem fully solved? Not quite:

    • DRY fixed the code: the logic is no longer typed twice.
    • But nothing says how many times that logic actually runs. If each class ends up with its own fresh AppOptions, we have tidied the code but still pay the cost twice.

    Tidying the code is not the same as removing the waste. That is the next step.


    The fix, step two: build it once and share it

    Ask the natural question: how many times does this app really need to build AppOptions?

    The connection string does not change while the app runs. So if five classes need it, there is no reason to pay the "expensive load" five times. Once is enough: one AppOptions, shared by everyone who needs it.

    In the last lesson you wired things up by hand, one new per class. But "build this once and hand the same one to everyone who asks" is not something a single new can say. You would have to write the sharing logic yourself, every time.

    That is the job of the DI container. It is a piece of .NET that does the wiring for you. Instead of writing new calls, you tell the container the rule you want, and it builds things by that rule whenever something asks.

    To try it, add the container to your console project:

    dotnet add package Microsoft.Extensions.DependencyInjection
    

    Then use it in Program.cs:

    using Microsoft.Extensions.DependencyInjection;
    
    var services = new ServiceCollection();
    
    services.AddSingleton<AppOptions>();
    services.AddSingleton<OrderProcessor>();
    services.AddSingleton<ReportGenerator>();
    
    using var provider = services.BuildServiceProvider();
    
    var orderProcessor = provider.GetRequiredService<OrderProcessor>();
    var reportGenerator = provider.GetRequiredService<ReportGenerator>();
    
    orderProcessor.Process();
    reportGenerator.GenerateReport();
    

    The first part is the registration: we tell the container which things exist and how to build them. The second part asks the container for finished objects. The container looks at each constructor, sees what it needs, and builds those too.

    AddSingleton is the rule here. A singleton is built once, and that same instance is handed to everyone who asks, for the entire life of the app.

    The "how often does the container build a fresh one?" question has a name: the lifetime. Singleton is one answer. .NET has two others, transient and scoped, which we meet in the next lessons.

    Proving it. Run the program and you will see:

    Loading configuration... (pretend this is expensive)
    OrderProcessor using AppOptions 4f9c1a2e-...
    ReportGenerator using AppOptions 4f9c1a2e-...
    
    • "Loading configuration..." printed once.
    • Both classes printed the same InstanceId.

    Two classes that know nothing about each other were handed the identical AppOptions, because the container built it only once.

    Singleton: one instance, shared AppOptions built once · id 4f9c1a2e… OrderProcessor ReportGenerator the same instance is handed to both

    Now break it on purpose. Change AddSingleton<AppOptions>() to AddTransient<AppOptions>() and run again. "Loading configuration..." now prints twice, and the two InstanceIds are different: a fresh instance, and a fresh "expensive load", every time something asks.

    Transient: a new instance each time AppOptions id aaa… AppOptions id bbb… OrderProcessor ReportGenerator two instances, two "expensive loads"

    We look at when that is the right choice in the next lesson. For now, notice that the behaviour changes visibly with a single word in one registration line.

    For settings that never change while the app runs, singleton is the right choice: one build, shared everywhere, with no wasted time or memory.


    Recap

    • Repeated loading logic across classes breaks DRY, and it costs real time and memory, not just tidiness.
    • Moving the logic into one class (AppOptions) fixes the duplicated code, but not how many times it runs.
    • The DI container closes that gap. AddSingleton tells it to build once and hand the same instance to every consumer, for the life of the app.
    • Singleton is the right choice for things that hold values that never change once built, like configuration.
    Ask yourself Lifetime
    Does this hold settings that never change while the app runs? Singleton

    Questions to think about

    Before the next lesson, try answering these in your own words. There are no answers given here on purpose. If you can explain them out loud to a friend, you are ready to move on.

    1. What does DRY mean? Besides being untidy, what does it cost when two classes each load the same settings?
    2. Moving the loading code into AppOptions fixed the repeated code. What was still left unfixed?
    3. What is a singleton? What can the DI container do for you that a single new cannot?
    4. A ShopInfo class holds the shop's name and opening hours. They are read once at start-up and never change. Which lifetime would you choose, and why?
    5. You change AddSingleton<AppOptions>() to AddTransient<AppOptions>() and run the program again. What would you see, and why?

    Next lesson: singleton works because AppOptions never changes. What happens when the thing being shared does change? That idea is called state, and it decides which lifetime you can safely pick.

    ← Dependency Injection BasicsWhat Is State, and Why Is Sharing It Dangerous? →