StrongInject Documentation

repository·main·Indexed 21 days ago

https://github.com/yairhalberstadt/stronginject

A compile-time dependency injection library for .NET using Roslyn Source Generators. It provides compile-time safety, high performance without reflection, and full support for asynchronous initialization and disposal. The library includes integration patterns for ASP.NET Core, Xamarin.Forms, WPF, and console applications, featuring support for scoped execution via Run/RunAsync and manual lifetime control through Owned<T> wrappers.

Tokens
12.6K
Snippets
22
Records
61
Agent score
73%

What's inside StrongInject

  1. What is a Container in StrongInject

    main

    A container acts as a factory that provides instances of a type on demand and manages their disposal.

    To create a container, you must inherit from either:

    • IContainer<T>: For synchronous resolution.
    • IAsyncContainer<T>: Required if resolution must be asynchronous.

    Important: You must ensure the container is capable of resolving T by providing suitable [Register] attributes. If no registration is found, you will encounter a compile-time error.

    using StrongInject;
    
    [Register(typeof(A))]
    public partial class MyContainer : IContainer<A> { }
    
    public class A : IDisposable { public void Dispose() { } }
    
    var myContainer = new MyContainer();
  2. Register dependencies using Factory Methods

    main

    You can register a method as a provider for a type by applying the [Factory] attribute to it. StrongInject will resolve all parameters of the method and call it to create the instance.

    Key Behaviors:

    • Return Type Mapping: The method is registered as a provider for its return type. If the return type is Task<T> or ValueTask<T>, it is registered as a provider for T and must be resolved asynchronously.
    • Scope Control: The Scope parameter in the [Factory] attribute determines the lifetime of the instance created by the method.
    • Parameter Resolution: All parameters defined in the factory method signature must be resolvable by the container.
    [Factory(Scope.SingleInstance)]
    private MyService CreateMyService(IDependency dep) => new MyService(dep);
  3. Manage lifetimes and integration patterns in StrongInject

    main

    The ASP.NET Core sample highlights several advanced patterns for using StrongInject:

    • Lifetime Control via Scopes: Use Scopes to differentiate between lifetimes. For example, Controllers should typically be InstancePerDependency, while caches should be SingleInstance.
    • Two-way DI Integration: You can pass the IServiceProvider as a parameter to the StrongInject Container. This allows StrongInject to resolve types registered in the standard Microsoft DI container, such as ILogger<T>.
    • Generic Factory Registration: You can use generic factory methods to register types like ILogger<T> for all T simultaneously.
    • Handling Async Resolution: While StrongInject supports asynchronous resolution, Microsoft.Extensions.DependencyInjection does not. If a dependency (like a DatabaseUsersCache) requires asynchronous preparation, you should design the consuming service to handle asynchronous requests rather than attempting to resolve the dependency asynchronously through the standard DI container.
  4. What are Decorators in StrongInject

    main

    A Decorator does not provide a new instance of a type; instead, it wraps or modifies an existing underlying instance that is resolved through normal registration.

    If multiple decorators are registered for the same type, they are applied in an "onion style" (wrapping one inside another). While the application order is deterministic, it is an implementation detail and should not be relied upon for logic.

    Limitations:

    • Decorators are not applied to parameters of delegates.
    • Decorators are not applied to [Instance] fields or properties when Options.DoNotDecorate is applied.
  5. Configure registration types with Registration Options

    main

    The Options enum is a [Flags] enum used to modify how types are registered in the container. You can combine multiple options using the bitwise OR (|) operator.

    To simplify syntax, you can use using static StrongInject.Options; to reference members directly without the Options. prefix.

    using static StrongInject.Options;
    
    // Example of combining options
    var options = UseAsFactory | DoNotDecorate;
  6. The StrongInject resolution order

    main

    When StrongInject needs to resolve a type, it checks potential providers in a specific sequence and stops as soon as it finds a provider capable of supplying the instance. The order is:

    1. Delegate Parameters: If the provider is a delegate, StrongInject uses the delegate's parameters as dependencies for the return type. Inner delegate parameters override outer ones of the same type. Note: A parameter can only resolve the exact same type; it cannot resolve base classes or interface implementations.
    2. Non-Generic Registrations: StrongInject checks for specific non-generic registrations. If a single 'best' registration is found, it is used. If multiple 'best' registrations exist, an error is produced.
    3. Generic Registrations: If no non-generic registration matches, StrongInject checks for generic registrations that can be satisfied by substituting the correct type parameters.
    4. Delegate Types: If the requested type is a delegate, StrongInject automatically creates one. If the return type is Task<T> or ValueTask<T>, it creates an async delegate.
    5. Array Types: If the requested type is an array, StrongInject finds all applicable non-generic and generic registrations for the element type, resolves all of them, and returns an array containing all instances.
  7. Configure [Instance] registration options

    main

    The [Instance] attribute accepts an Options parameter to customize how the instance is registered.

    Key capabilities include:

    1. Multiple Registrations: Register the instance as its base classes or interfaces, not just its concrete type.
    2. Factory Support: If the instance implements IFactory<T> or IAsyncFactory<T>, it can be registered as T, along with T's base classes, interfaces, and factories.
    3. Decoration Control: By default, instances can be decorated. To opt out of decoration, use Options.DoNotDecorate.

    Note on Disposal: StrongInject will not dispose of instance fields or properties. This design choice allows modules containing these instances to be safely shared among multiple containers.

    // Example of opting out of decoration
    [Instance(Options.DoNotDecorate)] public static readonly MyService _service = new MyService();
  8. Understanding and resolving 'Best Registration' errors

    main

    When resolving a single instance of a type T, StrongInject requires that there is exactly one best registration for that type. If multiple registrations exist and none qualify as the 'best', StrongInject will throw an error.

    How 'Best Registration' is determined:

    1. Direct vs. Imported: A registration declared directly on a module or container is always better than registrations declared on other modules that are imported by it.
    2. Uniqueness: A registration is only the best registration if it is strictly better than all other available registrations for that type. If there is a tie between multiple 'better' registrations, there is no best registration.

    Common Scenarios:

    • Container vs. Modules: If a Container and ModuleA both define a registration for SomeInterface, the one on the Container is the best registration.
    • Ambiguous Imports: If a Container imports both ModuleA and ModuleB, and both modules define a registration for SomeInterface, resolving SomeInterface will error because neither is 'better' than the other.
    • Hierarchical Imports: If Container imports ModuleA, and ModuleA imports ModuleB, then ModuleA's registration is the best registration.

    How to fix 'No Best Registration' errors:

    • Option 1 (Simplest): Add a single registration for the type directly to the Container. This will override all imported registrations.
    • Option 2 (Refactoring): If the container already has multiple registrations for a type, move those registrations into a separate module and import that module instead.