Card image cap

Understanding Dependency Injection and Usage in ASP.NET Core

ASP.NET Core  • Posted 23 days ago

While developing applications in the AspNetCore environment, we seldom use a "new" keyword to create new instance of a Dependency required in our components. We often come across terms like services, container, IoC and - Dependency Injection. In this article, let's get in detail about what Dependency Injection is about, and how Dependency Injection plays a major role in developing loosely coupled and well-designed applications in ASP.NET Core.

What is Dependency Injection?

"The term Dependency Injection is a phenomenon, where the required dependencies inside a component are not instantiated and instead the requesting component makes use of an "injected" instance of the dependency's abstract type for its cause."

Confusing isn't it? Let's try to understand this better with an example for illustrating the above statement.

Let's assume there is a component ReadersRepository which exposes a single function Get() that does some operation on a collection of Reader objects. The source for this collection of Reader objects is another class called FakeReadersContext, which supplies the Reader collection by means of a property "Readers".

Generally, we design the ReadersRepository class as below:

public class PlainReadersRepository : IReadersRepository
{
    public IEnumerable<Reader> Get(Func<Reader, bool> predicate)
    {
        var context = new FakeReadersContext();
        return context.Readers.Where(predicate);
    }
}

Now this approach isn't wrong; its just a bad design. This component is now tightly coupled with the class FakeReadersContext, which can't be replaced once this code is compiled. If there's another method which also requires this object, it creates its own object which is just awful.

Whereas the same component by virtue of Dependency Injection, looks like below:

public class ReadersRepository : IReadersRepository
{
    IReadersContext _context;

    public ReadersRepository(IReadersContext context)
    {
        _context = context;
    }

    public IEnumerable<Reader> Get(Func<Reader, bool> predicate)
    {
        return _context.Readers.Where(predicate);
    }
}

Compare this code snippet to the one before; both do the same thing, but how they are designed differs alot. First, there is no dependency with the FakeReadersContext; instead the ReadersRepository class uses an abstract type IReadersContext in the place of FakeReadersContext which also exposes the same property Readers which the FakeReadersContext did. Second, there is no more "new" keyword used inside of the method; the scope of the _context variable is now "instance-level" and is "assigned" with a value passed into the Constructor.

Since the constructor of a class is the first to be executed, the value is set during the ReadersRepository instantiation and is available by the time the Get() method is called.

This design of passing the required "dependency" through the constructor and the requesting component accessing by means of the dependency's abstraction is called "Dependency Injection".

Dependency Injection is a phenomenon, which is based on a software design principle and the fifth of the "SOLID" principles - the Dependency Inversion Principle.

The Dependency Inversion Principle

The Dependency Inversion Principle (DIP) states that:

"High level modules should not depend on low-level modules. Both should communicate by means of abstractions"

Which means that components shouldn't work with concrete instances for a cause and must only communicate by means of their abstract types.

In our previous example, the ReadersRepository class is communicating with its low-level dependency "FakeReadersContext" directly in the first case - which is against this principle. Hence in the second case, the class communicates with the abstract type IReadersContext which the FakeReadersContext implements.

A lot about the Dependency Inversion Principle has been talked in the below article:

Understanding SOLID Principles - The Dependency Inversion Principle

Why to use Dependency Injection?

Dependency Injection tries to solve two core design issues as shown by the example code snippet above.

  1. It makes components loosely coupled from one another - the IReadersContext abstract type can be replaced by any of its implementation which can serve a different purpose - say a different data source for the Readers collection.
  2. Since the components are loosely coupled, the component is now testable.
  3. Since the objects are NOT instantiated in the component, it is now not responsible for disposing the object - it is taken up by the one which creates the object of ReadersRepository along with its dependency.

Generally, developers implement Dependency Injection by employing IoC container frameworks inside their applications.

What is an IoC Container?

The principle of Dependency Inversion is also called as Inversion of Control (IoC). In the above example of ReadersRepository we know that the IReadersContext instance is "injected" from the source where the ReadersRepository is instantiated, and that source is responsible for maintaining the object and its dependency's lifetime.

"An IoC container is a framework which maintains these instances and injects the respective dependencies in the places where an object is requested."

A few examples of such frameworks are:

  • AutoFac
  • Ninject
  • StructureMap and so on.

In ASP.NET Core, the Dependency Injection mechanism is provided by the built-in IoC container framework which comes along with the AspNetCore library.

Ways of Injecting Dependencies:

Generally, Dependencies can be injected using an IoC container in three ways:

  • Constructor - the most used kind of injection, where the dependencies required by a component are injected via the constructor of that component and are accessed as instance variables. The one we saw earlier is an example of Constructor Injection.

  • Method - It is used when the instance of a dependency is only required within a single method in the entire component. The dependency is passed as a method parameter with a decoration to indicate that it is to be supplied by the container and the container passes an instance of the requested type when that method is called.

  • Property - It is used when a property of the component holds the dependency instance and it is resolved during runtime by the container. This type of injection is not available in the AspNetCore container.

Generally, a container requires us to configure the abstract types which are to be "injected" along with their concrete implementations that are passed whenever an instance of these abstract types are required.

In ASP.NET Core, this configuration happens at the Startup class within the ConfigureServices() method, where we add "services" to the IServiceCollection.

IServiceProvider and IServiceCollection:

As said before, any container requires the probable dependencies and their implementations be configured beforehand. In ASP.NET Core, we configure the abstract types which the container needs to maintain for objects, along with their implementations so that when a dependency abstract type that is configured inside the container is requested, the container injects an instance of its configured implementation type into the requesting component. These configured types are called as "services".

This collection of the dependency injectable services and their implementations are maintained by the IServiceCollection of the AspNetCore container. In the ConfigureServices() method of the Startup class, we add our own custom "services" to the IServiceCollection instance so that the container handles the injection for us.

Back to our example, let's assume that the IReadersRepository instance is called inside a DataService class, which is further accessed inside a controller. The flow looks like below:

public class DataService : IDataService
{
    private readonly IReadersRepository _readers;

    public DataService(IReadersRepository readers)
    {
        _readers = readers;
    }

    public IReadersRepository Readers => _readers;
}
public class ReadersController : ControllerBase
{
    private readonly IDataService _data;

    public ReadersController(IDataService data)
    {
        _data = data;
    }

    [HttpGet]
    public IActionResult Get()
    {
        var data = _data.Readers.Get(x => x.Id > 0);
        return Ok(data);
    }
}

All these abstract types injected starting from the Controller have dependencies within them which needed to be resolved - IDataService depends on IReadersRepository which depends on IReadersContext. All these are configured as services inside the AspNetCore container and are added to the IServiceCollection as below:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IReadersContext, FakeReadersContext>();
    services.AddSingleton<IReadersRepository, ReadersRepository>();
    services.AddSingleton<IDataService, DataService>();            
    services.AddControllers();
}

By marking all these as services, the container takes the responsibility of injecting a resolved instance of IDataService which is DataService into the ReadersController, which internally obtains a resolved instance of IReadersRepository - ReadersRepository and further ReadersRepository obtains a resolved instance of IReadersContext which is FakeReadersContext.

data/Admin/2020/7/di-flow-runtime-2.png

The container is completely responsible for serving instances of these types whenever it is requested and is also responsible for their disposal when they are rendered as expired.

Apart from the custom services that we can create, there are built-in services which are created upon application startup and can be used directly in the application by means of DI.

For example:

  • IConfiguration - access configuration settings
  • IHostEnvironment - access application hosted directory and paths
  • IApplicationBuilderFactory - access application builder
  • IServiceProvider - access the servicecollection to request for a service explicitly
  • IOptions, IOptionsSnapshot, IOptionsMonitor - access strictly-typed classes bound to config sections

are some of the most used built-in services.

IServiceProvider:

Apart from the constructor injection for resolving service instances inside a component, we can also use the IServiceProvider which can be used to request instance of a configured service inside a method.

public class ReadersController : ControllerBase
{
    private readonly IServiceProvider _sp;

    public ReadersController(IServiceProvider provider)
    {
        _sp = provider;
    }

    [HttpGet]
    public IActionResult Get()
    {
        var dService = _sp.GetRequiredService<IDataService>();
        var data = dService.Readers.Get(x => x.Id > 0);
        return Ok(data);
    }
}

Injectables and Lifetimes - Types of Services:

Observe that we've used the method AddSingleton() to add these types to the IServiceCollection. Why AddSingleton ? To answer this, we need to understand that whenever we're creating services whose instances are maintained by the container, we should also specify the lifetime of that type whose object is served. There are three types of services based on their lifetime properties.

  • Singleton - Similar to a static class, once the object created the same object is injected till the application shuts down.

  • Scoped - one object of the configured type is created and served within a scope - a HTTP request creates a scope within it and so we can say that the same object is reused within a request scope.

  • Transient - a new instance of the configured type is created and injected each time the object is requested.

These service lifetimes are discussed in detail in the below article:

Exploring ASP.NET Core Fundamentals - Understanding Singleton Transient and Scoped Service Lifetimes

Dependency Injection and Best Practices:

Now that we have looked at all about Dependency Injection, the IoC containers and also the services, let's conclude by discussing a little about the things to keep in mind, while designing services for Dependency Injection.

  • Services should also receive and access their dependencies internally via Dependency Injection
  • Services should avoid static classes, methods and things which can maintain state.
  • Avoid instantiating concrete types inside the services as much as possible. Instead, configure them to be injected.
  • If a class has too many services injected, it can be a sign that the class is handling multiple responsibilities and must be refactored to only hold a single responsibility.

The complete example is available under https://github.com/referbruv/aspnetcore-dependencyinjection-sample

dependency injection asp.net core dependency injection asp.net core example dependency injection asp.net core 3.1 dependency injection asp.net core web api dependency injection in asp.net core api dependency injection c# asp.net core web api dependency injection asp.net core mvc asp.net core dependency injection best practices asp.net core built in dependency injection built-in dependency injection in asp.net core dependency injection asp.net core controller dependency injection in asp.net core c# dependency injection container asp.net core custom dependency injection asp.net core dependency injection c# asp.net core implementing dependency injection in asp.net core dependency injection in .net core mvc dependency injection in asp.net core mvc asp.net core dependency injection get service

We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept