Decoding .NET Core Dependency Injection: Mastering Inversion of Control

Decoding .NET Core Dependency Injection

Introduction

Dependency Injection (DI) in .NET Core is part of the built-in features of the framework, associating configuration, logging, and the Option Pattern. The Dependency Injection (DI) Design Pattern is an implementation of Inversion of Control (IoC) between classes and their Dependencies.

In a more concrete definition, a Dependency is an object upon which another object relies. In the following example, the MessageWriter class offers a method, Write, that could be relied upon by other classes.

public class MessageWriter: IMessageWriter

{

    public void Write(string message)

    {

        Console.WriteLine($”MessageWriter.Write(message: \”{message}\”)”);

    }

}

If a class needs to use the MessageWriter Write method, it can create a new instance of the MessageWriter class. In the following example, the Worker class depends on the MessageWriter class:

The concept

The tooling provided by .NET allows for a flexible way of injecting dependencies into a class:

  •  The ability to define a dependency through the use of an interface or abstract class that defines how the dependent and dependent classes interact.
  •  The ability to register the dependency in the service container allows the framework to manage the lifetime of the dependency.
  •  The ability to have a service container via the IServiceProvider that is provided by .NET.
  •  The ability to register all dependencies at application start-up and append them to an IServiceCollection, and then build the service container using BuildServiceProvider.
  •  The ability to inject the dependency into the constructor of the class consuming the service.

 

In this example, the IMessageWriter interface defines the Write method, which is implemented by the MessageWriter class. The service is registered using AddSingleton, so only one instance of MessageWriter is created and used throughout the application until it shuts down. The Worker class receives IMessageWriter through dependency injection and uses it to write messages repeatedly in the background.

Example

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<IMessageWriter, MessageWriter>();

builder.Services.AddHostedService<Worker>();

builder.Build().Run();

interface IMessageWriter { void Write(string msg); }

class MessageWriter : IMessageWriter { public void Write(string msg) => Console.WriteLine(msg); }

class Worker(IMessageWriter w) : BackgroundService

{

    protected override async Task ExecuteAsync(CancellationToken t)

    {

        while (!t.IsCancellationRequested)

        {

            w.Write(“Hello”);

            await Task.Delay(1000, t);

        }

    }

}

Why You Need to "Decode" (Understand and Use) Dependency Injection

One of the main features of .NET Core is Dependency Injection (DI). This technology allows builders to take control of the life cycle of their objects, reduce tight coupling between them, and improve the ability to test code. Understanding how DI works in C# allows a developer to understand what happens when services are registered, resolved, and injected into an application using either IServiceProvider or ActivatorUtilities, so that the dependencies required by the constructor will be resolved automatically when created.

Key Benefits of Decoding .NET Core DI:

  • Reduces tight coupling between components
  • Makes code more testable and maintainable
  • Ensures services are reused efficiently (Singleton, Scoped, Transient)
  • Simplifies enterprise and custom software development

What Is Singleton vs. Transient vs Scoped?

These are the define how long a service instance lives in Dependency Injection.

1. Singleton

 Created once and reused for the entire app

Example

builder.Services.AddSingleton<IMyService, MyService>();

Explanation

  • Same instance used everywhere
  • Good for: logging, configuration

2. Transient

Created every time it is requested

Example

builder.Services.AddTransient<IMyService, MyService>();

Explanation

  • Same object within one request
  • New object for next request
  • Good for: database context

3. Scoped

Created once per request (web request)

Example

builder.Services.AddScoped<IMyService, MyService>();

Explanation

  • Same object within one request
  • New object for next request

Constructor injection behavior

In .NET, you can create services with the Interface of IServiceProvider or with ActivatorUtilities. You can also use constructor injection to automatically provide the value of each dependency while creating an instance of a class (constructor).

When a class has multiple constructors, .NET will resolve the dependencies of the service with the most constructed parameters, which will allow that constructor to be chosen by the DI service. 

Key Takeaways:

  • DI only uses the public constructors of an object.
  • We can have multiple overloads for constructors; however, only one overload can be DI-resolvable.

 

This paradigm is very important for Niotechone: As a developer who uses the above architecture will be able to create scalable and maintainable .NET applications. Developing these architectures with DI will afford Niotechone developers the ability to create clean and well-structured dependency-based applications for their enterprise and custom software projects.

Constructor Selection Rules in .NET

In accordance with dependency injection (DI) availability, the .NET Service Provider chooses the constructor; when it has multiple constructors, it picks the one with the greatest number of parameters, given that those parameters can all be resolved by DI.

Example

public class ExampleService

{

    public ExampleService() { }

    public ExampleService(ILogger<ExampleService> logger) { }

    public ExampleService(ServiceA serviceA, ServiceB serviceB) { }

}

When using the ILogger<ExampleService>, the constructor that would use ILogger would be used, even if there were two other parameters registered in the DI container that were available.

Even though the last (3rd) constructor had the most parameters, it cannot be used because DI cannot resolve all three dependencies. 

With this approach, .NET is ensuring that the most DI-friendly constructor will be picked and thus services in scalable applications can be managed and maintained more easily.

Conclusion

In conclusion, the value of Dependency Injection (DI) is tremendously beneficial to developers. DI enables developers to create applications that are scalable, maintainable, and loosely coupled. Developers’ understanding of DI will provide them with clean architectures, while making it easy for other developers (and themselves) to test and extend their architectures by utilizing Dependency Injection through concepts such as constructor injection, service lifetimes (Singleton, Scoped, Transient), and constructor selection rules.

Frequently Asked Questions FAQs

Dependency Injection is a design pattern in .NET where an object will receive its dependency from an external source, such as a service container, rather than creating it internally to the object.

There are three primary types of Dependency Injections:

  • Singleton - Only one instance of the class will be created and reused by the application
  • Scoped - A single instance will be created for each request
  • Transient - Creates a new instance of the class every time it is requested

Overall, Dependency Injection reduces the amount that classes are tightly coupled together, makes your code easier to test, and enables developers to create scalable, maintainable applications that are important to the success of an enterprise-level system.

Constructor Injection is when a class will automatically receive its dependencies by using the class's constructor when the Dependency Injection Container creates a new instance of the class.

The way .NET determines which of the multiple constructors to call is by choosing the constructor with the largest number of resolvable parameters (i.e., the parameters that were provided to the Dependency Injection Container).