Stashbox Dependency Injection Framework

repository·master·Indexed 41 days ago

https://github.com/z4kn4fein/stashbox

A high-performance, lightweight dependency injection framework for .NET, compatible with .NET Core, Xamarin, and Unity. It features advanced capabilities including child containers for multi-tenancy, a robust decorator pattern for extending service functionality, and support for open-generic types. Stashbox provides extensive extensions for ASP.NET Core, ASP.NET, OWIN, WCF, Hangfire, and various mocking libraries.

Tokens
36.9K
Snippets
118
Records
132
Agent score
100%

What's inside Stashbox

  1. Overview of Stashbox dependency injection

    master

    Stashbox is a lightweight, fast, and portable dependency injection (DI) framework designed for .NET-based solutions. It is built to encourage loosely coupled applications and simplify the construction of hierarchical object structures.

    Key features include:

    • Performance: Fast, thread-safe, and lock-free operations with a small memory footprint.
    • Developer Experience: An easy-to-use Fluent configuration API and fast feedback on registration/resolution issues.
    • Safety: Automatically tracks the dependency tree to detect cycles and provides warnings for misconfigurations.
  2. What is a Scope in Stashbox?

    master

    A scope is Stashbox's implementation of the unit-of-work pattern. It encapsulates a specific unit of work used to resolve and store instances.

    Key behaviors:

    • Singleton-per-scope: When a scoped service is resolved or injected, the scope ensures it is instantiated only once within that scope's lifetime.
    • Automatic Cleanup: When a scope is disposed, it automatically cleans up resources by disposing every tracked IDisposable or IAsyncDisposable instance it resolved.
    • Use Case: A common example is a web application where each HTTP request is assigned its own unique scope. When the request ends, the scope is closed and all scoped instances are disposed.
  3. Register multiple decorators for a service

    master

    You can register multiple decorators for a single service. The decoration order follows the registration order: the first decorator registered wraps the base service, and subsequent decorators wrap the resulting decorated service.

    container.Register<IEventProcessor, GeneralProcessor>();
    container.RegisterDecorator<IEventProcessor, LoggerProcessor>();
    container.RegisterDecorator<IEventProcessor, ValidatorProcessor>();
    
    // Resulting structure: ValidatorProcessor -> LoggerProcessor -> GeneralProcessor
    var processor = container.Resolve<IEventProcessor>();
  4. Handle contravariance and covariance in generic interfaces

    master

    Stashbox supports .NET variance for generic interfaces using the in and out keywords. Variance checking is enabled by default.

    • Contravariance (in): Allows argument types that are less derived than the defined generic parameter. For example, an IEventHandler<IGeneralEvent> can be resolved as an IEventHandler<IUpdatedEvent> if the interface is marked in.
    • Covariance (out): Allows argument types that are more derived than the defined generic parameter. For example, an IEventHandler<IUpdatedEvent> can be resolved as an IEventHandler<IGeneralEvent> if the interface is marked out.

    Note: You can disable variance checking via container configuration options.

    // Contravariance Example
    interface IEventHandler<in TEvent> { } 
    interface IGeneralEvent { }
    interface IUpdatedEvent : IGeneralEvent { }
    class GeneralEventHandler : IEventHandler<IGeneralEvent> { }
    
    container.Register<IEventHandler<IGeneralEvent>, GeneralEventHandler>();
    // Returns GeneralEventHandler because IGeneralEvent is less derived than IUpdatedEvent
    IEnumerable<IEventHandler<IUpdatedEvent>> eventHandlers = container.ResolveAll<IEventHandler<IUpdatedEvent>>();
    
    // Covariance Example
    interface IEventHandler<out TEvent> { } 
    // Returns both GeneralEventHandler and UpdatedEventHandler because IUpdatedEvent is more derived than IGeneralEvent
    IEnumerable<IEventHandler<IGeneralEvent>> eventHandlers = container.ResolveAll<IEventHandler<IGeneralEvent>>();
  5. Understand Service type vs Implementation type

    master

    In Stashbox, a registration maps a Service type to an Implementation type.

    • Service type: Usually an interface or an abstract class used for service resolution or dependency injection.
    • Implementation type: The actual concrete type registered to the Service type. It must implement or extend the Service type.

    To map a service, use Register<TService, TImplementation>(). To retrieve it, use Resolve<TService>().

    // Registration
    container.Register<IService, Implementation>();
    
    // Resolution
    IService service = container.Resolve<IService>(); // returns Implementation
  6. How Stashbox resolves dependencies

    master

    Stashbox manages a collection of registered services. When a service is requested, the container:

    1. Searches for a matching registration with the same service type as the requested type.
    2. Scans the implementation type's available constructors.
    3. Selects the constructor with the most arguments that it can successfully resolve by matching argument types to other existing registrations.
    4. Recursively repeats this process for every injectable dependency until a complete resolution tree is built.
    5. Instantiates the hierarchical object structure to return the requested service.
  7. Best practice: Avoid using the resolver parameter inside factories

    master

    Using the dependency resolver parameter inside a factory delegate creates a "black box" for the container. This can bypass critical validations for lifetime (e.g., captive dependencies) and circular dependencies because the container cannot see the resolution tree inside the delegate.

    Recommended Pattern: Instead of using the resolver, let the container inject the required dependencies directly into the factory delegate as parameters. This ensures the resolution tree remains stable and all parameters are validated.

    // RECOMMENDED: Dependencies are injected as parameters
    container.Register<IEventProcessor, EventProcessor>(options => options
        .WithFactory<ILogger, IEventValidator>((logger, validator) => 
            new EventProcessor(logger, validator)));
    
    // AVOID: Using the resolver inside the delegate
    container.Register<IEventProcessor, EventProcessor>(options => options
        .WithFactory(resolver => 
            new EventProcessor(resolver.Resolve<ILogger>(), resolver.Resolve<IEventValidator>())));
  8. Inject default values for primitives with Default Value Injection

    master

    When WithDefaultValueInjection() is enabled, the container will resolve unknown primitive dependencies using their language-defined default values (e.g., null for strings, 0 for integers).

    Note: For unknown reference types, they are resolved to null only when they are properties or fields.

    class Person 
    {
        public Person(string name, int age) { }
    }
    
    var container = new StashboxContainer(config => config
        .WithDefaultValueInjection());
    
    // name will be null, age will be 0
    var person = container.Resolve<Person>();
  9. Register and resolve open-generic types

    master

    Open-generic types (generic types without specific arguments, e.g., IValidator<>) allow you to register a single generic definition that Stashbox uses to satisfy various closed-generic requests.

    When a matching closed service type is requested, Stashbox automatically binds the type parameters to construct the appropriate closed-generic implementation.

    Priority Rule: A registered closed-generic type always takes priority over an open-generic type during service selection.

    // Register the open-generic definition
    container.Register(typeof(IValidator<>), typeof(Validator<>));
    
    // Stashbox binds the type parameters automatically
    IValidator<User> userValidator = container.Resolve<IValidator<User>>();
    IValidator<Role> roleValidator = container.Resolve<IValidator<Role>>();
    
    // Priority Example: Closed-generic registration overrides open-generic
    container.Register<IValidator<User>, UserValidator>();
    container.Register(typeof(IValidator<>), typeof(Validator<>));
    // UserValidator will be returned instead of Validator<User>
    IValidator<User> validator = container.Resolve<IValidator<User>>();
  10. Enable conventional resolution by parameter or member name

    master

    Conventional resolution allows the container to treat member and method parameter names as their dependency identifiers. This acts as an implicit dependency binding on every class member. To use this, you must enable it during container configuration using .TreatParameterAndMemberNameAsDependencyName().

    When enabled, the container attempts a named resolution for each dependency based on the name of the parameter, property, or field.

    // Enable conventional resolution
    new StashboxContainer(options => options
        .TreatParameterAndMemberNameAsDependencyName());
    
    // Example: Parameter name 'consoleLogger' matches the registered name
    class DbBackup : IJob
    {
        public DbBackup(ILogger consoleLogger) { }
    }
    
    container.Register<ILogger, ConsoleLogger>("consoleLogger");
    container.Register<IJob, DbBackup>();
    
    IJob job = container.Resolve<IJob>();
  11. Use ICompositionRoot to organize service wiring

    master

    An ICompositionRoot is an entry point used to define how services are wired together for a specific component or assembly. This helps modularize the registration logic.

    Implementation: Implement ICompositionRoot and define your registration logic inside the Compose(IStashboxContainer container) method. Your implementation can itself have dependencies that the container resolves.

    Usage:

    • container.ComposeBy<TRoot>(): Wires up a specific composition root implementation.
    • container.ComposeAssembly(Assembly assembly): Automatically finds and executes all ICompositionRoot implementations within the specified assembly.
    • container.ComposeBy<TRoot>(object dependencyOverride): Wires up a specific root while providing an override for its dependencies.
    class ExampleRoot : ICompositionRoot
    {
        public ExampleRoot(IDependency rootDependency) { }
    
        public void Compose(IStashboxContainer container)
        {
           container.Register<IServiceA, ServiceA>();
           container.Register<IServiceB, ServiceB>();
        }
    }
    
    // Usage patterns:
    container.ComposeBy<ExampleRoot>(); // Single
    container.ComposeAssembly(typeof(IServiceA).Assembly); // Assembly
    container.ComposeBy<ExampleRoot>(new CustomRootDependency()); // Override
  12. How child containers and parent-child relationships work

    master

    Child containers allow you to build hierarchical relationships where a child container can contain a different subset of services than its parent.

    Key behaviors:

    • Resolution Fallback: If a service is missing in the child, Stashbox asks the parent. If found in the parent, the parent returns the registration, and the resolution process jumps back to the child to resolve dependencies.
    • Overriding: Registrations in a child container with the same service type will override the parent's services.
    • Lifecycle: By default, disposing a parent container also disposes all its child containers. You can decouple this by setting attachToParent: false in CreateChildContainer().
    • Multi-tenancy: Child containers are the foundation for the ASP.NET Core multi-tenant extension.
    interface IDependency {}
    class B : IDependency {}
    class C : IDependency {}
    class A { public A(IDependency dependency) { } }
    
    using (var container = new StashboxContainer())
    {
        container.Register<A>();
        container.Register<IDependency, B>();
    
        var child = container.CreateChildContainer();
        child.Register<IDependency, C>();
        
        // 'A' is resolved from parent but gets 'C' (from child) as its dependency
        A fromChild = child.Resolve<A>();
    
        // 'A' gets 'B' (from parent) as its dependency
        A fromParent = container.Resolve<A>();
    }