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

On this page

    Service Lifetimes: Transient

    A quick recap, because this lesson builds on the last two.

    • Singleton: the container builds a dependency once and hands that same instance to every class that asks. It was right for AppOptions, whose settings never change.
    • State: a class with member variables that change is stateful. Sharing one between customers merges their data, like the shopping cart in the last lesson. So a stateful class should never be a singleton.

    This lesson gives the fix for stateful classes. First, let us see the same problem again, this time inside a chain of dependencies.


    A new scenario: ReceiptBuilder

    Different corner of the bookshop this time. When a customer checks out, something has to build their receipt, line by line, as items are added.

    public class ReceiptBuilder
    {
        private readonly List<string> _lines = new();
    
        public void AddLine(string line) => _lines.Add(line);
    
        public void PrintReceipt()
        {
            foreach (var line in _lines)
            {
                Console.WriteLine(line);
            }
        }
    }
    

    Nothing complicated: AddLine grows the list, and PrintReceipt shows what is in it so far.

    Like the shopping cart, ReceiptBuilder is stateful: the _lines list changes every time AddLine runs. It starts empty, and its whole job is to change as it is used.


    Two customers, one shared notepad

    Two customers are checking out at the same time:

    • Customer A buys a novel.
    • Customer B buys a notebook.

    Each checkout is handled by a CheckoutService:

    public class CheckoutService
    {
        private readonly ReceiptBuilder _receiptBuilder;
    
        public CheckoutService(ReceiptBuilder receiptBuilder)
        {
            _receiptBuilder = receiptBuilder;
        }
    
        public void ProcessOrder(string item, int qty, decimal price)
        {
            _receiptBuilder.AddLine($"Item: {item}, Qty: {qty}, Total: {qty * price}");
            _receiptBuilder.PrintReceipt();
        }
    }
    

    CheckoutService does not build its own ReceiptBuilder. It asks for one through the constructor, as always.

    Now register ReceiptBuilder as a singleton anyway, to see the rule from the last lesson in action:

    services.AddSingleton<ReceiptBuilder>();
    services.AddTransient<CheckoutService>();
    

    (AddTransient is the opposite of singleton. We explain it properly in a moment. For now, it means CheckoutService is built fresh every time.)

    Simulate both customers, one right after the other:

    var customerA = provider.GetRequiredService<CheckoutService>();
    customerA.ProcessOrder("Novel", 1, 12.50m);
    
    Console.WriteLine("---");
    
    var customerB = provider.GetRequiredService<CheckoutService>();
    customerB.ProcessOrder("Notebook", 1, 4.00m);
    

    Output:

    Item: Novel, Qty: 1, Total: 12.50
    ---
    Item: Novel, Qty: 1, Total: 12.50
    Item: Notebook, Qty: 1, Total: 4.00
    

    That is the merged-cart bug again. Customer B's receipt has Customer A's novel on it. Two unrelated customers, and B is walking away with A's purchase on their receipt.

    Here is exactly why:

    • CheckoutService is transient, so yes, A and B each really did get their own brand-new CheckoutService.
    • But both of those asked for a ReceiptBuilder, and ReceiptBuilder is a singleton.
    • The container does not care that two different CheckoutServices are asking. Singleton means the same instance every time, whoever asks.
    Singleton ReceiptBuilder: one shared list ReceiptBuilder (singleton) one shared list of lines CheckoutService (A) CheckoutService (B) Both are new, but both write into the same list.

    A brand-new CheckoutService gave a false sense of safety. The state actually lived one level deeper, in the thing it depends on, and that was still shared.

    The rule this points to: if something holds state that belongs to one scenario (one customer, one request, one piece of work), and mixing that state between scenarios would be dangerous, then singleton is the wrong choice.


    The fix: ReceiptBuilder as transient

    services.AddTransient<ReceiptBuilder>();
    services.AddTransient<CheckoutService>();
    

    Same two customers, no other code changed:

    Item: Novel, Qty: 1, Total: 12.50
    ---
    Item: Notebook, Qty: 1, Total: 4.00
    

    Correct. Each customer gets their own CheckoutService and now their own ReceiptBuilder too, so nothing carries over between them.

    Transient ReceiptBuilder: a fresh one each time ReceiptBuilder (A) "Novel" ReceiptBuilder (B) "Notebook" CheckoutService (A) CheckoutService (B) A fresh notepad for every customer, all the way down.

    This is what transient means: the container builds a brand-new instance every single time something asks for one. Never reused, never shared. That is exactly right for anything holding state that must not leak between two separate pieces of work.


    Recap

    • Singleton and transient solve opposite problems. Singleton avoids waste for things that never change. Transient avoids leaking state for things that do.
    • A class's own lifetime does not protect it from a dependency registered differently. CheckoutService being transient did not save Customer B. The shared state was hiding one level down, in a singleton ReceiptBuilder.
    • So the real question is not "is this class transient or singleton?" on its own. It is: "does anything in this chain of dependencies hold state that would be dangerous to share between two scenarios?"
    Ask yourself Lifetime
    Does this hold settings that never change while the app runs? Singleton
    Does this hold state that must never leak between two separate pieces of work? Transient
    Should it be shared within one customer's request, but not between customers? Scoped (next lesson)

    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. Why was ReceiptBuilder a bad fit for singleton, when AppOptions was a good one?
    2. CheckoutService was transient, yet Customer B's receipt showed Customer A's novel. Explain why, in your own words.
    3. What does transient mean? What does the container do each time something asks for a transient class?
    4. A ShoppingBasket holds the items one customer has picked. A DiscountList holds fixed discount rates that are loaded once and never change. Which lifetime would you give each, and why?
    5. A transient class depends on a singleton that keeps a changing list inside it. Is that safe? Why or why not?

    There is a third lifetime we have not covered: scoped. It exists because singleton and transient both fall short in one very common situation: something that should be shared within one customer's request, but not reused across different customers. That is next.

    ← What Is State, and Why Is Sharing It Dangerous?Service Lifetimes: Scoped →