Learning Dependency Injection in ASP.Net Core
This is the first of two posts (update: next post here) focused on removing the mystery around dependency injection and unit testing. The goal of these posts is to demonstrate how we can make loosely coupled, testable applications in ASP.NET Core and the value this adds to our apps and lives as developers. This will mostly benefit developers new to IoC, DI and unit testing.
Before we talk about unit testing in the next post we'll explore dependency injection right now and the role it plays within our software architecture.
What is dependency injection?
DI is a type of Inversion of Control (IoC).
Inversion of Control is just an intimidating term for a group of design principles intended to remove dependencies from your code. It works by offloading the instantiation of dependencies to another module called the container.
DI specifically is a type of IoC which allows dependencies (ie. other components and services in your program) to be injected directly from the container into either the constructor or public properties of a class that depends on them.
Let's see how this works with a simple example.
public class CodeEditor
{
private SyntaxChecker syntaxChecker;
public CodeEditor()
{
this.syntaxChecker = new SyntaxChecker();
}
}
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.
Instantiating SyntaxChecker by newing it up in the constructor like this creates a hard dependency between CodeEditor and SyntaxChecker because we've literally hard-coded its creation here.
This is not ideal.
For starters, if the author of SyntaxChecker removes that parameterless constructor at a later point in time - our code breaks - not good. This is just the tip of the iceberg. There are a number other issues that dependencies like this introduce which we'll get to shortly.
For now, let's consider a better approach.
public class CodeEditor
{
private ISyntaxChecker syntaxChecker;
public CodeEditor(ISyntaxChecker syntaxChecker)
{
this.syntaxChecker = syntaxChecker;
}
}
We've made two small yet powerful changes here. First, we created an abstraction for SyntaxChecker using an interface. This means we're not referencing the concrete implementation anymore, we're just referencing a contract of its behavior.
So we can accept any type of SyntaxChecker here that implements the methods defined in ISyntaxChecker - could be a JavaScriptSyntaxChecker, CSharpSyntaxChecker, PythonSyntaxChecker etc. As long as the class implements everything ISyntaxChecker says it should we're good to go.
// a contract to define the behavior of a syntax checker
public interface ISyntaxChecker
{
bool IsValid();
bool GetLineCount();
bool GetErrorCount();
...
}
// a concrete SyntaxChecker implementation focused on JavaScript
public class JavaScriptSyntaxChecker : ISyntaxChecker
{
public JavaScriptSyntaxChecker()
{
}
public bool IsValid()
{
// implement JavaScript IsValid() method here...
}
... other methods defined in our interface
}
The second big change to CodeEditor is that we got rid of the hard dependency on SyntaxChecker. We replaced the statement to new up a SyntaxChecker in the constructor with a parameter to request an ISyntaxChecker via the constructor. So, clients (or containers as we'll see shortly) creating a CodeEditor have control over which SyntaxChecker implementation to use.
JavaScriptSyntaxChecker jsc = new JavaScriptSyntaxChecker(); // dependency
CodeEditor codeEditor = new CodeEditor(jsc);
CSharpSyntaxChecker cssc = new CSharpSyntaxChecker(); // dependency
CodeEditor codeEditor = new CodeEditor(cssc);
There it is, the essence of dependency injection. Nothing too magical right?
But it does give us a ton of advantages. Two important ones are:
- The ability to control the creation of dependencies outside of the classes that use them. Usually, this is done in a central place like an IoC container rather than repeatedly throughout the application.
- The ability to easily test each class in isolation because we can pass fake or mocked objects into its constructor instead of needing to use a concrete implementation - more on this in the next post.
At this point, I hope you have a clear(er) understanding of how IoC and DI work together with abstractions (interfaces) to create a more loosely-coupled software architecture and some benefits this provides.
Dependency injection in ASP.Net Core
A new feature of ASP.Net Core is that it ships with a simple, lightweight IoC container making dependency injection a first class citizen right out of the box!
This is a big step forward as previous products like WebForms, MVC, SignalR and Web API each had their own mechanisms for using third party containers like Ninject, Autofac, SturctureMap and Castle Windsor.
Note that ASP.NET Core fully supports switching out the built-in IoC provider for one of the others if you require something more feature-rich. The built-in one is designed to be simple, fast and ideal for smaller projects so that's what we'll be focusing on today.
To begin I created a new ASP.NET Core Web Application.
The new project template already comes pre-loaded with the IoC container but if you ever need to add it separately it's part of the Microsoft.Extensions.DependencyInjection NuGet package.
Container concepts
The built-in container in ASP.NET Core is represented by the IServiceProvider interface which supports constructor injection by default.
The types we register with the container are known as services. You register your types with the container in the ConfigureServices method of the Startup class.
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
// registering our custom player repository
services.AddScoped<IPlayerRepository, PlayerRepository>();
// registering our custom game repository
services.AddScoped<IGameRepository, GameRepository>();
}
The IServiceCollection parameter is the collection that stores our registered types. Like other containers, it supports interface matching or creating instances using factory implementations.
We also need to consider the lifetime of the services we're registering. The lifetime of a service in the context of a web app is simply how long the service instance will live during a server request.
The default container supports three lifetimes:
- ServiceLifetime.Transient - A unique instance will be returned from each object request.
- ServiceLifetime.Scoped - In ASP.NET Core applications a scope is created around each server request. Instances registered as scope will be shared within a single request. This type of lifetime scope is useful for Entity Framework DbContext objects (Unit of Work pattern) to be shared across the object scope so you can run transactions across multiple objects.
- ServiceLifetime.Singleton - A single instance is created once and is shared throughout your application’s lifetime.
With our player and game repositories registered, consuming these services in our app couldn't be easier.
In the HomeController I'm using both constructor injection and parameter injection on the Index() action. No manually newing them up, they're just magically available to our controller through the wonders of IoC and DI - sweet! 🙂
public class HomeController : Controller
{
private readonly IPlayerRepository _playerRepository;
public HomeController(IPlayerRepository playerRepository)
{
_playerRepository = playerRepository;
}
public IActionResult Index([FromServices] IGameRepository gameRepository)
{
var players = _playerRepository.GetAll(); // constructor injected
var games = gameRepository.GetTodaysGames(); // parameter injected
return View();
}
Typically, you'll pass most of your dependencies through the constructor but in ASP.Net Core the parameter injection feature is neat and worth keeping in mind for objects that might be expensive to spin up or for some reason don't require constructor injection.
Wrapping up
Phew! If you made it this far - you rock!
IoC and DI are abstract concepts which makes them tricky to explain but I hope you were able to get some idea of their purpose, benefit, and how quickly you can apply them in your ASP.NET Core apps.
In the next post, we will leverage what we learned here by adding some unit tests to really demonstrate the power and value we get from putting our apps together using IoC and DI patterns.
Thanks for reading and please drop any questions or feedback in the comments below!
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.