Advanced Dependency Injection to use every day.
This repository provides the following packages:
You can install the latest stable version via NuGet:
> dotnet add package SteroidsDI
> dotnet add package SteroidsDI.Core
> dotnet add package SteroidsDI.AspNetCore
.NET Core has built-in support for Dependency Injection. It works and works quite well. We can use the dependencies of the three main lifetimes: singleton, scoped, transient. There are rules that specify possible combinations of passing objects with one lifetime in objects with another lifetime. For example, you may encounter such an error message:
Error while validating the service descriptor 'ServiceType: I_XXX Lifetime: Singleton ImplementationType: XXX': Cannot consume scoped service 'YYY' from singleton 'I_XXX'.
The error says that you cannot pass an object with a shorter lifetime to the constructor of a long-living object. Well that's right! The problem is clear. But how to solve it? Obviously, when using constructor dependency injection (.NET Core has built-in support only for constructor DI), we must follow these rules. So we have at least 3 options:
- Lengthen lifetime for injected object.
- Shorten lifetime for an object that injects a dependency.
- Remove such a dependency.
- Change the design of dependencies so as to satisfy the rules.
The first method is far from always possible. The second method is much easier to implement, although this will lead to a decrease in performance due to the repeated creation of objects that were previously created once. The third way... well, you understand, life will not become easier. So it remains to somehow change the design. This project just offers such a way to solve the problem, introducing a number of auxiliary abstractions. As many already know
Any programming problem can be solved by introducing an additional level of abstraction with the exception of the problem of an excessive number of abstractions.
The project provides three such abstractions:
- Well known
Func<T>
delegate. Defer<T>
/IDefer<T>
abstractions which look likeLazy<T>
but have a significant difference -Defer<T>
/IDefer<T>
do not cache the value.- A named factory interface, when implementation type is generated at runtime.
All these abstractions solve the same problem, approaching the design of their API from different angles. The challenge is to provide a dependency T through some intermediary object X where an explicit dependency on T is either not possible or not desirable. Important! No implementation in this package caches dependency T.
As mentioned above an example of impossibility is a dependency on a scoped lifetime in an object with a singleton lifetime. And an example of non-desirability is creating dependency is expensive and not always required.
There is one important point to make - injecting dependency is not the same as using dependency. In fact, in the case of constructor injection, the injection of the dependency in the constructor leads (in most cases) to storing a reference to the passed value in the some field. The dependency will be used later when calling the methods of "parent" object.
This method is the easiest and offers to inject Func<T>
instead of T
:
Before:
class MyObject
{
private IRepository _repo;
public MyObject(IRepository repo) { _repo = repo; }
public void DoSomething() { _repo.DoMagic(); }
}
After:
class MyObject
{
private Func<IRepository> _repo;
public MyObject(Func<IRepository> repo) { _repo = repo; }
public void DoSomething() { _repo().DoMagic(); }
}
How to configure in DI:
public void ConfigureServices(IServiceCollection services)
{
// First register your IRepository and then call
services.AddFunc<IRepository>();
}
Note that you should call AddFunc
for each dependency T
which you want to inject as Func<T>
.
This method suggests more explicit API - inject IDefer<T>
or Defer<T>
instead of T
:
Before:
class MyObject
{
private IRepository _repo;
public MyObject(IRepository repo) { _repo = repo; }
public void DoSomething() { _repo.DoMagic(); }
}
After:
class MyObject
{
private Defer<IRepository> _repo;
public MyObject(Defer<IRepository> repo) { _repo = repo; }
public void DoSomething() { _repo.Value.DoMagic(); }
}
How to configure in DI:
public void ConfigureServices(IServiceCollection services)
{
// First register your IRepository and then call
services.AddDefer();
}
Note that unlike AddFunc<T>
, the AddDefer
method needs to be called only once. Use IDefer<T>
interface if you
need covariance.
This method is the most difficult to implement, but from the public API point of view it is just as simple as previous two. It assumes that you declare a factory interface with one or more methods without parameters. Method name does not matter. Each factory method should return some dependency type configured in DI container:
public interface IRepositoryFactory
{
IRepository GetPersonsRepo();
}
And inject this factory into your "parent" type:
class MyObject
{
private IRepositoryFactory _factory;
public MyObject(IRepositoryFactory factory) { _factory = factory; }
public void DoSomething() { _factory.GetPersonsRepo().DoMagic(); }
}
How to configure in DI:
public void ConfigureServices(IServiceCollection services)
{
// First register your IRepository and then call
services.AddFactory<IRepositoryFactory>();
}
Implementation for IRepositoryFactory
will be generated at runtime.
In fact, each factory method can take one parameter of an arbitrary type - string, enum, custom class, whatever. In this case, a named binding should be specified. Then you may resolve required services passing the name of the binding into factory methods. If you want to provide a default implementation then you may configure default binding. Default binding is such a binding used in the absence of a named one. A user should set default binding explicitly to be able to resolve services for unregistered names.
public interface IRepositoryFactory
{
IRepository GetPersonsRepo(string mode);
}
public interface IRepository
{
void Save(Person person);
}
public class DemoRepository : IRepository
{
...
}
public class ProductionRepository : IRepository
{
...
}
public class RandomRepository : IRepository
{
...
}
public class DefaultRepository : IRepository
{
...
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IRepository, DemoRepository>()
.AddTransient<IRepository, ProductionRepository>()
.AddTransient<IRepository, RandomRepository>()
.AddFactory<IRepositoryFactory>()
.For<IRepository>()
.Named<DemoRepository>("demo")
.Named<ProductionRepository>("prod")
.Named<RandomRepository>("rnd")
.Default<DefaultRepository>();
}
public class Person
{
public string Name { get; set; }
}
public class SomeClassWithDependency
{
private readonly IRepositoryFactory _factory;
public SomeClassWithDependency(IRepositoryFactory factory)
{
_factory = factory;
}
private bool SomeInterestingCondition => ...
public void DoSomething(Person person)
{
if (person.Name == "demoUser")
_factory.GetPersonsRepo("demo").Save(person); // DemoRepository
else if (person.Name.StartsWith("tester"))
_factory.GetPersonsRepo("rnd").Save(person); // RandomRepository
else if (SomeInterestingCondition)
_factory.GetPersonsRepo("prod").Save(person); // ProductionRepository
else
_factory.GetPersonsRepo(person.Name).Save(person); // DefaultRepository
}
}
In the example above, the GetPersonsRepo
method will return the corresponding implementation of the IRepository
interface, configured for the provided name. For all unregistered names (including null) it will return DefaultRepository
.
Everything is simple here. All three methods come down to delegating dependency resolution to
the appropriate IServiceProvider
. What does appropriate mean? As a rule, in a ASP.NET Core application, everyone
is used to working with one (scoped) provider obtained from IHttpContextAccessor
- HttpContext.RequestServices
.
But in the general case, there can be many such providers. In addition, dependency-consuming code is not aware of their
existence. This code may be a general purpose library no tightly coupled with application specific environment. Therefore
abstraction for obtaining the appropriate IServiceProvider
is introduced. Yes,
one more abstraction again!
This project provides two built-in providers:
AspNetCoreHttpScopeProvider
for ASP.NET Core apps.GenericScopeProvider<T>
for general purpose libraries.
How to configure in DI:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpScope();
services.AddGenericScope<SomeClass>();
}
And of course you can always write your own provider:
public class MyScopeProvider : IScopeProvider
{
public IServiceProvider? GetScopedServiceProvider(IServiceProvider rootProvider) => rootProvider.ReturnSomeMagic();
}
And provide its registration in DI:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMyScope(this IServiceCollection services)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<IScopeProvider, MyScopeProvider>());
return services;
}
}
You can customize the behavior of AddFunc
/AddDefer
/AddFactory
APIs via ServiceProviderAdvancedOptions:
Just use standard extension methods from Microsoft.Extensions.Options/Microsoft.Extensions.Options.ConfigurationExtensions
packages.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ServiceProviderAdvancedOptions>(options => options.AllowRootProviderResolve = true)
services.Configure<ServiceProviderAdvancedOptions>(Configuration.GetSection("Steroids"));
}
You can see how to use all the aforementioned APIs in the example project.
Q. Wait a moment. Doesn't Microsoft.Extensions.DependencyInjection
have support for this out of the box?
A. Unfortunately no. I myself would rather be able to use the existing feature than to write my own package.
Q. Isn't what you offer is a ServiceLocator? I heard that the ServiceLocator is anti-pattern.
A. Yes, ServiceLocator is a known antipattern.
The fundamental difference with the proposed solutions is that ServiceLocator allows you to resolve
any dependency in runtime while Func<T>
, Defer<T>
and Named Factory are designed to resolve
only known dependencies specified at the compile-time. Thus, the principal difference is that all
the dependences of the class are declared explicitly and are injected into it. The class itself does
not pull these dependencies secretly within its implementation. This is so called Explicit Dependencies Principle.
Q. Is this some kind of new dependency injection approach?
A. Actually not. A description of this approach can be found in articles/blogs many years ago, for example here.
Q. What should I prefer - Func<>
or [I]Defer<>
?
A. The main thing is they all work equally under the hood. The difference is in which context you are going to use these APIs.
There are two main differences:
-
The advantage of
Func<>
is that the code in which you injectFunc<>
does not require any new dependency, it is well-known .NET delegate type. On the contrary[I]Defer<>
requires a reference toSteroidsDI.Core
package. -
You should call
AddFunc
for each dependencyT
which you want to inject asFunc<T>
. On the contrary theAddDefer
method needs to be called only once.
Q. What if I want to create my own scope to work with, i.e. not only consume it but also provide?
A. First you should somehow get an instance of root IServiceProvider
.
Then create scope by calling CreateScope()
method on it and set it into GenericScope
:
var rootProvider = ...;
using var scope = rootProvider.CreateScope();
GenericScope<SomeClass>.CurrentScope = scope;
...
... Some code here that works with scopes.
... All registered Func<T>, [I]Defer<T> and
... factories use created scope.
...
GenericScope<T>.CurrentScope = null;
Or you can use a bit simpler approach with Scoped<T>
struct.
IScopeFactory scopeFactory = ...; // can be obtained from DI, see AddMicrosoftScopeFactory extension method
using (new Scoped<SomeClass>(scopeFactory))
or
await using (new Scoped<SomeClass>(scopeFactory)) // Scoped class supports IAsyncDisposable as well
{
...
... Some code here that works with scopes.
... All registered Func<T>, [I]Defer<T> and
... factories use created scope.
...
}
Also see ScopedTestBase and ScopedTestDerived for more info. This example shows how you can add scope support to all unit tests.
The results are available here.