Skip to content

sungam3r/SteroidsDI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SteroidsDI

Buy Me A Coffee

License

codecov Nuget Nuget

GitHub Release Date GitHub commits since latest release (by date) Size

GitHub contributors Activity Activity Activity

Run unit tests Publish preview to GitHub registry Publish release to Nuget registry CodeQL analysis

Advanced Dependency Injection to use every day.

Installation

This repository provides the following packages:

Package Downloads Nuget Latest Description
SteroidsDI.Core Nuget Nuget Dependency Injection primitives
SteroidsDI Nuget Nuget Advanced Dependency Injection for Microsoft.Extensions.DependencyInjection: AddDefer, AddFunc, AddFactory; depends on SteroidsDI.Core
SteroidsDI.AspNetCore Nuget Nuget Scope Provider for ASP.NET Core; depends on SteroidsDI.Core

You can install the latest stable version via NuGet:

> dotnet add package SteroidsDI
> dotnet add package SteroidsDI.Core
> dotnet add package SteroidsDI.AspNetCore

What is it ? Why do I need it ?

.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:

  1. Lengthen lifetime for injected object.
  2. Shorten lifetime for an object that injects a dependency.
  3. Remove such a dependency.
  4. 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:

  1. Well known Func<T> delegate.
  2. Defer<T>/IDefer<T> abstractions which look like Lazy<T> but have a significant difference - Defer<T>/IDefer<T> do not cache the value.
  3. 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.

Func<T>

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>.

IDefer<T> and Defer<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.

Named factory

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.

How it works?

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:

  1. AspNetCoreHttpScopeProvider for ASP.NET Core apps.
  2. 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;
    }
}

Advanced behavior

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"));
}

Examples

You can see how to use all the aforementioned APIs in the example project.

FAQ

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:

  1. The advantage of Func<> is that the code in which you inject Func<> does not require any new dependency, it is well-known .NET delegate type. On the contrary [I]Defer<> requires a reference to SteroidsDI.Core package.

  2. You should call AddFunc for each dependency T which you want to inject as Func<T>. On the contrary the AddDefer 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.

Benchmarks

The results are available here.