Dependency Injection
Quick Primer to Dependency Injection
The Nexus Mods app uses Dependency Injection (DI) for creating classes.
Dependency Injection is like having a smart assistant who prepares all the tools (interfaces, a.k.a. abstract classes, traits) for a class you are trying to create.
This reduces a lot of boilerplate, helps with maintenance and makes our code cleaner.
A Quick Primer
In the Nexus Mods app, we mostly use 'Constructor Injection'
When you create a new class, you specify what 'services' it needs right in the constructor, via interfaces.
To instantiate the class, you then call serviceProvider.GetRequiredService<MyClass>()
, where
serviceProvider is an IServiceProvider
.
The DI system then automatically provides these 'services' when it creates an instance of your class.
Example: Creating a Class With and Without DI
Let's look at a simple example of how a service is resolved with and without DI. This can be shown in two tabs for clarity.
public class MyClass
{
private readonly IFoo _foo;
private readonly IBar _bar;
private readonly IBaz _baz;
public MyClass(IFoo foo, IBar bar, IBaz baz)
{
_foo = foo; // Foo is provided by DI
_bar = bar; // Bar is provided by DI
_baz = baz; // Baz is provided by DI
}
}
// Create Class
var myGame = serviceProvider.GetRequiredService<MyClass>();
When we ask the DI system for an instance of IFoo
, IBar
, and IBaz
by calling serviceProvider.GetRequiredService<MyClass>()
,
the DI system automatically provides IFoo
, IBar
and IBaz
for the class.
public class MyClass
{
private readonly IFoo _foo;
private readonly IBar _bar;
private readonly IBaz _baz;
public MyClass(IFoo foo, IBar bar, IBaz baz)
{
_foo = foo; // Foo is provided by user
_bar = bar; // Bar is provided by user
_baz = baz; // Baz is provided by user
}
}
// Create Class
var myGame = new MyClass(
new Foo( new SomethingFooNeeds(), new SomethingOtherFooNeeds() ),
new Bar(),
new Baz( new SomethingBazNeeds() ),
);
Without DI, we have to manually create IFoo
, IBar
and IBaz
that are needed by MyClass
.
This can get very messy and inflexible over the course of the long run.
How Does it Know
How does the DI 'magically' know what to insert to the constructor?
Each of our projects has a Services.cs
file which contains a method called Add<ServiceName>
. For example, AddFileExtractors
.
When we start the App or Tests, we add services to an IServiceCollection
(see AddApp), to add our different 'services'.
Example Add<ServiceName>
(Click to Expand)
/// <summary>
/// Adds file extraction related services to the provided DI container.
/// </summary>
/// <param name="coll">Service collection to register.</param>
/// <param name="settings">Settings for the extractor.</param>
/// <returns>Service collection passed as parameter.</returns>
public static IServiceCollection AddFileExtractors(this IServiceCollection coll, IFileExtractorSettings? settings = null)
{
if (settings == null)
coll.AddSingleton<IFileExtractorSettings, FileExtractorSettings>();
else
coll.AddSingleton(settings);
coll.AddSingleton<IFileExtractor, FileExtractor>();
coll.AddSingleton<IExtractor, SevenZipExtractor>();
coll.TryAddSingleton<TemporaryFileManager, TemporaryFileManagerEx>();
return coll;
}
Example:
-
Register 'Foo' as 'Foo':
services.AddSingleton<Foo>();
-
Register 'Foo' as 'IFoo':
services.AddSingleton<IFoo, Foo>();
-
Register Foo as 'IFoo' and 'Foo':
services.AddAllSingleton<IFoo, Foo>();
Although we mostly use Singleton
in the Nexus Mods app, services can have different lifetimes
- Singleton: Only one instance, instance is reused for every requested parameter. (like
static
field)// Only one instance of Foo is created and used everywhere services.AddSingleton<IFoo, Foo>();
- Scoped: Created and reused in each 'scope' (
CreateScope()
). Scopes can be used to e.g. handle a web request.// A new single instance of MyService is created for each scope services.AddScoped<IFoo, Foo>();
- Transient: A new instance is created every time.
// A new instance of MyService is created each time it's requested services.AddTransient<IFoo, Foo>();
Adding Dependency Injection to your New Project
If you are creating a new project, you'll need to manually add dependency injection.
To do this, add a Services.cs
file to your project, which should look something like this:
public static class Services
{
public static IServiceCollection AddYourThing(this IServiceCollection services)
{
// example
// services.AddAllSingleton<ITypeFinder, TypeFinder>();
return services;
}
}
And add the NuGet package Microsoft.Extensions.DependencyInjection.Abstractions
to your project. You can usually do it
by right clicking and hitting Manage NuGet Packages
in your IDE of choice.
Tips and Tricks
Obtaining Multiple Instances of a Registration
It's possible to retrieve all implementations registered for a particular interface.
This is useful when you have multiple classes that fulfill the same role but in different ways.
For example, to get all registered implementations of IGame
(all games):
// Returns all registered IGame implementations
var games = services.GetRequiredService<IEnumerable<IGame>>();