Welcome to a brand new series! We're going to dive into how .NET does Dependency Injection, and how we can use it to make our apps much nicer to change. Let's go!
What Is Dependency Injection?
In short, dependency injection is a methodology by which a code object receives other objects that it depends upon (called dependencies), rather than those dependencies needing to be instantiated by that object.
I like to compare it to a needle and thread: the needle must have the thread injected into it (i.e. put through the eye of the needle) before it can do any meaningful work.
Why do we do this? To improve our separation of concerns; that is, to allow different parts of the code base to be able to change independently of other parts.
Background - Dependency Inversion Principle
The concept of Dependency Injection arises from a principle of software design called the Dependency Inversion Principle. This is one of the SOLID principles, and it consists of two parts:
- High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
The basic idea behind this principle is to ensure that both high- and low-level implementations depend on abstractions of an implementation, not the concrete implementation itself. By relying on abstractions rather than concretions, we allow for classes to change more easily; if the concrete details of an implementation change, the classes which rely on the abstraction of that class will likely not need to also change.
In short: the Dependency Inversion Principle is the theory, and Dependency Injection is one implementation of that theory.
Example - Tight Coupling
To see why the dependency inversion principle and dependency injection are good ideas, consider the following classes, MoviesPageModel
and MovieRepository
:
public class MoviesPageModel
{
public List<Movie> movies { get; set; } = new List<Movie>();
public void OnGet()
{
MovieRepository movieRepo = new MovieRepository();
List<Movie> allMovies = movieRepo.GetAll();
}
}
public class MovieRepository()
{
public List<Movie> GetAll()
{
//Returns a list of all movies in our database
}
}
In this instance, we say that MoviesPageModel
and MovieRepository
are "tightly coupled" because if one of them changes their implementation, the other will likely have to change too. For example, say we modify MovieRepository
to have a constructor that takes a connection string to the database.
We must also change the implementation of MoviesPageModel
to match the new implementation of MovieRepository
.
public class MoviesPageModel
{
public List<Movie> movies { get; set; } = new List<Movie>();
public void OnGet()
{
MovieRepository movieRepo
= new MovieRepository("MyConnectionString");
List<Movie> allMovies = movieRepo.GetAll();
}
}
In this particular case, each class's functionality relies on the concrete implementation of the other class, which results in the classes being tightly coupled. Tight coupling is not a good thing to have in our projects, because it increases the amount of work we must do when an implementation detail changes.
By utilizing dependency injection, we can make it so these classes are able to change independently. Before that, though, we must understand how dependency injection works in .NET 6.
Dependency Injection Basics
In .NET 6, dependency injection is a "first-class citizen", which means it is natively supported by .NET. You don't need any third party implementations to use DI in your .NET apps!
To use DI, we must inject dependencies into other classes that depend upon them. These dependencies need an abstraction (which is usually an interface), and the dependent class will invoke that abstraction when it needs the corresponding functionality. However, the concrete implementation of the dependency exists elsewhere, and the class which uses the abstraction does not need to care what exactly that concrete implementation is. In this way, dependency injection in .NET 6 satisfies the Dependency Inversion Principle.
From here, the way to use DI in .NET consists of the answers to five questions:
- What can be a dependency? Anything with an abstraction. Most commonly, these are small, easily modified classes or objects.
- Where do these dependencies exist? We add those services to the "container" for a project.
- How do these dependencies get injected into the classes that need them? .NET will do this automatically for services in the container.
- How do these dependencies behave? We can assign "lifetimes" to dependencies to specify whether they get created once, or on every request, or every time they are needed.
- What kinds of issues can we arrive at with DI? The main one is "circular dependencies", but there are others.
This post will deal with question 1. The next two parts of this series will answer the remaining questions.
What can be injected?
In short: anything with an abstraction.
Generally speaking, we want our dependencies to be small, focused, and easily changed (this is a general software design rule, not something specific to Dependency Injection). For example, let's consider a simple "repository" class, or a class which accesses a data store of some kind (most commonly a database) and returns objects created from data in that data store.
Remember the MovieRepository
class from earlier? We can modify that class to use an interface IMovieRepository
and make it injectable:
public interface IMovieRepository
{
List<Movie> GetAll();
Movie GetByID(int id);
}
public class MovieRepository : IMovieRepository
{
public List<Movie> GetAll()
{
//Implementation
}
public Movie GetByID(int id)
{
//Implementation
}
}
As another example, consider that we want an additional class that provides access to an in-memory cache (ignoring the built-in IMemoryCache
object in .NET 6). We could create such a class, and make it injectable, using an interface and generics:
public interface ICacheService
{
void Add(string key, object value);
T Get<T>(string key);
}
public class CacheService : ICacheService
{
public void Add(string key, object value)
{
//Implementation
}
public T Get<T>(string key)
{
//Implementation
}
}
We will be using these sample classes more thoroughly in the upcoming parts of this series.
The Sample Project
As always, there's a sample project hosted on GitHub that we used to create this post. Check it out!
Summary
Dependency injection in .NET 6 is a process by which dependencies are passed into dependant objects by an object which holds and creates the dependencies, called a container.
In .NET 6, DI is a first-class citizen, natively supported by the framework.
Each dependency consists of an abstraction and an implementation (most commonly an interface and an implementing class, respectively). Any object with both of these can be a dependency. Generally, we want dependencies to be small, focused, and easily changed.
In The Next Post...
We will discuss how to register dependencies with the .NET container, and how to inject them into classes that need them. Stick around!