Let’s explore registering the dependencies of the Demo Feature. That feature contains the following code:
namespace CompositionRoot.DemoFeature;
public class MyFeature
{
private readonly IMyFeatureDependency _myFeatureDependency;
public MyFeature(IMyFeatureDependency myFeatureDependency)
{
_myFeatureDependency = myFeatureDependency;
}
public void Operation()
{
// use _myFeatureDependency
}
}
public interface IMyFeatureDependency { }
public class MyFeatureDependency : IMyFeatureDependency { }
As we can see, there is nothing complex but two empty classes and an interface. Remember that we are exploring the registration of dependencies, not what to do with them or what they can do—yet.Now, we want the container to serve an instance of the MyFeatureDependency class when a dependency requests the IMyFeatureDependency interface as the MyFeature class does. We want a singleton lifetime.To achieve this, in the Program.cs file, we can write the following code:
builder.Services.AddSingleton<MyFeature>();
builder.Services.AddSingleton<IMyFeatureDependency, MyFeatureDependency>();
We can also chain the two method calls instead:
builder.Services
.AddSingleton<MyFeature>()
.AddSingleton<IMyFeatureDependency, MyFeatureDependency>()
;
However, this is not yet elegant. What we want to achieve is this:
builder.Services.AddDemoFeature();
To build that registration method, we can write the following extension method:
using CompositionRoot.DemoFeature;
namespace Microsoft.Extensions.DependencyInjection;
public static class DemoFeatureExtensions
{
public static IServiceCollection AddDemoFeature(this IServiceCollection services)
{
return services
.AddSingleton<MyFeature>()
.AddSingleton<IMyFeatureDependency, MyFeatureDependency>()
;
}
}
As highlighted, the registration is the same but uses the services parameter, which is the extended type, instead of the builder.Services (builder does not exist in that class, yet the services parameter is the same object as the builder.Services property).If you are unfamiliar with extension methods, they come in handy for extending existing classes, like we just did. Besides having a static method inside a static class, the this keyword next to the first parameter determines whether it is an extension method.For example, we can build sophisticated libraries that are easy to use with a set of extension methods. Think System.Linq for such a system.Now that we learned the basics of dependency injection, there is one last thing to cover before revisiting the Strategy design pattern.
Using external IoC containers
ASP.NET Core provides an extensible built-in IoC container out of the box. It is not the most powerful IoC container because it lacks some advanced features, but it does the job for most applications.Rest assured; we can change it to another one if need be. You might also want to do that if you are used to another IoC container and want to stick to it.Here’s the strategy I recommend:
- Use the built-in container, as per Microsoft’s recommendation.
- When you can’t achieve something with it, look at your design and see if you can redesign your feature to work with the built-in container and simplify your design.
- If it is impossible to achieve your goal, see if extending the default container using an existing library or coding the feature yourself is possible.
- If it is still impossible, explore swapping it for another IoC container.
Assuming the container supports it, it is super simple to swap. The third-party container must implement the IServiceProviderFactory<TContainerBuilder> interface. Then, in the Program.cs file, we must register that factory using the UseServiceProviderFactory<TContainerBuilder> method like this:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory<ContainerBuilder>(new ContainerBuilderFactory());
In this case, the ContainerBuilder and ContainerBuilderFactory classes are just wrappers around ASP.NET Core, but your third-party container of choice should provide you with those types. I suggest you visit their documentation to know more.Once that factory is registered, we can configure the container using the ConfigureContainer<TContainerBuilder> method and register our dependencies as usual, like this:
builder.Host.ConfigureContainer<ContainerBuilder>((context, builder) =>
{
builder.Services.AddSingleton<Dependency1>();
builder.Services.AddSingleton<Dependency2>();
builder.Services.AddSingleton<Dependency3>();
});
That’s the only difference; the rest of the Program.cs file remains the same.As I sense you don’t feel like implementing your own IoC container, multiple third-party integrations already exist. Here is a non-exhaustive list taken from the official documentation:
- Autofac
- DryIoc
- Grace
- LightInject
- Lamar
- Stashbox
- Simple Injector
On top of replacing the container entirely, some libraries extend the default container and add functionalities to it. We explore this option in Chapter 11, Structural Patterns.Now that we have covered most of the theory, we revisit the Strategy pattern as the primary tool to compose our applications and add flexibility to our systems.