• Stars
    star
    469
  • Rank 93,098 (Top 2 %)
  • Language
    C#
  • License
    MIT License
  • Created over 3 years ago
  • Updated 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Pure DI for .NET

Pure DI for .NET

NuGet License

Key features

Pure.DI is not a framework or library, but a source code generator. It generates a partial class to create object graphs in the Pure DI paradigm. To make this object graph accurate, it uses a set of hints that are checked at compile time. Since all the work is done at compile time, you only have effective code ready to use at run time. The generated code is independent of .NET library calls or reflection and is efficient in terms of performance and memory consumption because, like normal code, it is subject to all optimizations and integrates seamlessly into any application - without using delegates, extra method calls, boxing, type conversions, etc.

  • DI without any IoC/DI containers, frameworks, dependencies and therefore without performance impact or side effects.

    Pure.DI is actually a .NET code generator. It generates simple code as well as if you were doing it yourself: de facto just a bunch of nested constructors` calls. And you can see this code at any time.

  • A predictable and verifiable dependency graph is built and verified on the fly while you write the code.

    All the logic for analyzing the graph of objects, constructors, methods happens at compile time. Thus, the Pure.DI tool notifies the developer about missing or circular dependency, for cases when some dependencies are not suitable for injection, etc., at compile-time. Developers have no chance of getting a program that crashes at runtime due to these errors. All this magic happens simultaneously as the code is written, this way, you have instant feedback between the fact that you made some changes to your code and your code was already checked and ready for use.

  • Does not add any dependencies to other assemblies.

    Using a pure DI approach, you don't add runtime dependencies to your assemblies.

  • Highest performance, including compiler and JIT optimizations.

    All generated code runs as fast as your own, in pure DI style, including compile-time and run-time optimizations. As mentioned above, graph analysis doing at compile-time, but at run-time, there are just a bunch of nested constructors, and that's it.

  • It works everywhere.

    Since a pure DI approach does not use any dependencies or the .NET reflection at runtime, it does not prevent your code from working as expected on any platform: Full .NET Framework 2.0+, .NET Core, .NET, UWP/XBOX, .NET IoT, Xamarin, Native AOT, etc.

  • Ease of use.

    The Pure.DI API is very similar to the API of most IoC/DI libraries. And it was a deliberate decision: the main reason is that programmers do not need to learn a new API.

  • Ultra-fine tuning of generic types.

    Pure.DI offers special type markers instead of using open generic types. This allows you to more accurately build the object graph and take full advantage of generic types.

  • Supports basic .NET BCL types out of the box.

    Pure.DI already supports many of BCL types like Array, IEnumerable<T>, IList<T>, ISet<T>, Func<T>, ThreadLocal, Task<T>, MemoryPool<T>, ArrayPool<T>, ReadOnlyMemory<T>, Memory<T>, ReadOnlySpan<T>, Span<T>, IComparer<T>, IEqualityComparer<T> and etc. without any extra effort.

  • Good for creating libraries or frameworks and where resource consumption is particularly critical.

    High performance, zero memory consumption/preparation overhead and no dependencies make it an ideal assistant for building libraries and frameworks.

Schrödinger's cat shows how it works CSharp

The reality is that

Cat

Let's create an abstraction

interface IBox<out T> { T Content { get; } }

interface ICat { State State { get; } }

enum State { Alive, Dead }

Here's our implementation

class CardboardBox<T> : IBox<T>
{
    public CardboardBox(T content) => Content = content;

    public T Content { get; }
}

class ShroedingersCat : ICat
{
  // Represents the superposition of the states
  private readonly Lazy<State> _superposition;

  public ShroedingersCat(Lazy<State> superposition) => _superposition = superposition;

  // Decoherence of the superposition at the time of observation via an irreversible process
  public State State => _superposition.Value;

  public override string ToString() => $"{State} cat";
}

It is important to note that our abstraction and implementation knows nothing about DI magic or any frameworks. Also, please note that an instance of type Lazy<> has been used here only as an example. However, using this type with non-trivial logic as a dependency is not recommended, so consider replacing it with some simple abstract type.

Let's glue it all together

Add a package reference to

NuGet

Package Manager

Install-Package Pure.DI

.NET CLI

dotnet add package Pure.DI

Bind abstractions to their implementations or factories, define lifetimes and other options in a class like the following:

partial class Composition
{
  // In fact, this code is never run, and the method can have any name or be a constructor, for example,
  // and can be in any part of the compiled code because this is just a hint to set up an object graph.
  // Here the setup is part of the generated class, just as an example.
  private static void Setup() => DI.Setup(nameof(Composition))
      // Models a random subatomic event that may or may not occur
      .Bind<Random>().As(Singleton).To<Random>()
      // Represents a quantum superposition of 2 states: Alive or Dead
      .Bind<State>().To(ctx =>
      {
          ctx.Inject<Random>(out var random);
          return (State)random.Next(2);
      })
      // Represents schrodinger's cat
      .Bind<ICat>().To<ShroedingersCat>()
      // Represents a cardboard box with any content
      .Bind<IBox<TT>>().To<CardboardBox<TT>>()
      // Composition Root
      .Root<Program>("Root");
}

The code above is just a chain of hints to define the dependency graph used to create a Composition class with a Root property that creates the Program composition root below. It doesn't really make sense to run this code because it doesn't do anything at runtime. So it can be placed anywhere in the class (in methods, constructors or properties) and preferably where it will not be called. Its purpose is only to check the dependency syntax and help build the dependency graph at compile time to create the Composition class. The first argument to the Setup method specifies the name of the class to be generated.

Time to open boxes!

class Program
{
  // Composition Root, a single place in an application
  // where the composition of the object graphs for an application take place
  public static void Main() => new Composition().Root.Run();

  private readonly IBox<ICat> _box;

  internal Program(IBox<ICat> box) => _box = box;

  private void Run() => Console.WriteLine(_box);
}

Root is a Composition Root here, a single place in an application where the composition of the object graphs for an application takes place. Each instance is resolved by a strongly-typed block of statements like the operator new, which are compiling with all optimizations with minimal impact on performance or memory consumption. The generated Composition class contains a Root property that allows you to resolve an instance of the Program type.

Root property
public Sample.Program Root
{
  get
  {
    Func<State> stateFunc = new Func<State>(() =>
    {
      if (_randomSingleton == null)
      {
        lock (_lockObject)
        {
          if (_randomSingleton == null)
          {
            _randomSingleton = new Random();
          }
        }
      }
      
      return (State)_randomSingleton.Next(2);      
    });
    
    return new Program(
      new CardboardBox<ICat>(
        new ShroedingersCat(
          new Lazy<Sample.State>(
            stateFunc))));    
  }
}

You can find a complete analogue of this application with top level statements here. For a top level statements application the name of generated composer is "Composer" by default if it was not override in the Setup call.

Pure.DI works the same as calling a set of nested constructors, but allows dependency injection. And that's a reason to take full advantage of Dependency Injection everywhere, every time, without any compromise!

Just try!

Download the example project code

git clone https://github.com/DevTeam/Pure.DI.Example.git

And run it from solution root folder

cd ./Pure.DI.Example
dotnet run

Examples

Basics

Lifetimes

Attributes

Base Class Library

Interception

Hints

Applications

Generated Code

Each generated class, hereinafter referred to as composition, needs to be configured. It starts with the Setup(...) method:

DI.Setup("Composition")
    .Bind<IDependency>().To<Dependency>()
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");
The following class will be generated
partial class Composition
{
    public Composition() { }

    internal Composition(Composition parent) { }

    public IService Root
    {
        get
        {
            return new Service(new Dependency());
        }
    }

    public T Resolve<T>()  { ... }

    public T Resolve<T>(object? tag)  { ... }

    public object Resolve(System.Type type) { ... }

    public object Resolve(System.Type type, object? tag) { ... }
}
Setup arguments

The first parameter is used to specify the name of the composition class. All setups with the same name will be combined to create one composition class. In addition, this name may contain a namespace, for example for Sample.Composition the composition class is generated:

namespace Sample
{
    partial class Composition
    {
        ...
    }
}

The second optional parameter can have several values to determine the kind of composition.

CompositionKind.Public

This is the default value. If this value is specified, a composition class will be created.

CompositionKind.Internal

If this value is specified, the class will not be generated, but this setup can be used for others as a base. For example:

DI.Setup("BaseComposition", CompositionKind.Internal)
    .Bind<IDependency>().To<Dependency>();

DI.Setup("Composition").DependsOn("BaseComposition")
    .Bind<IService>().To<Service>();    

When the composition CompositionKind.Public flag is set in the composition setup, it can also be the base composition for others like in the example above.

CompositionKind.Global

If this value is specified, no composition class will be created, but this setup is the base for all setups in the current project, and DependsOn(...) is not required.

Constructors

Default constructor

Everything is quite banal, this constructor simply initializes the internal state.

Argument constructor

It replaces the default constructor and is only created if at least one argument is provided. For example:

DI.Setup("Composition")
    .Arg<string>("name")
    .Arg<int>("id")
    ...

In this case, the argument constructor looks like this:

public Composition(string name, int id) { ... }

and default constructor is missing.

Child constructor

This constructor is always available and is used to create a child composition based on the parent composition:

var parentComposition = new Composition();
var childComposition = new Composition(parentComposition); 

The child composition inherits the state of the parent composition in the form of arguments and singleton objects. States are copied, and compositions are completely independent, except when calling the Dispose() method on the parent container before disposing of the child container, because the child container can use singleton objects created before it was created.

Properties

To be able to quickly and conveniently create an object graph, a set of properties is generated. These properties are called compositions roots here. The type of the property is the type of a root object created by the composition. Accordingly, each access to the property leads to the creation of a composition with the root element of this type.

Public Composition Roots

To be able to use a specific composition root, that root must be explicitly defined by the Root method with a specific name and type:

DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>("MyService");

In this case, the property for type IService will be named MyService and will be available for direct use. The result of its use will be the creation of a composition of objects with a root of type IService:

public IService MyService
{
    get
    { 
        ...
        return new Service(...);
    }
}

This is recommended way to create a composition root. A composition class can contain any number of roots.

Private Composition Roots

When the root name is empty, a private composition root is created. This root is used in these Resolve methods in the same way as public roots. For example:

DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>();
private IService Root1ABB3D0
{
    get { ... }
}

These properties have a random name and a private accessor and cannot be used directly from code. Don't try to use them. Private composition roots can be resolved by the Resolve methods.

Methods

Resolve

By default a set of four Resolve methods are generated:

public T Resolve<T>() { ... }

public T Resolve<T>(object? tag) { ... }

public object Resolve(Type type) { ... }

public object Resolve(Type type, object? tag) { ... }

These methods can resolve public composition roots as well as private roots and are useful when using the Service Locator approach when the code resolves composition roots in place:

var composition = new Composition();

composition.Resolve<IService>();

This is not recommended way to create composition roots. To control the generation of these methods, see the Resolve hint.

Dispose

Provides a mechanism for releasing unmanaged resources. This method is only generated if the composition contains at least one singleton instance that implements the IDisposable interface. To dispose of all created singleton objects, call the composition Dispose() method:

using(var composition = new Composition())
{
    ...
}
Setup hints

Setup hints

Hints are used to fine-tune code generation. Setup hints can be used as in the following example:

DI.Setup("Composition")
    .Hint(Hint.Resolve, "Off")
    .Hint(Hint.ThreadSafe, "Off")
    .Hint(Hint.ToString, "On")
    ...

In addition, setup hints can be comments before the Setup method in the form hint = value, for example:

// Resolve = Off
// ThreadSafe = Off
DI.Setup("Composition")
    .Hint(Hint.ToString, "On")
    ...
Hint Values Default C# version
Resolve On or Off On
OnNewInstance On or Off Off 9.0
OnNewInstanceImplementationTypeNameRegularExpression Regular expression .+
OnNewInstanceTagRegularExpression Regular expression .+
OnNewInstanceLifetimeRegularExpression Regular expression .+
OnDependencyInjection On or Off Off 9.0
OnDependencyInjectionImplementationTypeNameRegularExpression Regular expression .+
OnDependencyInjectionContractTypeNameRegularExpression Regular expression .+
OnDependencyInjectionTagRegularExpression Regular expression .+
OnDependencyInjectionLifetimeRegularExpression Regular expression .+
OnCannotResolve On or Off Off 9.0
OnCannotResolveContractTypeNameRegularExpression Regular expression .+
OnCannotResolveTagRegularExpression Regular expression .+
OnCannotResolveLifetimeRegularExpression Regular expression .+
OnNewRoot On or Off Off
ToString On or Off Off
ThreadSafe On or Off On
ResolveMethodModifiers Method modifier public
ResolveMethodName Method name Resolve
ResolveByTagMethodModifiers Method modifier public
ResolveByTagMethodName Method name Resolve
ObjectResolveMethodModifiers Method modifier public
ObjectResolveMethodName Method name Resolve
ObjectResolveByTagMethodModifiers Method modifier public
ObjectResolveByTagMethodName Method name Resolve
DisposeMethodModifiers Method modifier public
FormatCode On or Off Off

Resolve Hint

Determines whether to generate Resolve methods. By default a set of four Resolve methods are generated. Set this hint to Off to disable the generation of resolve methods. This will reduce class composition generation time and no private composition roots will be generated in this case. The composition will be tiny and will only have public roots. When the Resolve hint is disabled, only the public root properties are available, so be sure to define them explicitly with the Root<T>(...) method.

OnNewInstance Hint

Determines whether to generate partial OnNewInstance method. This partial method is not generated by default. This can be useful, for example, for logging:

internal partial class Composition
{
    partial void OnNewInstance<T>(ref T value, object? tag, object lifetime)            
    {
        Console.WriteLine($"'{typeof(T)}'('{tag}') created.");            
    }
}

You can also replace the created instance of type T, where T is actually type of created instance. To minimize the performance penalty when calling OnNewInstance, use the three related hints below.

OnNewInstanceImplementationTypeNameRegularExpression Hint

It is a regular expression to filter by the instance type name. This hint is useful when OnNewInstance is in the On state and you want to limit the set of types for which the method OnNewInstance will be called.

OnNewInstanceTagRegularExpression Hint

It is a regular expression to filter by the tag. This hint is useful also when OnNewInstance is in the On state and you want to limit the set of tag for which the method OnNewInstance will be called.

OnNewInstanceLifetimeRegularExpression Hint

It is a regular expression to filter by the lifetime. This hint is useful also when OnNewInstance is in the On state and you want to limit the set of lifetime for which the method OnNewInstance will be called.

OnDependencyInjection Hint

Determines whether to generate partial OnDependencyInjection method to control of dependency injection. This partial method is not generated by default. It cannot have an empty body due to the return value. It must be overridden when generated. This can be useful, for example, for interception.

// OnDependencyInjection = On
// OnDependencyInjectionContractTypeNameRegularExpression = ICalculator[\d]{1}
// OnDependencyInjectionTagRegularExpression = Abc
DI.Setup("Composition")
    ...

To minimize the performance penalty when calling OnDependencyInjection, use the three related hints below.

OnDependencyInjectionImplementationTypeNameRegularExpression Hint

It is a regular expression to filter by the instance type name. This hint is useful when OnDependencyInjection is in the On state and you want to limit the set of types for which the method OnDependencyInjection will be called.

OnDependencyInjectionContractTypeNameRegularExpression Hint

It is a regular expression to filter by the resolving type name. This hint is useful also when OnDependencyInjection is in the On state and you want to limit the set of resolving types for which the method OnDependencyInjection will be called.

OnDependencyInjectionTagRegularExpression Hint

It is a regular expression to filter by the tag. This hint is useful also when OnDependencyInjection is in the On state and you want to limit the set of tag for which the method OnDependencyInjection will be called.

OnDependencyInjectionLifetimeRegularExpression Hint

It is a regular expression to filter by the lifetime. This hint is useful also when OnDependencyInjection is in the On state and you want to limit the set of lifetime for which the method OnDependencyInjection will be called.

OnCannotResolve Hint

Determines whether to generate a partial OnCannotResolve<T>(...) method to handle a scenario where an instance which cannot be resolved. This partial method is not generated by default. It cannot have an empty body due to the return value. It must be overridden on creation.

// OnCannotResolve = On
// OnCannotResolveContractTypeNameRegularExpression = string|DateTime
// OnDependencyInjectionTagRegularExpression = null
DI.Setup("Composition")
    ...

To avoid missing bindings by mistake, use the two related hints below.

OnNewRoot Hint

Determines whether to generate a static partial OnNewRoot<TContract, T>(...) method to handle the new composition root registration event.

// OnNewRoot = On
DI.Setup("Composition")
    ...

OnCannotResolveContractTypeNameRegularExpression Hint

It is a regular expression to filter by the resolving type name. This hint is useful also when OnCannotResolve is in the On state and you want to limit the set of resolving types for which the method OnCannotResolve will be called.

OnCannotResolveTagRegularExpression Hint

It is a regular expression to filter by the tag. This hint is useful also when OnCannotResolve is in the On state and you want to limit the set of tag for which the method OnCannotResolve will be called.

OnCannotResolveLifetimeRegularExpression Hint

It is a regular expression to filter by the lifetime. This hint is useful also when OnCannotResolve is in the On state and you want to limit the set of lifetime for which the method OnCannotResolve will be called.

ToString Hint

Determine if the ToString() method should be generated. This method provides a text-based class diagram in the format mermaid. To see this diagram, just call the ToString method and copy the text to this site.

// ToString = On
DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>("MyService");
    
var composition = new Composition();
string classDiagram = composition.ToString(); 

ThreadSafe Hint

This hint determines whether the composition of objects will be created in a thread-safe manner. The default value of this hint is On. It is good practice not to use threads when creating an object graph, in which case the hint can be disabled, resulting in a slight performance gain.

// ThreadSafe = Off
DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>("MyService");

ResolveMethodModifiers Hint

Overrides modifiers of the method public T Resolve<T>().

ResolveMethodName Hint

Overrides name of the method public T Resolve<T>().

ResolveByTagMethodModifiers Hint

Overrides modifiers of the method public T Resolve<T>(object? tag).

ResolveByTagMethodName Hint

Overrides name of the method public T Resolve<T>(object? tag).

ObjectResolveMethodModifiers Hint

Overrides modifiers of the method public object Resolve(Type type).

ObjectResolveMethodName Hint

Overrides name of the method public object Resolve(Type type).

ObjectResolveByTagMethodModifiers Hint

Overrides modifiers of the method public object Resolve(Type type, object? tag).

ObjectResolveByTagMethodName Hint

Overrides name of the method public object Resolve(Type type, object? tag).

DisposeMethodModifiers Hint

Overrides modifiers of the method public void Dispose().

FormatCode Hint

Specifies whether the generated code should be formatted. This option consumes a lot of CPU resources. This hint can be useful for examining the generated code or for giving presentations, for example.

Development environment requirements

Supported frameworks

Project template

Install the DI template Pure.DI.Templates

dotnet new -i Pure.DI.Templates

Create a "Sample" console application from the template di

dotnet new di -o ./Sample

And run it

dotnet run --project Sample

Please see this page for more details about the template.

Troubleshooting

Generated files

You can set build properties to save the generated file and control where the generated files are stored. In a project file, add the element to a , and set its value to true. Build your project again. Now, the generated files are created under obj/Debug/netX.X/generated/Pure.DI/Pure.DI.SourceGenerator. The components of the path map to the build configuration, target framework, source generator project name, and fully qualified type name of the generator. You can choose a more convenient output folder by adding the element to the application's project file. For example:

<Project Sdk="Microsoft.NET.Sdk">
    
    <PropertyGroup>
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
    </PropertyGroup>
    
</Project>
Log files

You can set build properties to save the log file. In the project file, add a element to the and set the path to the log directory, and add the related element <CompilerVisibleProperty Include="PureDILogFile" /> to the to make this property visible in the source generator. To change the log level, specify the same with the PureDISeverity property, as in the example below:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <PureDILogFile>.logs\Pure.DI.log</PureDILogFile>
        <PureDISeverity>Info</PureDISeverity>
    </PropertyGroup>

    <ItemGroup>
        <CompilerVisibleProperty Include="PureDILogFile" />
        <CompilerVisibleProperty Include="PureDISeverity" />
    </ItemGroup>

</Project>

The PureDISeverity property has several options available:

Severity Description
Hidden Debug information.
Info Information that does not indicate a problem (i.e. not prescriptive).
Warning Something suspicious, but allowed. This is the default value.
Error Something not allowed by the rules of the language or other authority.

Benchmarks

Transient
Method MeanErrorStdDev MedianRatioRatioSD
'Hand Coded'0.0128 ns0.0166 ns0.0238 ns0.0000 ns
'Pure.DI composition root'3.9792 ns0.1370 ns0.3998 ns3.8511 ns
Pure.DI4.8851 ns0.1555 ns0.2375 ns4.8104 ns
'Pure.DI non-generic'7.9274 ns0.2154 ns0.5364 ns7.7121 ns
LightInject11.0228 ns0.2509 ns0.6249 ns10.8849 ns
DryIoc20.5335 ns0.4306 ns0.6954 ns20.4471 ns
SimpleInjector22.7638 ns0.4747 ns0.8799 ns22.6135 ns
MicrosoftDependencyInjection23.4549 ns0.4783 ns0.9328 ns23.4470 ns
Autofac8,924.6020 ns178.0725 ns277.2375 ns8,920.4597 ns
Singleton
Method MeanErrorStdDev MedianRatioRatioSD
'Hand Coded'0.0175 ns0.0208 ns0.0223 ns0.0025 ns
'Pure.DI composition root'4.4046 ns0.1675 ns0.4913 ns4.2524 ns
Pure.DI5.4675 ns0.1651 ns0.4867 ns5.2425 ns
'Pure.DI non-generic'7.8375 ns0.2136 ns0.3262 ns7.7274 ns
DryIoc19.3453 ns0.4133 ns0.7964 ns19.0213 ns
SimpleInjector21.9625 ns0.4665 ns0.5900 ns21.9175 ns
MicrosoftDependencyInjection23.4351 ns0.3397 ns0.2837 ns23.3851 ns
LightInject31.4078 ns0.6519 ns1.0150 ns30.9192 ns
Autofac6,292.0519 ns123.0989 ns184.2486 ns6,275.1659 ns
Func
Method MeanErrorStdDevMedianRatioRatioSD
'Hand Coded'79.40 ns2.333 ns6.842 ns76.23 ns1.000.00
'Pure.DI composition root'83.99 ns2.211 ns6.483 ns80.88 ns1.070.12
Pure.DI84.28 ns2.507 ns7.314 ns81.52 ns1.070.11
'Pure.DI non-generic'91.09 ns3.432 ns10.011 ns88.74 ns1.160.17
DryIoc109.17 ns3.266 ns9.579 ns106.92 ns1.380.16
LightInject441.57 ns8.785 ns21.877 ns431.23 ns5.560.58
Autofac8,645.17 ns170.212 ns319.699 ns8,604.62 ns109.9010.55
Array
Method MeanErrorStdDevMedianRatioRatioSD
'Hand Coded'79.99 ns2.698 ns7.954 ns76.70 ns1.000.00
'Pure.DI composition root'88.79 ns2.717 ns7.884 ns86.02 ns1.120.15
Pure.DI92.69 ns3.242 ns9.509 ns88.42 ns1.170.15
'Pure.DI non-generic'93.28 ns2.674 ns7.884 ns90.48 ns1.180.14
LightInject99.37 ns3.547 ns10.347 ns97.70 ns1.250.18
DryIoc108.40 ns3.156 ns9.257 ns104.45 ns1.370.17
Autofac10,368.08 ns203.428 ns429.098 ns10,342.65 ns130.1215.68
Enum
MethodMeanErrorStdDevMedianRatioRatioSD
'Hand Coded'188.5 ns3.90 ns11.51 ns183.6 ns1.000.00
Pure.DI188.7 ns3.82 ns10.12 ns184.9 ns1.000.08
'Pure.DI composition root'199.6 ns4.25 ns12.52 ns195.1 ns1.060.09
'Pure.DI non-generic'207.1 ns4.17 ns11.06 ns203.2 ns1.100.07
LightInject213.0 ns4.65 ns13.71 ns206.0 ns1.130.10
DryIoc222.9 ns4.56 ns13.38 ns217.4 ns1.190.11
MicrosoftDependencyInjection234.3 ns5.20 ns15.17 ns228.7 ns1.250.10
Autofac9,865.7 ns196.14 ns240.87 ns9,755.9 ns51.194.06
Benchmarks environment

BenchmarkDotNet v0.13.6, Windows 10 (10.0.19045.3208/22H2/2022Update)
Intel Core i7-10850H CPU 2.70GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 7.0.304
  [Host]     : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2
  DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2