G’day guys!
You’ve probably noticed that Dependency Injection (DI) has become the defacto norm these days, and arguably rightfully so, since it (if used properly) leads to a testable, changeable, de-coupled codebase. But perhaps one challenge (at least in the ASP.NET ecosystem) is that because so much of the bootstrapping is taken care of when we create a new ASP.NET project, it can be confusing and unclear as to how to set up DI in your automated tests or other top-level projects that don’t get the out-of-the-box scaffolding that ASP.NET web projects do. There are a few guides on how to do this out there, but here’s a no-fluff minimalistic one.
Oh, before going any further, I’ll mention that the examples shown here will be using Microsoft’s no-frills DI container that comes out of the box with ASP.NET Core projects. The concepts covered here will be similar for other containers, but the code itself will differ slightly.
First, lets order some packages
When it comes to versioning, chances are you’ve already got these installed in other projects in your solution. So as long as you’re not mixing and matching .NET versions between projects, it makes sense to keep the versions the same. For this reason, I’ll just list the packages you need and let you figure out the appropriate version.
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.Json
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.DependencyInjection.Abstractions
Microsoft.Extensions.Options
Add your app settings
When it comes to configuration, you probably have some custom app settings defined in an appsettings.*.json
file within your existing ASP.NET web project. You’ll need to get this into your new (bare bones) project for the same settings to apply. You have a few options for doing this:
1) Create a custom settings file for your new project; 2) Copy the existing appsettings.json file from your web project into your new one; or 3) Add a link from the existing appsettings.json file to your new project
I usually prefer option 3, so any changes you make in the appsetting.json file in your main web project will also take effect in your new project, and you don’t need to maintain two (or more) config files.
* The appearance of this may look slightly different in Rider or VS for Mac, but it should still be there.
Set it all up
Here, I suggest creating a xxxBootstrapper
class to handle the bootstrapping of the DI container and any other setup, where xxx
is a prefix for your project. Eg: If this is a test project, you could call this TestBootstrapper
.
For example:
public static class TestBootstrapper
{
public static void Bootstrap(IServiceCollection services)
{
// todo: Add any test services
}
}
That’s great, but how do I actually create the IServiceCollection instance?
Ask politely :) Seriously though, just create a new ServiceCollection
, which is the concrete implementation of IServiceCollection
.
public static class ServiceProviderFactory
{
public static IServiceProvider CreateServiceProvider()
{
var services = new ServiceCollection();
LibraryBootstrapper.Bootstrap(services);
TestBootstrapper.Bootstrap(services);
return services.BuildServiceProvider();
}
}
Since we’ll probably be doing this in every single test and the bootstrapping logic will probably grow as our solution does, I’ve placed this inside a factory for convenience.
Putting it all together
Here it is :) Do note that Setup
and Test
are NUnit-specific attributes, which may differ from what you’ll need to use depending on the testing framework you’re using - although you ought to use NUnit because all the others are stupid.
*** The above statement is a joke. Use whichever testing framework you like.
using ConsoleDependencyInjection.Library;
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleDependencyInjection.Tests;
public class MyServiceTests
{
private IMyService _sut;
[SetUp]
public void Setup()
{
_sut = ServiceProviderFactory
.CreateServiceProvider()
.GetService<IMyService>();
}
[Test]
public void Test1()
{
Assert.DoesNotThrow(() =>
{
_sut.DoTheThing();
});
}
}
In the Setup
method (which runs before each test), we create an instance of the service provider from the factory we created earlier, and using a Service Locator approach to DI, ask the provider for the service we want to test. To see the whole thing in action, you can check out the repo on Github.
Thanks for reading! Catch ya!