In this article, I will be talking about a concept that is extremely important to Object Oriented Programming, a concept soo important that it comes up in every single interview. That concept is Dependency Injection
There are many concepts that you will learn in .NET programming that may seem overwhelming at first, but once you get the concept it becomes second nature when designing and coding out applications. In this series of articles, the .NET Fundamentals Series, I will take a deep dive, along with examples, of each of the concepts to help new developers gain a higher level understanding of .NET programming fundamentals. In these articles I will not be talking about basic programming concepts like objects, classes, and the such. I'm currently working on another series for those basics of object oriented programming. In this series, it will be presumed that you have basic, working knowledge of OOP.
In this article I want to start with a very import concept in .NET programming. This concept is so important that you will be questioned at every single interview about it. It is a topic that is sometime confusing at first, but once you understand the concept, it becomes second nature when coding out any .NET Application. This article is all about Dependency Injection. We will be talking about what it is, why it was created, and what are the benefits to using Dependency Injection in applications
Dependency Injection (or DI), by definition, is a design pattern that promotes loose coupling and enhances testability and maintainability of applications. It's a technique where one object supplies dependencies for another. But why was this concept created and what does “loose coupling” refer to? Let's take a look at Dependency Injection and what this all means. In the past, applications were built with tightly coupled components. This meant that every component built heavily relied on other components in the application. But why was this a problem? Well, as technology or business needs changed, applications inevitably need to be updated. When one change was made to meet a requirement, other changes had to made in order to keep the application from breaking. Let's create a small example to illustrate this point…
using System;
using System.Collections.Generic;
// Simulated article model
public class Article
{
public int Id { get; set; }
public string Title { get; set; }
}
// Service that retrieves articles
public class ArticleService
{
public List<Article> GetArticles()
{
// Simulated data retrieval
return new List<Article>
{
new Article { Id = 1, Title = "Intro to C#" },
new Article { Id = 2, Title = "Understanding .NET" }
};
}
}
// Consumer without Dependency Injection
class Program
{
static void Main(string[] args)
{
// Directly instantiate the service
ArticleService service = new ArticleService();
List<Article> articles = service.GetArticles();
foreach (var article in articles)
{
Console.WriteLine($"ID: {article.Id}, Title: {article.Title}");
}
}
}
Above is a simple code snippet that would be used in retrieving articles from a source. In this snippet, the Article service is tightly coupled to Program. In a small program, this might be ok. But if the business needs change or an app-breaking change occurs in the framework that demands the code be updated, this code could possibly break the entire app. It will be difficult to swap out this code or even test this code down the road. So, how can we change this? Let's take a look at a code snippet that allows for DI…
using Microsoft.Extensions.DependencyInjection;
class Program
{
static void Main(string[] args)
{
// Setup DI container
var serviceProvider = new ServiceCollection()
.AddSingleton<IArticleService, ArticleService>()
.BuildServiceProvider();
// Get service via DI
var service = serviceProvider.GetService<IArticleService>();
var articles = service.GetArticles();
foreach (var article in articles)
{
Console.WriteLine($"ID: {article.Id}, Title: {article.Title}");
}
}
}
// Interface for loose coupling
public interface IArticleService
{
List<Article> GetArticles();
}
// Implementation of the interface
public class ArticleService : IArticleService
{
public List<Article> GetArticles()
{
return new List<Article>
{
new Article { Id = 1, Title = "Intro to Dependency Injection" },
new Article { Id = 2, Title = ".NET Best Practices" }
};
}
}
In the above example you can see some changes made to the code. The first that should be noted is that in Program, there is now a DI container, where the dependency injection is occurring. The next change is the introduction of an Interface. Note that now, instead of the ArticleService being used directly in the Program class, it is using the interface instead to access the method for getting the article. The final change is that the ArticleService is using the OOP pillar of Inheritance to inherit the interface. So why is this good? Let's take a look at a scenario where the business has decided that in order to make error handling and maintenance easier. Your team has decided to add logging to the method for getting articles and you want to do this without a lot of changes to the program. Let's see how that is done…
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
class Program
{
static void Main(string[] args)
{
// Setup DI container
var serviceProvider = new ServiceCollection()
.AddLogging(configure => configure.AddConsole())
.AddSingleton<IArticleService, ArticleService>()
.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Starting application...");
var service = serviceProvider.GetRequiredService<IArticleService>();
var articles = service.GetArticles();
foreach (var article in articles)
{
Console.WriteLine($"ID: {article.Id}, Title: {article.Title}");
}
logger.LogInformation("Application finished.");
}
}
// Interface for loose coupling
public interface IArticleService
{
List<Article> GetArticles();
}
// Implementation using injected logger
public class ArticleService : IArticleService
{
private readonly ILogger<ArticleService> _logger;
public ArticleService(ILogger<ArticleService> logger)
{
_logger = logger;
}
public List<Article> GetArticles()
{
_logger.LogInformation("Fetching articles...");
var articles = new List<Article>
{
new Article { Id = 1, Title = "Intro to Dependency Injection" },
new Article { Id = 2, Title = ".NET Best Practices" }
};
_logger.LogInformation($"Retrieved {articles.Count} articles.");
return articles;
}
}
// Simple model
public class Article
{
public int Id { get; set; }
public string Title { get; set; }
}
Notice that in Program, not many changes were made to application. GetArticles() has the only major changes. In Program, articles is still implementing the same interface. This means everything that is depending on that Interface will not be changed, only the piece of code that needs to add the logging services. This means any debugging that needs to be done is confined to the only changes that have been made to the GetArticles() method. But what are some of the other benefits for Dependency Injection?
Enhanced Testability - When you are writing tests, it very difficult to replace dependencies with mocks in a unit test. However, in a unit test if you are inheriting the interface, now you can create a testing mockup based on the interface. This means that you can create isolated and reliable test cases.
Improved Maintainability - With Dependency Injection, you are only changing the implementation of the code, not any of the components that are consuming the service. This means that there is a much less chance of the app breaking because of the changes and if a break occurs, it is confined to the implementation changes only.
Increased Flexibility - Implemented service can be extended without breaking any of the consuming components.
Scalability - In DI, components can be extended and also, components can be developed, tested, and deployed independently.
In this article we took a look at Dependency Injection; what it is, how it is used, and the benefits of using it. Now this is a much bigger topic that includes service registration, implementation in unit testing, and scaling up (or down) applications as needed and I'll go over that in future articles. Right now the goal was to show you just how important this concept of OOP is. Dependency Injection is not only limited to .NET. In Java, SpringBoot also allows for DI in there own way. This is a concept worth diving into more. Once you get a good grasp of this all it will become second nature when designing applications. I hope that this gives you a good start. Good luck in learning and as always, Happy Coding!
With 3 years experience in .NET Application Development, Jay still puts heavy committment towards learning new things in the space. He also has a passion for sharing his knowledge with those who wish to learn more about Full Stack .NET Development.
The more you learn, the more you learn there's more to learn! -Ramsey Lewis
Post a comment