This post is part 2 of a 3-part series. You might want to read Part 1 first.

We know from the previous post in this series that in order to make a dependency injectable into another code object, the dependency must:

  • Have an abstraction (most commonly an interface) AND
  • Be inserted into the .NET container.

We know how to do the first part; we need to create an abstraction (most commonly an interface) for any class we want to be injected as a dependency. For this series, we're using a MovieRepository class and IMovieRepository interface, which has the following methods:

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
    }
}

This post is all about how to do the second part: how to add dependencies to .NET's container so that they can be injected into their dependent classes. We'll also talk about what exactly the container is, and whether or not we need to worry about disposing dependencies.

Photo by Deepak Rautela / Unsplash

The Sample Project

As always, there's a sample project hosted on GitHub that we used to create this post. Check it out!

GitHub - exceptionnotfound/NET6DependencyInjectionDemo
Contribute to exceptionnotfound/NET6DependencyInjectionDemo development by creating an account on GitHub.

Anatomy of the Program.cs File

Registering dependencies in the container takes place in the Program.cs file in a .NET 6 application. Here's the default Program.cs file that Visual Studio 2022 created when I made a net .NET 6 Razor Pages app:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

This file does many things, but when implementing DI we're mostly concerned with the builder object. That object is of type WebApplicationBuilder, and using it we can create the dependencies, routes, and other items we need for our web application to work as expected.

Adding Dependencies to the Container

.NET 6's implementation of WebApplicationBuilder exposes the Services object, which allows us to add services to the .NET container, which .NET will then inject to dependent classes.

The object also defines a large set of methods that add common .NET objects to the container, such as:

builder.Services.AddMvc(); //Adds basic MVC functionality
builder.Services.AddControllers(); //Adds support for MVC controllers (views and routing would need to be added separately)
builder.Services.AddLogging(); //Adds default logging support
builder.Services.AddSignalR(); //Adds support for SignalR

These methods often take "options" classes, which define how those parts of the application are going to behave. For example, let's say we want to add anti-forgery support to our app in order to prevent cross-site scripting (XSRF) attacks. We can do so like this, which uses the Options pattern to specify the form field name and the HTTP header name.

builder.Services.AddAntiforgery(options =>
{
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
});

In our sample app (which is in the GitHub repo), we use the Options pattern in a similar thing in order change the default Razor Page:

builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
    options.Conventions.AddPageRoute("/Movies", "");
});

But what about our custom class MovieRepository? In order to add that to the container, we need to select a service lifetime for it. The next post will discuss service lifetimes in more detail; for now, all you need to know is that the following code will add an instance of MovieRepository to the container.

builder.Services.AddTransient<IMovieRepository, MovieRepository>();

Use Extension Methods to Register Dependencies

In real-world applications, we most often have quite a few more dependencies than a single repository. In order to keep our Program.cs file clean and readable, we often create extension methods that register groups of related dependencies with the .NET container.

using DependencyInjectionNET6Demo.Repositories;
using DependencyInjectionNET6Demo.Repositories.Interfaces;

namespace DependencyInjectionNET6Demo.Extensions;

public static class ServiceExtensions
{
    public static void RegisterRepos(this IServiceCollection collection)
    {
        collection.AddTransient<IMovieRepository, MovieRepository>();
        //Add other repositories
    }

    public static void RegisterLogging(this IServiceCollection collection)
    {
        //Register logging
    }

    public static void RegisterAuth(this IServiceCollection collection)
    {
        //Register authentication services.
    }
}

Then, in the Program.cs file, we can call them like so:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.RegisterAuth();
builder.Services.RegisterRepos();
builder.Services.RegisterLogging();

In this way, our Program.cs file is kept clean and easy to modify.

Whether we use extension methods or just the basic methods in Program.cs, we will reach a point where all of our services are now in our container. But how do we inject them into dependent classes?

Injecting Dependencies

In .NET 6, dependencies must be injected through a constructor. Let's say we have a Razor Page called Movies.cshtml, which uses a class called MoviesPageModel as its code-behind file:

@page "/Movies"
@model MoviesPageModel
@{
    ViewData["Title"] = "Movies";
}

<div class="text-center">
    <h1 class="display-4">Movies</h1>
</div>

<table class="table">
    <thead>
        <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Release Date</th>
            <th>Runtime</th>
            <th></th>
        </tr>
    </thead>

    @foreach(var movie in Model.Movies)
    {
        <tr>
            <td>@movie.ID</td>
            <td>@movie.Title</td>
            <td>@movie.ReleaseDate</td>
            <td>@movie.RuntimeMinutes min</td>
            <td><a asp-page="/MovieDetails" asp-route-id="@movie.ID">Details</a></td>
        </tr>
    }
</table>
public class MoviesPageModel : PageModel
{
    public List<Movie> Movies { get; set; } = new List<Movie>();

    public MoviesPageModel() { }

    public void OnGet()
    {
        //Implementation
    }
}

Based on the markup in the Movies.cshtml razor page, we can say with good certainty that this page needs to display a collection of movies. The MovieRepository can provide that collection to the page.

To do this, we need to inject an instance of MovieRepository's abstraction (the interface IMovieRepository) into MoviePageModel, call the method IMovieRepository.GetAll() to get the movies, and put that result into the MoviePageModel.Movies property during the OnGet() method. All of this can be done like so:

public class MoviesPageModel : PageModel
{
    private readonly IMovieRepository _movieRepo;
    public List<Movie> Movies { get; set; } = new List<Movie>();

    public MoviesPageModel(IMovieRepository movieRepo)
    {
        _movieRepo = movieRepo;
    }

    public void OnGet()
    {
        Movies = _movieRepo.GetAll();
    }
}

If we run the app at this point, we see that, in fact, we do get a list of movies displayed on this page:

Great! We can now inject our MovieRepository class!

Injection to Other Classes

If instead you are using MVC and not Razor Pages, you could inject to a Controller class like so:

public class MovieController : Controller
{
    private readonly IMovieRepository _movieRepo;
    
    public MovieController(IMovieRepository movieRepo)
    {
        _movieRepo = movieRepo;
    }
}

"But wait!" you say. "That doesn't look any different from the Razor Pages example." You are correct. Any injectable class can be injected into any other class.

But that can lead us to potential problems.

Circular Dependencies

Imagine that, in addition to the MovieRepository and IMovieRepository, we have an ActorRepository class and an IActorRepository interface, the former of which needs an instance of IMovieRepository injected to it:

public interface IActorRepository { }

public class ActorRepository : IActorRepository
{
    private readonly IMovieRepository _movieRepo;

    public ActorRepository(IMovieRepository movieRepo)
    {
        _movieRepo = movieRepo;
    }
}

After all, sometimes our app might want an actor's data, as well as all of the movies they have been in.

But let's also say we want the reverse: we sometimes want a movie with the actors that acted in it. For that, we might decide we need to inject IActorRepository into MovieRepository:

public class MovieRepository : IMovieRepository
{
    private readonly IActorRepository _actorRepo;

    public MovieRepository(IActorRepository actorRepo)
    {
        _actorRepo = actorRepo;
    }
    
    //...Rest of implementation
}

Welcome to a Very Bad Idea! We have created a circular dependency: MovieRepository depends on IActorRepository, and ActorRepository depends on IMovieRepository. Therefore the container can't actually create either of these dependencies, because they both depend upon each other.

In fact, if you run the sample project with this circular dependency in place, you'll get the following error message:

"InvalidOperationException: A circular dependency was detected for the service of type DependencyInjectionNET6Demo.Repositories.Interfaces.IMovieRepository".

The solution is straightforward: only one of these dependencies can depend upon the other. In this particular case, we'll remove ActorRepository's dependence on MovieRepository.

public class ActorRepository : IActorRepository { }

This problem is easy to see when you only have two dependencies, but in real-world projects it can be much harder to catch. You could have a first dependency which injects a second, and the second injects a third, and third injects a fourth, but the fourth injects the first!

Which leads to one of the guiding rules of software app design when using dependency injection: classes should not depend upon other classes at the same layer in the architecture. For our app, repository-level classes should not inject other repositories. However, classes at a higher level (e.g. Razor Pages models or MVC controllers) can inject classes from the lower level.

Dependencies with No Further Dependencies

A cool situation you might run into is having an injectable class that itself has no dependencies. A common example for this might be a logging class; it is unlikely for a logging class to have any other dependencies.

public interface ILogger 
{
    //Implementation
}

public class MyLogger : ILogger
{
    //Implementation
}
I am ignoring the built-in .NET ILogger interface

These are the best kinds of dependencies because they are exempt from the above rule; they can be freely injected into any layer of your application, since they cannot cause circular dependencies.

Disposal of Dependencies

You might be wondering: if the container creates instances of dependencies, how are those instances deleted or disposed of? Lucky for us, the .NET 6 container handles this natively! Instances of dependencies are automatically deleted or disposed of either at the end of a request, or at application shutdown, depending on the dependency's service lifetime.

Summary

Dependencies are added to .NET 6's container in the Program.cs file, using methods such as AddTransient<T>. .NET 6 includes a bunch of "shortcut" functions to add commonly-used implementations, such as AddMvc() or AddSignalR(). We can use extension methods to add groups of related dependencies into the container.

Dependencies are injected into objects via that object's constructor. The constructor takes the dependency's abstraction as a parameter, and assigns that value to a local, private variable.

Circular dependencies happen when two or more dependencies depend upon each other. In general, to prevent problems like this, dependencies should not depend upon other dependencies at the same level in the application architecture.

In The Next Post...

Coming up in the next (and last) post in this series, we take a look at the service lifetimes we mentioned in this post and walk through what they are and how they are meant to be used. Thanks for reading!