.NET Core Singleton Dependency Injection With Real Use Case

Introduction

In this article, we will cover the .NET Core Singleton Dependency Injection with real use cases. It will help you to understand when you can use Singleton Dependency injection in your application. We will use it in the .NET core custom middle-ware pipeline, and we will see how it helps you to avoid unnecessary database calls or unnecessary reads from some other cache engines in your application. .NET core DI is a very powerful feature to main the dependencies of your repository, entities, etc.

What does it mean by Singleton?

Singleton is a design pattern, It means that there will be a single copy of your object inside server memory, which will be shared among all the requests (http/client).

So, when you register any dependency in your application as a Singleton, then you will get a single copy of an object per server/node/instance. If you host your application on multiple servers/nodes/instances, then they all individually maintain their singleton object. It won't be shared between multiple servers.

As it is shared among all the requests, then you cannot save specific user-related information on your singleton object. We should create a singleton object of a type (class) that holds the value that is either common for all or shared between multiple users/customers. 

When to use (Example)

Scenario:- Consider you have an API application, and you added a new feature in that application for logging data which is going to write in your database. Before writing to the database, you just call the logging method first to ensure that you logged the object which is going to write in your database. But you do not want to roll out that feature to all the customers, You have a set of customers who will help you out in testing the new logging feature. After getting positive feedback from customers, you will roll out this to all the customers.

So, to achieve this, we generally keep a flag in the database for this feature & we will read that flag in the application middleware; then, we get to know whether can a user log the information or not. But what is the problem here?

The database call on each HTTP request we receive in our API would add a little load on the database for each HTTP request.

Such scenarios are good candidates where we can use the Singleton Dependency Injection.   Logging is one example, the other use case is you want to write your data for reporting to some other data sources also (Elastic Search, DataLake, etc.), but you want to keep it as a feature when someone pays then only you will enable it for them.

There also, you can keep that feature details in your singleton object.

Implementation of Scenario

We have a SQL database with three tables Customers, Features CustomerFeatures.  Which we will use to identify if some particular feature is assigned to the logged-in customer or not:

Customers

Features

CustomerFeatureMapping

So, we have these three tables & we have mapped the features with customers. If you see, this is feature mapping:

We have a stored procedure as "usp_Features_By_CustomerId", it will give you the enabled settings for a customer:

CREATE PROCEDURE usp_Features_By_CustomerId
    @CustomerId int
AS
BEGIN
  
    SELECT c.Name AS CustomerName, f.Name AS Feature
    FROM CustomerFeatureMapping cfm
    INNER JOIN Customers c ON c.CustomerId = cfm.CustomerId
    INNER JOIN Features f ON f.FeatureId = cfm.FeatureId
    WHERE c.CustomerId = @CustomerId
  
END
GO

Here is the structure of our Web API Project:

#A. appsettings.json

Contains the connection string of our SQL database

#B. CustomerFeatures.cs

It is the class that we will use as a Singleton object in the Dependency Injection. It contains a dictionary of numbers and List<string>. 

/// <summary>  
/// This class will be added to Dependency Injection as a singleton, 
/// which will contain a dictionary, where we will have a mapping for each customerId
/// and the List of features enabled.  
/// </summary>  
public class CustomerFeatures  
{  
    /// <summary>  
    /// This dictionary will hold a record for each customer Id and the Feature class object.   
    /// </summary>  
    public Dictionary<int, List<string>> EnabledFeatures = new Dictionary<int, List<string>>();  
} 

#C. Middleware

We have two classes here, CustomMiddleware and RequestMiddleware. It is the middleware that will handle the HTTP request before reaching the actual API controller. In this middleware, we will add the logic to fetch the customer features and put them into the dictionary of the #B class. Here we will check if the dictionary (EnabledFeatures) in singleton object already contains a record for a logged-In customer; then, we will not fetch it from SQL.

RequestMiddleware.cs

In this class, we have the InvokeAsync method, which we will get execute before each controller API call. (We will bind this Middleware to our request pipeline later in #D.) Here we are running the logic (line no. #25 to  #29) that determines whether to get the data from SQL or not. In this way, you can avoid unnecessary SQL calls for your http request. For demo purposes, we are passing loggedInUserId as the Http header in the request. Generally, we get it from our authentication mechanism. 

/// <summary>  
/// This method will be called on each API call, before invoking your API.  
/// </summary>  
/// <param name="context">Your request HttpContext</param>  
/// <param name="customerFeatureMap">The CustomerFeatureMap (Singleton) object</param>  
/// <returns></returns>  
public async Task InvokeAsync(HttpContext context, CustomerFeatures customerFeatureMap)  
{  
    // Customer ID - I am passing it in the header. In general, you should read it from your authentication object like token/cookie.  
    // int loggedInUserId = 0;  

    if (context?.Request.Headers.TryGetValue("loggedInUserId", out StringValues loggedInUserId) == true)  
    {  
        int customerId = Convert.ToInt32(loggedInUserId);  

        // If customer features dictionary is null, then we will initialize it.  
        if (customerFeatureMap.EnabledFeatures == null)  
            customerFeatureMap.EnabledFeatures = new Dictionary<int, List<string>>();  

        /* Checking if the logged-in user ID data is already present in a singleton object.  
         * If it is present, then we will read it from the singleton object. Otherwise, we will fetch the feature details for  
         * the logged-in user from SQL and put it in the singleton object.  
         */
        if (!customerFeatureMap.EnabledFeatures.ContainsKey(customerId))  
        {  
            var features = GetCurrentCustomerFeatures(customerId);  
            customerFeatureMap.EnabledFeatures.Add(customerId, features);  
        }  

        await _next(context);  
    }  
}  

#endregion  

// Fetch the Logged-in customer features from SQL  
public List<string> GetCurrentCustomerFeatures(int customerId)  
{  
    List<string> customerFeatures = new List<string>();  

    // Read Connection string from app settings file  
    string sqlConnectionString = _configuration.GetConnectionString("SingletonDB");  

    using (SqlConnection con = new SqlConnection(sqlConnectionString))  
    {  
        con.Open();  
        using (SqlCommand cmd = new SqlCommand("usp_Features_By_CustomerId", con))  
        {  
            cmd.CommandType = System.Data.CommandType.StoredProcedure;    
            cmd.Parameters.Add(new SqlParameter("@CustomerId", customerId));  

            using (SqlDataReader reader = cmd.ExecuteReader())  
            {  
                while (reader.Read())  
                {  
                    customerFeatures.Add(reader["Feature"].ToString());  
                }  
            }  
        }  
        con.Close();  
    }  

    return customerFeatures;  
}

#D. Startup.cs

Here, we are adding our custom middleware (CustomMiddleware.cs) in the request pipeline inside the Configure method and adding CustomerFeatures as a Singleton in Dependency Injection inside the ConfigureServices method.

public void ConfigureServices(IServiceCollection services)  
{  
    services.AddControllers();  

    // Here we are setting our CustomerFeatures class object as a Singleton  
    services.AddSingleton<CustomerFeatures>();  
}  

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.  
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  

    // Our Custom middleware, which will be used for handling the HTTP requests and validation, etc.  
    app.UseCustomMiddleware(Configuration);  

    app.UseHttpsRedirection();  
    app.UseRouting();  
    app.UseAuthorization();  

    app.UseEndpoints(endpoints =>  
    {  
        endpoints.MapControllers();  
    });  

    var builder = new ConfigurationBuilder();  
    builder.SetBasePath(env.ContentRootPath);  
    foreach (var file in settingsFile)  
    {  
        builder.AddJsonFile(file, reloadOnChange: true, optional: false);  
    }  
}

#E. SingletonDemoController.cs

Here, we have our API (IsLoggingEnabled) and the Injected dependency of CustomerFeatures class (In Constructor),

public class SingletonDemoController : ControllerBase  
{  
    readonly CustomerFeatures _customerFeature;  

    #region Constructor  
    /// <summary>  
    /// Injecting dependency of the Singleton class - CustomerFeatures  
    /// </summary>  
    /// <param name="customerFeatures"></param>  
    public SingletonDemoController(CustomerFeatures customerFeatures)  
    {  
        _customerFeature = customerFeatures;  
    }  
    #endregion  

    #region API  

    /// <summary>  
    /// This API checks if Logging is enabled. This is just a sample. You could use such values from your  
    /// singleton object, instead of looking into your database on every HTTP request.  
    /// </summary>  
    /// <param name="customerId"></param>  
    /// <returns></returns>  
    [HttpGet("IsLoggingEnabled")]  
    public IActionResult IsLoggingEnabled(int customerId)  
    {  
        // Here we are checking in the singleton object  
        if ((_customerFeature.EnabledFeatures.ContainsKey(customerId) && _customerFeature.EnabledFeatures[customerId].Contains("Logging")))  
        {  
            // Your logging code  
        }  

        return Ok(_customerFeature.EnabledFeatures.ContainsKey(customerId) && _customerFeature.EnabledFeatures[customerId].Contains("Logging"));  
    }  

    #endregion  
}  

Executing application

  • Add the debug points on the InvokeAsync Method of Request Middleware in Constructor and IsLoggingEnabled of SingletonDemo Controller.
  • Set a Request Header in your HTTP Request as "loggedInUserId", and pass customerID in it ( We will pass 1 as we have a record with customer Id 1 in our table customers).
  • Run the application and call the API (I am using Postman as I have to pass a header to our request)

     
  • When you click on send or hit the API, you can see that in the first call for customerID 1, The count is 0. Once this code is executed, in the next request, we will count as 1 as will get the customeId 1 record in the singleton object:

  • Again hit the API for customerId 1, and you will see that we already have that EnabledFeature object filled for this customer. So we do not need to fetch it from DB,

You can see that the singleton object maintains its state. So, we should be careful while using it.

The other use case for singleton is if you have a multi-tenant application where you have separate databases for each customer. You keep the connection string for each customer in one unified database. On each API call, you fetch the connection string from DB. There also, you can put the connection details in singleton objects to minimize your data storage trips.

Conclusion

Here, we saw how we can consume the Singleton dependency injection of the .NET core. We checked that it maintains its state on multiple HTTP calls. So, we should be a little careful as it is a shared object for all your HTTP requests. I hope this article helps you to understand how singleton works in .NET Core DI.


Similar Articles