Dependency Injection (DI) stands as a cornerstone in ASP.NET Core development, revolutionizing how developers manage dependencies within their applications. In this article, we'll embark on a journey to unravel the intricacies of DI in ASP.NET Core, peppered with insightful code examples to solidify our understanding.
What is a Dependency?
A dependency is an object that a class needs in order to function. Instead of a class creating its own instances of these objects, it receives them from an external source, often through constructor parameters or property setters.
Simply think if you ever created an object of a class with 'new' keyword, i.e. "new Example()". If yes then, you have introduced a dependency in your code. And it's quite normal in our day to day code.
So, should we care about Dependency Injection?
Yes we should care about it actually. There are some reason we should consider about dependency injection in our applications.
- Loose Coupling
- Enhances Testability
- Improves Code Maintainability
- Encourages Adherence to SOLID Principles
- Simplifies Configuration Management
Loose coupling: We inject a dependency using 'new' keyword. It's okay to use 'new' keyword and make an object. But the problem occurs when we make a direct connection between client class 'MyService' (below example) class and "Repository" class as 'new Repository()'. If you close your eyes and think, in future, if you want to change implementation of "Repository" class, then you have to change both in "Repository" and "MyService" class . But should we allow that? A big NO! Because, it may introduce inconsistencies in our code. And it will be hard to scale.
public class MyService { private readonly Repository _repository; public MyService() { _repository = new Repository(); } // Methods using _repository... }
In the below code, there's no direct dependency with the implementation but with a contract named 'IRepository', if you look closely. Means, no implementation is there. So based on your need, you can change your implementation anytime! And that sound's scalable, isn't it?
public class MyService { private readonly IRepository _repository; public MyService(IRepository repository) { _repository = repository; } // Methods using _repository... }
Enhances Testability:
Tightly coupled classes are hard to unit test because you can't easily isolate the class under test from its dependencies. So, it's always good to avoid tightly coupled injection. In the below example code, we have easily mocked our "IRepository" abstraction layer.
public class MyServiceTests { [Fact] public void TestMethod() { // Arrange var mockRepository = new Mock<IRepository>(); var service = new MyService(mockRepository.Object); // Act // Call methods on service... // Assert // Verify expectations... } }
Code Maintainability:
As previously said, when classes manage their dependencies, it becomes challenging to update or replace those dependencies without modifying the class itself, leading to maintenance issues. By using dependency injection we can sort this problem out centrally.
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IRepository, SqlRepository>(); services.AddScoped<MyService>(); }
Encourages Adherence to SOLID principles:
Without DI, it’s easy to violate SOLID principles, particularly the Single Responsibility Principle and the Dependency Inversion Principle. DI encourages a design where classes are responsible for a single task and depend on abstractions (interfaces) rather than concrete implementations.
Simplifies Configuration Management:
Managing different configurations for various environments (development, testing, production) can be challenging. In this scenarios, DI allows us to inject different configurations or services based on the environment or context, simplifying configuration management.
public void ConfigureServices(IServiceCollection services) { if (Environment.IsDevelopment()) { services.AddSingleton<IRepository, InMemoryRepository>(); } else { services.AddSingleton<IRepository, SqlRepository>(); } }
Step by Step process to setup dependency in a .NET Core application:
Create a Project:
Step 1:
First thing's first. Let's start with creating a .NET API project from scratch. To do that, we need to have an installed Visual Studio. If you don't have Visual Studio installed, you can visit here to install Visual Studio easily. Once the installation in completed, open Visual Studio and you will see a section containing options as shown below image. From that section, select the red marked area that is "Create a new project" option.
Step 2:
On the next page, you should see an option for "ASP.NET Core Web API" as shown in the image below. If you don't see it immediately, don't worry! You can find this option using the search option available in this page. Just ensure that you are creating an ASP.NET Core Web API application.
Step 3:
In this step, please choose an appropriate name for your project and select a suitable location for it. After confirming the project name and location, proceed by clicking the "Next" button. The "Next" button is so satisfying sometime! Just kidding :D
Step 4:
Now, it's time to select your .NET version. In my case, I have selected .NET 6 from the dropdown. You can leave other options as is and press "Create" button at the bottom.
Step 5:
In this step, you need nothing to do but the exploration of the created project and directories inside of it. If this is your first project, savor the moment! There’s nothing quite like the satisfaction of seeing something new that you’ve built. Enjoy the journey of discovering what each part of your project does!
And you project creation is completed.
Create Class and Interface:
Step 1:
Class is the main building block of an object oriented language like C#. A class in C# is a blueprint or template for creating objects, which are instances of the class. It defines a type by encapsulating data (fields) and behaviors (methods) that operate on that data. let's have a look into an example code provided below where how a class looks like after creating it in our application.
Class:
namespace DIExampleAPI { public class Example : IExample { public string DoIt() { Console.WriteLine("Done!"); return "Done"; } } }
And how it looks into our application:
Step 2:
Let's create an interface to make a contract for our "Example" class. So, now you might be thinking, "what is interface! why should we have to have that at all!". Interface is really an interesting concept in an object oriented environment.
Let's understand that with a real life example. Suppose, you are going to switch on your light in your room. Now, you can easily turn on the light with just a simple press, without even knowing what happens inside the wire, electricity and the switch. That's the thing we are going to achieve using our interface and class. Ultimately, class is something like the implementation details inside of switch and interface is the switch button.
Another question you might be thinking on, "Why we choose interface here?". The answer is, it provides us with a way to dependency injection making our project flexible and scalable. We will discuss on that in-depth in some other blog.
Let's have a look on the interface in the below code.
namespace DIExampleAPI { public interface IExample { string DoIt(); } }
And how it looks like in our project:
Inject the dependency:
Step 1:
In this step, let's take a moment to explore the "Program.cs" file, the heart of any .NET Core project. This file is where the magic begins, featuring the fantastic service container (DI Container) that makes managing dependencies a breeze.
The service container was first introduced in ASP.NET Core 2.0 and has since become an integral part of .NET applications. In our project, the "Program.cs" file is where we'll register our dependencies, thanks to the builder (WebApplicationBuilder) object. This object provides us with the "Services (IServiceCollection)" collection, making dependency registration easy and convenient.
In the below image, if you look into "builder.Service" is basically provides us with IServiceCollection, so that we can register our dependencies there.
Additionally, in this file, you'll find the app object created by builder.Build(). This is another marvel that we'll discuss in a future blog post. Shortly, the "app" object helps build the request pipeline for each and every incoming requests in our application.
Step 2:
Now we are ready to register the dependency of our class and interface in the service container. In the image below, we can see that we have used the "AddScoped" method to register our dependency. There are three ways to register dependencies:
- AddScoped
- AddTransient
- AddSingleton
Each of these methods offers unique characteristics to our application. There's a lot to explore about these methods, which we will cover in another blog post. As you can see in the image below, I have used "AddScoped" to create an instance of the "Example" class for every request made to this application. That instance of the "Example" class will be bound to the "IExample" interface as an object. In the next step, we will see how we can inject our object into our client/caller class.
Step 3:
We are on the final step to dependency injection process. Let's clean our project a bit as we do not need "WeatherForecast" related items. After that, let's add our new controller "DoItController" as shown below.
And in this step, we will set our dependency to be prepared for injection in the runtime. To do that, we will add a read only field named "_example". And will initiate that field using the constructor of our Controller class. Means, we are done with our dependency configuration! We can use the object injected into "_example" field anywhere as shown in the below image. In this case, we have created a controller method "GetDoIt" to use the injected object. Yes, That's it!
using Microsoft.AspNetCore.Mvc; namespace DIExampleAPI.Controllers { [ApiController] [Route("[controller]")] public class DoItController : ControllerBase { private readonly IExample _example; public DoItController(IExample example) { _example = example; } [HttpGet(Name = "DoIt")] public IActionResult GetDoIt() { var result = _example.DoIt(); return Ok(result); } } }
Test the Application:
By default .NET 6 comes with swagger API documentation installed inside. So, we need not configure it manually. Let's start our application in debug mode. That should look like the image provided below,
Now, let's expand and execute.
In the below image, you will be able to view the response with "Done". That means we are ultimately getting the result form the object of "Example" class.
Conclusion:
Incorporating Dependency Injection (DI) in ASP.NET Core is a game-changer for developers, offering a structured and efficient way to manage dependencies within applications. This journey into the world of DI has illuminated its vital role in fostering loose coupling, enhancing testability, improving code maintainability, adhering to SOLID principles, and simplifying configuration management.
By abstracting dependencies and injecting them where needed, we pave the way for more scalable and maintainable code. We've seen firsthand through examples how DI allows us to replace tightly coupled class dependencies with flexible, easily testable interfaces.
The process of setting up DI in a .NET Core application, from creating a project to injecting dependencies, is straightforward. The Program.cs
file becomes the heart of your application, where dependencies are registered and managed. The use of services like AddScoped
, AddTransient
, and AddSingleton
gives you control over the lifecycle of your dependencies, ensuring your application is both efficient and responsive.
To recap, our example journey involved creating a simple Example
class and an IExample
interface, registering these in the service container, and finally injecting them into a controller. This setup not only made our application cleaner and more modular but also prepared it for future scalability.
As we move forward, embracing DI will continue to enhance our development practices, making our code more robust and easier to manage. Whether you are new to DI or refining your skills, understanding and applying these principles will undoubtedly lead to better, more maintainable software.
Happy coding! And remember, the power of DI lies not just in writing less code, but in writing better, more flexible code. Enjoy your journey in mastering Dependency Injection in ASP.NET Core!