What's the difference between singleton, scoped, and transient?

It's trivial to register a dependency in a .NET API, but it's important to clarify a few terms that drastically change a dependency's lifetime.

What's the difference between singleton, scoped, and transient?
Photo by Agê Barros / Unsplash

I saw an issue with a .NET 6 API recently, where DI was in full use, but someone realized one of the dependencies wasn't behaving as expected. Several different areas of the API were requesting a new instance of something, but didn't seem to be getting what they asked for. The problem didn't actually present itself that nicely so it took awhile to track down, but in the end it was obvious (as most solved problems are) that everyone was getting the same instance of a certain resource instead of different instances.

When you create an API in .NET, it's pretty easy right from the get-go to define which concrete classes you should get when requesting various interfaces. That's supported right out of the box. They provide a few different methods for doing that though, so it's important to understand the differences between AddSingleton, AddScoped, and AddTransient.

To (really briefly) summarize them:

  • Singleton - One instance of a resource, reused anytime it's requested.
  • Scoped - One instance of a resource, but only for the current request. New request (i.e. hit an API endpoint again) = new instance
  • Transient - A different instance of a resource, everytime it's requested.

It's usually easier to see things in action though, which (as it turns out) is pretty easy to do. Here's a class with a single property that provides a GUID value, which is generated when the class is instantiated, and some interfaces to use in the next step.

public interface IIDSingleton : IID { }
public interface IIDScoped : IID { }
public interface IIDTransient : IID { }

public interface IID
{
    Guid Value { get; }
}

public class ID : IIDSingleton, IIDScoped, IIDTransient
{
    public Guid Value { get; private set; } = Guid.NewGuid();
}

And here's a minimal API with a single endpoint that defines some dependencies to inject, and (more importantly) how those dependencies should be resolved. When someone requests an IIDSingleton for example, it should resolve to a single instance of the ID class.. always just that single instance, no matter what. When someone requests an IIDTransient though, it should always be a new instance.

using SingletonVsTransientDI;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IIDSingleton>(new ID());
builder.Services.AddScoped<IIDScoped, ID>();
builder.Services.AddTransient<IIDTransient, ID>();

var app = builder.Build();

app.MapGet("/now", (IIDSingleton idSingleton,
                    IIDScoped idScoped1, IIDScoped idScoped2,
                    IIDTransient idTransient1, IIDTransient idTransient2) =>
{
    return $"Singleton instance: {idSingleton.Value}\r\n\r\n" +
        $"Scoped instance 1: {idScoped1.Value}\r\nScoped instance 2: {idScoped2.Value}\r\n\r\n" +
        $"Transient instance 1: {idTransient1.Value}\r\nTransient instance 2: {idTransient2.Value}";
});

app.Run();

The way I'm requesting two IIDScoped and IIDTransient dependencies above is silly, but it's to keep the example simple. In reality, you'd request different dependencies that (perhaps several levels deep) each happen to make their own requests for some other (same) dependency. Whether or not their independent requests provided them the same instance or not would depend on how those resources were registered (as a singleton, transient, etc).

Here it is in action. When I press "refresh" to make a new request to the /now endpoint, keep an eye on three things - the singleton instance never changes, every request to the scoped instance is the same until a new request is made (aka, I hit refresh), and the transient instance always changes.

Hopefully that makes it clearer what the differences are. If you'd like to see the code and play around with it a bit yourself, you'll find it here, alongside tons of samples from other blog posts.

Maybe better naming would've helped here? Not that I have any good ideas. At least "singleton" has single in the name, and a transient thing is fleeting or momentary, so I guess that works too. But scoped? Naming things is hard.

I didn't want to delve any deeper into Dependency Injection and IOC, but if you're interested in learning more, check out these resources too: