← Back to Article List         
Singleton Design Pattern

Singleton Design Pattern

Published on 27 Sep 2026     18 min read Design Principles & Patterns
Singleton Pattern

Singleton Design Pattern in C#

1. What is Singleton Design Pattern?

Singleton is a Creational Design Pattern that ensures a class has only one instance throughout the application and provides a common way to access that instance.

Simple definition:

Singleton = Create only one object of a class and reuse the same object everywhere.

For example:

Application
    │
    ├── Service A ──┐
    ├── Service B ──┼──► Same Singleton Object
    └── Service C ──┘

Instead of:

Service A → Object 1
Service B → Object 2
Service C → Object 3

Singleton gives:

Service A ──┐
Service B ──┼──► Object 1
Service C ──┘

2. Why Do We Need Singleton?

Suppose we have a simple application counter.

public class AppCounter
{
    public int Count { get; private set; }

    public void Increment()
    {
        Count++;
    }
}

If different parts of the application create their own objects:

var counter1 = new AppCounter();
var counter2 = new AppCounter();

counter1.Increment();

Console.WriteLine(counter1.Count);
Console.WriteLine(counter2.Count);

Output:

1
0

Why?

Because:

counter1 → AppCounter Object #1 → Count = 1

counter2 → AppCounter Object #2 → Count = 0

They are different objects.


3. Requirement

Suppose our requirement is:

The entire application should maintain one common counter.

We want:

Application
    │
    ├── Module A ──┐
    │              │
    ├── Module B ──┼──► AppCounter
    │              │      Count
    └── Module C ──┘

Everyone should use the same instance.

This is where Singleton can be useful.


4. Simple Singleton Program

Let's create a very simple Singleton.

public class AppCounter
{
    private static AppCounter? _instance;

    private AppCounter()
    {
    }

    public static AppCounter Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new AppCounter();
            }

            return _instance;
        }
    }

    public int Count { get; private set; }

    public void Increment()
    {
        Count++;
    }
}

Then:

public class Program
{
    public static void Main()
    {
        AppCounter counter1 = AppCounter.Instance;

        counter1.Increment();

        AppCounter counter2 = AppCounter.Instance;

        counter2.Increment();

        Console.WriteLine(counter1.Count);
        Console.WriteLine(counter2.Count);

        Console.WriteLine(
            ReferenceEquals(counter1, counter2));
    }
}

Output:

2
2
True

The important output is:

True

It proves:

counter1
    │
    └────► Same AppCounter Object
                     ▲
    ┌────────────────┘
counter2

5. Full Simple Program

using System;

public class AppCounter
{
    // Holds the single instance
    private static AppCounter? _instance;

    // Private constructor
    private AppCounter()
    {
        Console.WriteLine("AppCounter object created.");
    }

    // Provides access to the single instance
    public static AppCounter Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new AppCounter();
            }

            return _instance;
        }
    }

    public int Count { get; private set; }

    public void Increment()
    {
        Count++;
    }
}

public class Program
{
    public static void Main()
    {
        AppCounter counter1 = AppCounter.Instance;

        counter1.Increment();

        AppCounter counter2 = AppCounter.Instance;

        counter2.Increment();

        Console.WriteLine($"Counter 1: {counter1.Count}");
        Console.WriteLine($"Counter 2: {counter2.Count}");

        Console.WriteLine(
            $"Same Object: {ReferenceEquals(counter1, counter2)}");
    }
}

Output

AppCounter object created.

Counter 1: 2
Counter 2: 2

Same Object: True

Notice:

AppCounter object created.

appears only once.


6. How Does Singleton Work?

Three things are particularly important.

1. Static Instance Variable

private static AppCounter? _instance;

This variable stores the single object.

Because it is static, it belongs to the class rather than to an individual object.

Conceptually:

AppCounter Class
      │
      └── static _instance
                │
                ▼
          AppCounter Object

7. Private Constructor

This is one of the most important parts:

private AppCounter()
{
}

Normally, if the constructor were public:

public AppCounter()
{
}

anyone could write:

var a = new AppCounter();
var b = new AppCounter();
var c = new AppCounter();

That would create three objects and defeat the purpose of Singleton.

Therefore, the constructor is:

private AppCounter()

Now this is not allowed outside the class:

var counter = new AppCounter();

The compiler will reject it because the constructor is inaccessible.

So:

Private constructor prevents normal external object creation.


8. Public Static Instance

The application still needs some way to get the object.

That's why we have:

public static AppCounter Instance

The client uses:

AppCounter.Instance

instead of:

new AppCounter()

9. What Happens the First Time?

Suppose we execute:

AppCounter counter1 = AppCounter.Instance;

The getter executes:

if (_instance == null)
{
    _instance = new AppCounter();
}

Initially:

_instance = null

Therefore:

Is _instance null?
       │
      YES
       ↓
new AppCounter()
       ↓
Store object in _instance
       ↓
Return _instance

Now:

_instance
    │
    ▼
AppCounter Object

10. What Happens the Second Time?

Now:

AppCounter counter2 = AppCounter.Instance;

Again:

if (_instance == null)

But this time:

_instance != null

because the object already exists.

Therefore:

Is _instance null?
       │
       NO
       ↓
Don't create another object
       ↓
Return existing _instance

So:

counter1 ─────┐
              │
              ▼
        AppCounter Object
              ▲
              │
counter2 ─────┘

11. Complete Execution Flow

First call:

AppCounter.Instance

Flow:

AppCounter.Instance
       ↓
Is _instance null?
       ↓
      Yes
       ↓
new AppCounter()
       ↓
Store in _instance
       ↓
Return object

Second call:

AppCounter.Instance
       ↓
Is _instance null?
       ↓
       No
       ↓
Return existing object

That's the basic Singleton mechanism.


12. Why Is static Important?

Consider:

private static AppCounter? _instance;

static means the variable belongs to the class itself.

You don't need an AppCounter object to access:

AppCounter.Instance

This is important because if we needed an object first:

var counter = new AppCounter();

we would already have broken the Singleton concept.

Therefore we access the singleton through the class:

AppCounter.Instance

13. Lazy Creation

Our example creates the object only when somebody first requests it.

if (_instance == null)
{
    _instance = new AppCounter();
}

This is called:

Lazy Initialization

Meaning:

Don't create the object until it is actually needed.

For example:

Application Starts
       ↓
No AppCounter created

Some code calls AppCounter.Instance
       ↓
AppCounter created

14. Important Problem With Our Simple Version

The previous example is good for understanding the pattern, but it has a thread-safety problem.

Imagine two threads execute this simultaneously:

if (_instance == null)
{
    _instance = new AppCounter();
}

Possible situation:

Thread A                  Thread B

_instance == null        _instance == null
       ↓                        ↓
     true                     true
       ↓                        ↓
new AppCounter()          new AppCounter()

Potentially, two objects could be created.

For a real multithreaded application, we need a thread-safe implementation.


15. Recommended Simple C# Singleton

A cleaner implementation uses Lazy<T>:

public sealed class AppCounter
{
    private static readonly Lazy<AppCounter> _instance =
        new Lazy<AppCounter>(() => new AppCounter());

    private AppCounter()
    {
        Console.WriteLine("AppCounter created.");
    }

    public static AppCounter Instance
    {
        get
        {
            return _instance.Value;
        }
    }

    public int Count { get; private set; }

    public void Increment()
    {
        Count++;
    }
}

Or more concisely:

public sealed class AppCounter
{
    private static readonly Lazy<AppCounter> _instance =
        new(() => new AppCounter());

    private AppCounter()
    {
    }

    public static AppCounter Instance => _instance.Value;

    public int Count { get; private set; }

    public void Increment()
    {
        Count++;
    }
}

Lazy<T> gives us lazy initialization and handles thread-safe instance initialization by default.

 

Why use Lazy<AppCounter>?

It gives two important benefits:

Lazy initialization: the AppCounter isn't created until it is first needed.

Thread-safe initialization: Lazy<T>'s default mode safely ensures one initialization when multiple threads try to access .Value concurrently.

One additional point: while Lazy<T> makes creation of the Singleton instance thread-safe, the mutable Count++ operation itself is not thread-safe. In a multithreaded application such as Web API, we'd use Interlocked.Increment() or another synchronization mechanism for the counter state.

 


16. Why sealed?

Notice:

public sealed class AppCounter

sealed prevents another class from inheriting from AppCounter.

For example:

public class MyCounter : AppCounter

is not allowed.

For Singleton implementations, sealing the class is commonly used to prevent inheritance from complicating the one-instance guarantee.


17. Singleton Structure

The pattern can be visualized as:

             Singleton Class
          ┌────────────────────┐
          │                    │
          │ private constructor│
          │                    │
          │ static instance    │
          │                    │
          │ Instance property  │
          │                    │
          └─────────┬──────────┘
                    │
                    ▼
             Single Object
                    ▲
              ┌─────┼─────┐
              │     │     │
           Client A B     C

18. Real-World Uses

Singleton-style lifetimes are useful when one shared instance is appropriate, for example:

  • Application-wide configuration service
  • In-memory shared cache service
  • Metrics collector
  • Feature/configuration registry
  • Expensive stateless service that is safe to share

However, in modern ASP.NET Core, you normally let the DI container manage the singleton lifetime instead of manually implementing Instance.


19. Singleton in ASP.NET Core

This is particularly important for your Web API work.

ASP.NET Core provides Singleton lifetime through DI:

builder.Services.AddSingleton<AppCounter>();

Then a controller can receive it:

[ApiController]
[Route("api/[controller]")]
public class CounterController : ControllerBase
{
    private readonly AppCounter _counter;

    public CounterController(AppCounter counter)
    {
        _counter = counter;
    }

    [HttpGet]
    public IActionResult Get()
    {
        _counter.Increment();

        return Ok(_counter.Count);
    }
}

In this case, you do not need to manually write:

private static AppCounter? _instance;

or:

public static AppCounter Instance

ASP.NET Core DI manages the lifetime.

ASP.NET Core DI Container
            │
            ▼
      AppCounter Object
            ▲
       ┌────┼────┐
       │    │    │
Request 1   │   Request 3
            │
         Request 2

The same registered singleton service instance is reused across requests within that application/service-provider lifetime.


20. Singleton Pattern vs AddSingleton()

These are related, but don't confuse them.

Traditional Singleton Pattern

The class itself controls creation:

public static AppCounter Instance => ...

Client:

AppCounter counter = AppCounter.Instance;

ASP.NET Core DI Singleton

The DI container controls the lifetime:

builder.Services.AddSingleton<AppCounter>();

Client uses constructor injection:

public CounterController(AppCounter counter)
{
    _counter = counter;
}

For modern ASP.NET Core applications, DI-managed singleton lifetime is usually preferable because dependencies remain explicit and testing is easier.


21. Singleton vs Scoped vs Transient

Since you're working with ASP.NET Core Web API, this distinction is essential.

builder.Services.AddSingleton<MyService>();
builder.Services.AddScoped<MyService>();
builder.Services.AddTransient<MyService>();

Singleton

One shared instance
      ↓
Request 1 ──┐
Request 2 ──┼── Same Object
Request 3 ──┘

Scoped

Typically one instance per HTTP request:

Request 1 → Object A
Request 2 → Object B
Request 3 → Object C

Within Request 1, consumers in the same scope can receive the same scoped instance.

Transient

A new instance is generally created each time the service is resolved:

Resolve → Object A
Resolve → Object B
Resolve → Object C

22. Important Warning in Web API

A Singleton service lives much longer than a request.

Therefore, be careful about injecting shorter-lived scoped dependencies into it.

For example, don't directly constructor-inject an EF Core DbContext registered as scoped into a singleton:

public class MySingletonService
{
    private readonly AppDbContext _context;

    public MySingletonService(AppDbContext context)
    {
        _context = context;
    }
}

With the usual:

builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddSingleton<MySingletonService>();

this creates a lifetime mismatch.

Conceptually:

Singleton
   │
   └── tries to hold Scoped DbContext

A singleton can outlive the request scope that a scoped dependency belongs to.


23. Thread Safety Is Important

Another major point:

Singleton instance creation being thread-safe does not automatically make all of the singleton object's mutable state thread-safe.

For example:

public int Count { get; private set; }

public void Increment()
{
    Count++;
}

In a concurrent Web API, many requests may call Increment() simultaneously.

Count++ is not an atomic compound operation.

For a production counter, you could use:

private int _count;

public int Increment()
{
    return Interlocked.Increment(ref _count);
}

This is an important practical distinction:

Singleton creation thread safety
              ≠
Singleton business-state thread safety

24. Advantages

1. Single Instance

Ensures one shared instance according to the singleton's defined lifetime.

2. Shared State

When shared mutable state is genuinely required, different consumers can access the same object.

3. Controlled Object Creation

Traditional Singleton controls construction through:

private AppCounter()

and:

AppCounter.Instance

4. Avoids Repeated Expensive Construction

If an object is expensive to initialize and safe to share, reusing one instance can avoid repeated initialization.

5. Lazy Initialization

With implementations such as:

Lazy<AppCounter>

the object can be created only when first needed.

6. Built-in DI Support in ASP.NET Core

ASP.NET Core provides:

AddSingleton<T>()

so you usually don't need to manually implement the classic Singleton mechanics.


25. Disadvantages

Singleton should not be used simply because "one object sounds efficient."

Potential problems include:

  • Global/shared mutable state
  • Thread-safety issues
  • Hidden dependencies with traditional static access
  • Harder unit testing when code directly calls static singleton accessors
  • Lifetime problems
  • Accidental retention of data for too long

For example:

AppCounter.Instance.Increment();

creates a dependency on a globally accessible object.

Constructor injection:

public MyService(AppCounter counter)

makes the dependency more explicit and easier to replace during testing.


26. When Should We Use Singleton?

Consider Singleton when:

One shared instance is genuinely required
                +
Object is safe to share
                +
Its dependencies have compatible lifetimes

Good candidates can include stateless or thread-safe shared services.

Be cautious with:

Per-user data
Per-request data
EF Core DbContext
Mutable collections without synchronization
Request-specific information

Those generally should not simply be stored in a singleton.


27. Traditional Singleton Flow

Remember it this way:

Client asks for Instance
          ↓
Does instance already exist?
          ↓
      ┌───┴───┐
      │       │
     No      Yes
      │       │
      ↓       │
Create       │
Object       │
      │       │
      └───┬───┘
          ↓
Return same object

Key Points

  • Singleton is a Creational Design Pattern.
  • It ensures a class has a single shared instance according to the intended application/runtime scope.
  • Traditional Singleton normally uses a private constructor.
  • The private constructor prevents normal external new creation.
  • A static field/property provides access to the singleton instance.
  • Lazy<T> is a clean way to implement lazy, thread-safe initialization in C#.
  • sealed is commonly used to prevent inheritance from complicating the Singleton guarantee.
  • Singleton is useful only when one shared instance is genuinely appropriate.
  • Shared mutable state requires careful thread-safety handling.
  • Thread-safe Singleton creation does not automatically make its internal mutable state thread-safe.
  • In ASP.NET Core, prefer DI-managed lifetime using AddSingleton<T>() in most application code.
  • Singleton, Scoped, and Transient are the three commonly used ASP.NET Core DI lifetimes.
  • Avoid directly injecting scoped services such as a typical EF Core DbContext into a Singleton.
  • Avoid storing request-specific or user-specific mutable data in a Singleton.
  • Singleton can reduce repeated construction of expensive, safely shareable objects.
  • Overusing Singleton can introduce global state, coupling, testing difficulties, and concurrency problems.

One-line interview answer

Singleton is a creational design pattern that ensures a class has one shared instance and provides a controlled way to access that instance; in ASP.NET Core, this lifetime is usually managed through Dependency Injection using AddSingleton().

 

In real-world ASP.NET Core / Web API projects, Singleton is normally used for a service that is expensive to create, stateless or safely shared, and does not contain request/user-specific state.

Common industry scenarios

Scenario Singleton? Example
Application configuration ✅ Settings loaded once and reused
In-memory lookup/reference data ✅ Country codes, status mappings
Thread-safe application cache ✅ Shared reference-data cache
Metrics/telemetry collector ✅ Application-wide counters/metrics
Expensive reusable service ✅ Parser/rules engine initialized once
Feature/configuration provider ✅ Shared application configuration
HttpClient infrastructure ✅ conceptually Long-lived handlers managed by IHttpClientFactory
EF Core DbContext ❌ Usually Scoped
Current user information ❌ Request-specific
Shopping cart/session data ❌ User-specific
Request processing service with mutable state ❌ Usually Scoped/Transient

Practical example: reference-data cache

Suppose your Web API frequently needs a list of countries.

Without caching, every request might do:

GET /api/customer
      ↓
CustomerService
      ↓
Database
      ↓
SELECT * FROM Countries

Thousands of requests could repeatedly retrieve data that rarely changes.

You could create a shared, thread-safe reference-data service:

public class ReferenceDataCache
{
    private readonly Dictionary<int, string> _countries = new()
    {
        { 1, "India" },
        { 2, "USA" },
        { 3, "UK" }
    };

    public string? GetCountry(int id)
    {
        return _countries.GetValueOrDefault(id);
    }
}

Register it:

builder.Services.AddSingleton<ReferenceDataCache>();

Inject it:

public class CustomerService
{
    private readonly ReferenceDataCache _cache;

    public CustomerService(ReferenceDataCache cache)
    {
        _cache = cache;
    }

    public string? GetCountry(int countryId)
    {
        return _cache.GetCountry(countryId);
    }
}

ASP.NET Core creates one ReferenceDataCache instance and reuses it:

                    DI Container
                         │
                         ▼
                ReferenceDataCache
                    Object #1
                  ▲      ▲      ▲
                  │      │      │
Request 1 ────────┘      │      │
Request 2 ───────────────┘      │
Request 3 ──────────────────────┘

A very important production rule

Before registering your own service as:

AddSingleton<MyService>();

ask:

Can this same object safely be used simultaneously by many requests?

If yes, Singleton may be appropriate.

If the class contains mutable fields such as:

public string CurrentUser { get; set; }
public int CurrentOrderId { get; set; }

it is generally a bad Singleton.

Imagine:

Request 1 → User = Syed
                     ↘
                    Same Singleton Object
                     ↗
Request 2 → User = John

Concurrent requests can interfere with shared mutable state.

Another important example: DbContext

Don't do:

builder.Services.AddSingleton<AppDbContext>();

DbContext is designed around a unit-of-work and is not thread-safe. In Web API it is normally registered as Scoped:

builder.Services.AddDbContext<AppDbContext>();

Conceptually:

Request 1 → DbContext #1

Request 2 → DbContext #2

Request 3 → DbContext #3

Also avoid a singleton depending directly on a scoped DbContext:

Singleton Service
      ↓
Scoped DbContext   ❌

because the singleton has a longer lifetime.

What I see most often in ASP.NET Core

For business/application services, Scoped is often more common than Singleton, especially when the service depends on EF Core:

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<IPaymentService, PaymentService>();

Singleton is more appropriate for application-wide infrastructure or immutable/thread-safe shared services:

builder.Services.AddSingleton<IReferenceDataCache, ReferenceDataCache>();
builder.Services.AddSingleton<IMetricsCollector, MetricsCollector>();
builder.Services.AddSingleton<IRuleEngine, RuleEngine>();

Easy rule to remember

Singleton
   ↓
Application-wide + thread-safe shared service

Scoped
   ↓
One instance per HTTP request
   ↓
Business services / DbContext

Transient
   ↓
New instance each resolution
   ↓
Small lightweight stateless services

Interview answer: In production ASP.NET Core applications, Singleton is typically used for thread-safe, application-wide services that are safe and useful to reuse across requests, such as shared configuration/reference-data providers, caches, metrics collectors, or expensive reusable stateless components. It should not hold request-specific or user-specific mutable state, and it should not directly depend on scoped services such as DbContext.

 

ASP.NET Core Singleton Example — Shared Configuration

A practical scenario is an application-wide configuration service containing values such as:

  • Application name
  • Support email
  • Maximum upload size
  • Maintenance mode

These values are shared across the application and don't contain per-user/per-request state.

1. appsettings.json

{
  "ApplicationSettings": {
    "ApplicationName": "My Portfolio API",
    "SupportEmail": "support@example.com",
    "MaxUploadSizeMB": 10,
    "MaintenanceMode": false
  }
}

2. Create Configuration Model

public class ApplicationSettings
{
    public string ApplicationName { get; set; } = string.Empty;

    public string SupportEmail { get; set; } = string.Empty;

    public int MaxUploadSizeMB { get; set; }

    public bool MaintenanceMode { get; set; }
}

3. Register It as Singleton

In Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var settings = builder.Configuration
    .GetSection("ApplicationSettings")
    .Get<ApplicationSettings>();

builder.Services.AddSingleton(settings!);

var app = builder.Build();

app.MapControllers();

app.Run();

The important line is:

builder.Services.AddSingleton(settings!);

The configuration is read and converted into an ApplicationSettings object during startup.

That same object is then available throughout the application.

appsettings.json
       ↓
IConfiguration
       ↓
ApplicationSettings object
       ↓
AddSingleton()
       ↓
ASP.NET Core DI Container
       ↓
Same object shared with consumers

4. Inject It Into a Controller

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ConfigurationController : ControllerBase
{
    private readonly ApplicationSettings _settings;

    public ConfigurationController(
        ApplicationSettings settings)
    {
        _settings = settings;
    }

    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new
        {
            _settings.ApplicationName,
            _settings.SupportEmail,
            _settings.MaxUploadSizeMB,
            _settings.MaintenanceMode
        });
    }
}

ASP.NET Core automatically passes the singleton object into:

public ConfigurationController(
    ApplicationSettings settings)

You don't need to manually create it.


5. Test the API

Call:

GET /api/configuration

Response:

{
  "applicationName": "My Portfolio API",
  "supportEmail": "support@example.com",
  "maxUploadSizeMB": 10,
  "maintenanceMode": false
}

6. What Happens Internally?

When the application starts:

var settings = builder.Configuration
    .GetSection("ApplicationSettings")
    .Get<ApplicationSettings>();

ASP.NET Core reads:

"ApplicationSettings": {
    "ApplicationName": "My Portfolio API",
    "SupportEmail": "support@example.com",
    "MaxUploadSizeMB": 10,
    "MaintenanceMode": false
}

and creates:

ApplicationSettings Object
--------------------------------
ApplicationName = My Portfolio API
SupportEmail     = support@example.com
MaxUploadSizeMB  = 10
MaintenanceMode  = false

Then:

builder.Services.AddSingleton(settings!);

puts that specific instance into the DI container.

Now imagine three controllers use it:

                 DI Container
                      │
                      ▼
          ApplicationSettings
               Object #1
             ▲     ▲     ▲
             │     │     │
Controller A ┘     │     │
Controller B ──────┘     │
Controller C ────────────┘

All receive the same object.


7. Prove It Is the Same Object

Create two services.

Service A

public class ServiceA
{
    private readonly ApplicationSettings _settings;

    public ServiceA(ApplicationSettings settings)
    {
        _settings = settings;
    }

    public ApplicationSettings GetSettings()
    {
        return _settings;
    }
}

Service B

public class ServiceB
{
    private readonly ApplicationSettings _settings;

    public ServiceB(ApplicationSettings settings)
    {
        _settings = settings;
    }

    public ApplicationSettings GetSettings()
    {
        return _settings;
    }
}

Register them:

builder.Services.AddTransient<ServiceA>();
builder.Services.AddTransient<ServiceB>();

Controller:

[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
    private readonly ServiceA _serviceA;
    private readonly ServiceB _serviceB;

    public TestController(
        ServiceA serviceA,
        ServiceB serviceB)
    {
        _serviceA = serviceA;
        _serviceB = serviceB;
    }

    [HttpGet]
    public IActionResult Get()
    {
        var settings1 = _serviceA.GetSettings();
        var settings2 = _serviceB.GetSettings();

        return Ok(new
        {
            SameObject = ReferenceEquals(settings1, settings2)
        });
    }
}

Response:

{
  "sameObject": true
}

Even though ServiceA and ServiceB are separate objects:

ServiceA ──────┐
               │
               ▼
       ApplicationSettings #1
               ▲
               │
ServiceB ──────┘

They received the same singleton configuration object.


8. But There Is an Important Production Detail

For configuration specifically, ASP.NET Core has the Options Pattern, which is usually preferable to manually doing this:

builder.Services.AddSingleton(settings!);

For example:

builder.Services.Configure<ApplicationSettings>(
    builder.Configuration.GetSection("ApplicationSettings"));

and then:

public ConfigurationController(
    IOptions<ApplicationSettings> options)
{
    _settings = options.Value;
}

There are three commonly used options interfaces:

IOptions<T>
IOptionsSnapshot<T>
IOptionsMonitor<T>

They have different configuration reload/lifetime behavior.

So for an interview:

Can configuration be shared using a Singleton?

Yes.

But for normal ASP.NET Core configuration, I would say:

Use the built-in Options Pattern rather than creating a custom Singleton configuration service unless you have a specific reason.

Key point

Custom application-wide shared object
            ↓
       AddSingleton<T>()

Configuration from appsettings.json
            ↓
       Options Pattern
            ↓
IOptions<T> / IOptionsMonitor<T>

For static configuration that does not need to change while the application is running, the Singleton example above is simple and valid. For configuration that should react to changes, IOptionsMonitor<T> is generally the more appropriate direction.