DEV Community

Cover image for ASP.NET Dependency Injection
Mohamad Lawand
Mohamad Lawand

Posted on

ASP.NET Dependency Injection

In this article we will be exploring Asp.Net Dependency Injection. On this episode we will explore and dig deep into dependency Injection, what is it and how does it work. What are the different types of DI life time and when we should use each.

You can watch the full video on Youtube

You can get the full source code on GitHub
https://github.com/mohamadlawand087/v44-dependencyInjection

So what are we going to cover today

  • What is Dependency Injection
  • Advantages of Dependency Injection
  • Disadvantage of Dependency Injection
  • IoC Container
  • Service lifetimes
  • Code Deep Dive

As always please comment your questions, clarifications and suggestions in the comments down below. Please like, share and subscribe if you like the video. It will really help the channel

What is Dependency Injection

In essence dependency injection is a technique in which an object receives other objects that it depends on.

But what does that mean let us take a step back, before understanding what it means in programming, let’s first see what it means in general as it will help us understand the concept
better.

Dependency or dependent means relying on something for support. Like if I say we are relying too much on computers than it means we are dependent on them.

In short dependency injection means that in order of an objection to work as it should i needs to recieve other object that is depends on. Let us see in a simple console application what we mean when an object depend on another objection

When class A uses some functionality of class B, then its said that class A has a dependency of class B.

public class A 
{
    public void GetInfo()
    {
        var b = new B();
        b.PrintHello("Mohamad");
    }
}

public class B 
{

    public void PrintHello(string name)
    {
        Console.WriteLine($"Hello {name}");
    }
}
Enter fullscreen mode Exit fullscreen mode

In C#, before we can use methods of other classes, we first need to create the object of that class (i.e. class A needs to create an instance of class B).

So, transferring the task of creating the object to someone else and directly using the dependency is called dependency injection.

Advantages of Dependency Injection

  • Reduce Code Duplication
  • Loose Coupling
  • Testability
  • Seperation of Concerns

Disadvantage of Dependency Injection

  • Complex: There is a learning curve, and if overused can lead to management issues and other problems.
  • Error Management: Many compile time errors are pushed to run-time.
  • Tracability: If not implemented correctly tracability of issues and optimisation could be hard to do

Asp.Net Core provides a built in dependency injection container, the framework take the responsibility of creating and managing the lifetime of the dependency that it creates.

Dependency Injection is configured using the ConfigureServices method in the startup class. Inside that method we add all of the servies we want to utilise.

Let us create a webapi project and take it from there, we will use the dotnet new to create the project.

dotnet new webapi -n SampleApi
Enter fullscreen mode Exit fullscreen mode

As we can see already in our startup class constructore there is a dependency on the IConfiguration, this configuration option which provides a key/value pair which could be loaded from anywhere appsettings.json, env variables or Azure configuration, Azure key vault.

This dependency injection is being handled by the Asp.Net core runtime, and there are some limitations on what dependencies we can inject in the startup constructor.

Inside the ConfigureServices method we have 2 services that has been added for us when we created our application and which are

// This will add all of the dependencies which will allow WebApi to work
services.AddControllers();

// This will all all of the Swagger dependencies
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "SampleApi", Version = "v1" });
});
Enter fullscreen mode Exit fullscreen mode

Now let us navigate to the WeatherForcastController, we can see that the in this controller constructor we are requesting for an ILogger dependency.

We are basicly stating here that we need a logger in our controller and we dont really know how this logger is configured or where all of the information are going, so if we are logging to a file, a console or any 3rd party service.

The default implementation of the logger in Asp.Net is to log to the console. We can add a small log message in our action and see how it works

_logger.LogInformation("Hello everyone, welcome to the show");
Enter fullscreen mode Exit fullscreen mode

And now let us run our application

dotnet run
Enter fullscreen mode Exit fullscreen mode

And let us navigate to the browser and call the endpoint https://localhost:5001/WeatherForecast

We can see now the log message appearing in the console

Alt Text

If have configured the logged to log a text file, we would have seen this log in a text file. With dependency injections we have inverted the dependencies, our controller now does not care about the logger setup and how does it work or where does it log to. It will basically just request a logger and the Dependency Injection framework provides it for the controller.

IoC Container

When a new object is required, its dependencies need to be assigned to concrete classes. This task can be delegated to a container. When an instance of a particular type is requested to a container, it will inject the implementations required by that type. Those implementations are defined in a set of mappings that can easily be changed.

The Container creates an object of the specified class and also injects all the dependency objects through a constructor, a property, or a method at run time and disposes it at the appropriate time. This is done so that we don't have to create and manage objects manually. Containers provide support for the following DI life cycle.

  • Register: Determines which dependency to instantiate when it encounters a particular type.
  • Resolve: We don't need to create objects manually. The container does it for us. The container includes some methods to resolve the specified type and creates an object of the specified type, injects the required dependencies, and returns the object.
  • Dispose: Manages the lifetime of the dependent objects.

Service lifetimes

The service life-time means how long the service will live before it's disposed of. There are currently three different lifetimes:

  • Transiant: An of the service is created each time we call that service. Its like having the new keyword to initiate the service everytime we are injecting it. this is used for lightweight stateless services
var myService = new MyService();
Enter fullscreen mode Exit fullscreen mode
  • Scopped: these services are created once in the entire request scope. Within the context of a request if we ask for a service we will get the same instance. At the end of the request the service is disposed
  • Singleton: created once when they are requested and the same instance is used through out the application lifetime. Until the App shuts down the same instance will be provided for us.

Code Deep Dive

Let us start exploring the lifetimes in more details

Inside our souce code we will create a new folder called services in the root directory, inside the services folder we will create another folder called Interfaces. Inside the interface folder we will create a new interface called IOperation and it will have the following

public interface IOperation
{
    string OperationId { get; }
}
Enter fullscreen mode Exit fullscreen mode

Next we will create the following 3 interfaces IOperationTransient, IOperationScoped, IOperationSingleton each of these interfaces will represent a lifetime we want to implement

public interface IOperationTransient : IOperation { }
Enter fullscreen mode Exit fullscreen mode
public interface IOperationScoped : IOperation { }
Enter fullscreen mode Exit fullscreen mode
public interface IOperationSingleton : IOperation { }
Enter fullscreen mode Exit fullscreen mode

Now inside our Services folder let us create a new class called Operation, this class will implement the 3 intefaces we created previously IOperationTransient, IOperationScoped, IOperationSingleton. The implementation will create a guid and store the last 6 char of the guid.

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^6..];
    }

    public string OperationId { get; }
}
Enter fullscreen mode Exit fullscreen mode

The next step will be to update the startup class to register the the Operation class according the Naming lifetime. Inside the ConfigureServices in the Startup class we need to add the following

services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
Enter fullscreen mode Exit fullscreen mode

As we can see have registered 3 services with the 3 different lifetimes. Now lets go to our controller and implement the the following interfaces. Inside the WeatherForcast controller let us add the following

public class WeatherForecastController : ControllerBase
{       
    private readonly ILogger<WeatherForecastController> _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public WeatherForecastController(
                ILogger<WeatherForecastController> logger,
                IOperationTransient transientOperation,
                IOperationScoped scopedOperation,
                IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    [HttpGet]
    public IActionResult Get()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        return Ok();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let us run our application and call the endpoint "https://localhost:5001/WeatherForecast"

dotnet run
Enter fullscreen mode Exit fullscreen mode

Once we load the endpoint we can see the weather forcast is populated, when we go to our terminal we can see the logs for the 3 lifetime are populated

Alt Text

We can see the allocated Ids for each of the different lifetime, now let us request the endpoint again and see the results

Alt Text

As we can see from the results above that transiant id stayed the same as the same instance of the obj is provided acrros the applicaiton lifetime while the Scoped is only available within the request scop and transiant is initiated everytime it is requested.

Now let us take it a step further by create a new service and injecting this service in our controller.

Inside our services folder will create a new class called FirstService and add the following

public class FirstService
{
    public readonly IOperationTransient _transientOp;
    public readonly IOperationScoped _scopedOp;
    public readonly IOperationSingleton _singletonOp;
    private readonly ILogger<FirstService> _logger;

    public FirstService(
        IOperationTransient transientOp,
        IOperationScoped scopedOp,
        IOperationSingleton singletonOp,
        ILogger<FirstService> logger
    )
    {
     _transientOp = transientOp;
     _scopedOp = scopedOp;
     _singletonOp = singletonOp;   
     _logger = logger;
    }

    public void GenerateResult()
    {
        _logger.LogInformation("Service 1 - Transient: " + _transientOp.OperationId);
        _logger.LogInformation("Service 1 - Scoped: "    + _scopedOp.OperationId);
        _logger.LogInformation("Service 1 - Singleton: " + _singletonOp.OperationId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let us register these dependencies inside our startup class so they will be part of the DI container when we need to use them. Inside the ConfigureServices method in the startup class let us add the following

services.AddTransient<FirstService, FirstService>();
Enter fullscreen mode Exit fullscreen mode

Now let us update our WeatherForcastController with this new service

public class WeatherForecastController : ControllerBase
{

    private readonly ILogger<WeatherForecastController> _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;
    private readonly FirstService _firstService;

    public WeatherForecastController(
                ILogger<WeatherForecastController> logger,
                IOperationTransient transientOperation,
                IOperationScoped scopedOperation,
                IOperationSingleton singletonOperation,
                FirstService firstService)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
        _firstService = firstService;
    }

    [HttpGet]
    public IActionResult Get()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        _firstService.GenerateResult();

       return Ok();
    }

    [HttpGet]
    [Route("GetAll")]
    public IActionResult GetAll()
    {
        _logger.LogInformation("Second endpoint - Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Second endpoint - Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Second endpoint - Singleton: " + _singletonOperation.OperationId);

        _firstService.GenerateResult();

       return Ok();
    }

}
Enter fullscreen mode Exit fullscreen mode

Now let us run our application

dotnet run
Enter fullscreen mode Exit fullscreen mode

And we can see the results as expected
Alt Text

Discussion (0)