DryIoc Documentation

repository·master·Indexed 22 days ago

https://github.com/dadhi/dryioc

A high-performance, small, and full-featured Inversion of Control (IoC) container for .NET. It supports .NET 4.5, .NET Standard 2.0/2.1, and .NET 6.0, 8.0, and 9.0. Key features include advanced registration management, open generics, service lifetime control (Transient, Singleton, Scoped), nested decorators, and cycle detection. The ecosystem includes DryIocAttributes for MEF-style attribute-based injection and DryIocZero for compile-time generated factories.

Tokens
59K
Snippets
167
Records
222
Agent score
77%

What's inside DryIoc

  1. Core features of DryIoc

    master

    DryIoc is a high-performance IoC container for .NET that provides extensive capabilities for dependency management. Key features include:

    • Registration Management: Map services to implementation types, including support for registering once, replacing, or removing registrations. You can also register delegate factories, existing service instances, or perform batch registrations from assemblies.
    • Service Identification: Use service keys of any type to identify registrations, allowing multiple non-keyed implementations for a single service.
    • Advanced Injection: Supports constructor parameter injection, optional property/field injection, and injection into existing objects. It also supports static and instance factory methods.
    • Open Generics: Full support for open-generics, including type constraints, variance, and complex nesting.
    • Service Lifetimes: Control service lifetime via Reuse and lifetime scoping, including nested disposable scopes, ambient scope context, and various Reuse types like Transient, Singleton, and Scoped (with support for scoping to specific service ancestors).
    • Decorators and Wrappers:
      • Decorators: Supports nested decorators with relative order control, generic/non-generic decorators, and decorators with different Reuse settings than the decorated service.
      • Wrappers: Supports service collections (T[], IEnumerable<T>, LazyEnumerable<T>, ICollection<T>), single service wrappers (Lazy<T>, Func<T>, Meta<TMetadata, T>, Tuple<TMetadata, T>, KeyValuePair<TKey, T>), and parameter currying via Func<TArg, T>.
  2. DryIoc Performance and Benchmarks

    master

    DryIoc is designed for high performance and low memory allocation. In realistic scenarios involving a unit-of-work scope and an object graph of 40 dependencies 4 levels deep, DryIoc demonstrates significantly lower latency and allocation compared to other IoC containers like Autofac, Lamar, or Grace.

    Key performance characteristics include:

    • Cold Start: Fast registration and initial resolution.
    • Hot Run: Extremely low latency when opening scopes and resolving services repeatedly.
    • Memory Efficiency: Low Gen0/Gen1/Gen2 allocations and small memory footprint.
  3. What is DryIoc.Messages?

    master
    The DryIoc.Messages namespace provides simple and extensible abstractions for the Message (Request), Response, and Handler pattern. It is designed to be similar to the MediatR pattern, allowing you to implement decoupled communication between components. By leveraging DryIoc's feature composition, you can achieve MediatR-like functionality directly within the DryIoc container without needing to pull in additional external libraries.
  4. Select factory methods based on resolution context

    master

    You can use Made.Of to provide a predicate that selects a constructor or factory method dynamically based on the current resolution context. The predicate receives a request object (req) which provides information about the resolution process.

    Common properties of the request object include:

    • req.IsResolutionRoot: True if the service is being resolved directly (not as a dependency of another service).
    • req.Parent: A collection of services that are currently being resolved as part of the parent object graph. You can inspect these to check for specific ServiceKey values or types.

    This is useful for implementing complex logic where the instantiation strategy changes depending on whether the service is a top-level dependency or part of a specific parent's graph.

    c.Register<IFoo>(made: Made.Of(req =>
    {
        // If being injected into a parent with a specific service key
        if (req.Parent.Any(p => "special".Equals(p.ServiceKey)))
            return typeof(Consumer).GetMethod(nameof(Consumer.GetMyFoo), BindingFlags.Public | BindingFlags.Static);
    
        // If being resolved as a root
        if (req.IsResolutionRoot)
            return typeof(Foo).GetConstructors().FirstOrDefault(c => c.GetParameters().Length == 0);
    
        // Default fallback
        return typeof(Foo).GetConstructors().First(c => c.GetParameters().Any(p => p.ParameterType == typeof(FooNameProvider)));
    }));
  5. How MefAttributedModel and DryIoc.Attributes relate

    master

    The DryIoc.MefAttributedModel extension depends on the DryIoc.Attributes package.

    • DryIoc.Attributes: Re-defines MEF attributes for platforms lacking System.ComponentModel.Composition and extends them to support DryIoc features like reuses, decorators, and wrappers.
    • DryIoc.MefAttributedModel: Provides the runtime logic to use these attributes for DI configuration and service registration.

    Why the separation? You can use DryIoc.Attributes to mark types for export without needing the full MefAttributedModel runtime. This is useful for compile-time tools like DryIocZero, which scan attributes and generate factory delegates at compile-time, eliminating the need for heavy reflection at runtime.

  6. How the DryIoc Resolution Pipeline works

    master

    When calling container.Resolve<X>() for a non-singleton service, DryIoc follows a multi-stage lifecycle to balance initial speed with subsequent performance:

    1. First Call (Discovery & Interpretation): DryIoc discovers and constructs the expression tree for the object graph of X. It then interprets this expression to obtain the service. This interpreted expression is then cached.
    2. Second Call (Compilation & Replacement): DryIoc finds the cached expression, compiles it into a high-performance delegate, replaces the cached expression with this delegate, and then invokes it.
    3. Third Call (Invocation): DryIoc finds the cached delegate and invokes it directly.

    Note on Singletons: Singletons are always interpreted rather than compiled because they are created once, and one-time interpretation is faster than the overhead of compilation plus invocation. They are injected as ConstantExpression unless wrapped in Func or Lazy wrappers.

  7. Lock-free service creation in DryIoc v5

    master

    In DryIoc v5, the container is fully lock-free on modern platforms.

    Instead of using traditional lock statements for the creation of scoped and singleton services, DryIoc uses a spin-wait based approach to ensure that service creation happens exactly once.

    *Note: For older platforms (e.g., < .NET Standard 2.0 or < .NET 4.5), DryIoc v5 may still use lock for compatibility, but on modern runtimes, it is lock-free.

  8. What are Wrappers in DryIoc

    master

    A Wrapper in DryIoc is a data structure that operates on one or more registered services. They allow you to change how a service is provided to a consumer (e.g., delaying instantiation or providing multiple implementations).

    Wrappers fall into two categories:

    1. Open-generic types: These use a generic argument to identify the service type being wrapped. Examples include Func<TService>, Lazy<TService>, and IEnumerable<TService>. Note that an open-generic wrapper with multiple type arguments (like Func<TArg0, TArg1, TService>) wraps exactly one service type (TService).
    2. Non-generic types: The wrapped service is identified via the RequiredServiceType. An example is LambdaExpression.

    Key Rules:

    • Nesting: Wrappers are composable (e.g., IEnumerable<Lazy<TService>>).
    • Overriding: If you explicitly register a wrapper type (like Func<>) as a normal service, it will override the default wrapper behavior.
    • Single Service Limit: A single wrapper cannot wrap multiple different service types simultaneously.
    // Example of a class using a wrapper in its constructor
    class B
    {
        public B(Lazy<A> a) { }
    }
    
    // Usage in container
    var container = new Container();
    container.Register<A>();
    container.Register<B>();
    
    // Lazy is available without explicit registration of the wrapper itself!
    var b = container.Resolve<B>();
  9. Handle DryIoc exceptions with ContainerException

    master

    When an error occurs during registration or resolution, DryIoc throws a ContainerException (derived from InvalidOperationException). This ensures that errors are explicitly attributed to the container rather than the application code.

    To programmatically identify and handle specific error types, inspect the Error property, which provides an error code. All available error codes and their corresponding messages are defined in the DryIoc.Error class.

    try
    {
        var service = container.Resolve<IMyService>();
    }
    catch (ContainerException ex)
    {
        if (ex.ErrorName == Error.NameOf(Error.UnableToResolveUnknownService))
        {
            // Handle specific error
        }
    }
  10. Understand Reuse (Lifetimes) in DryIoc

    master

    Reuse (also known as lifestyle) determines how many instances of a service are created and how they are shared among consumers. A service created with a specific reuse is shared between its consumers until its lifetime ends.

    DryIoc provides several basic reuse types:

    • Reuse.Transient: A new instance is created every time the service is resolved or injected.
    • Reuse.Singleton: A single instance is created per container and lives until the container is disposed.
    • Reuse.Scoped: An instance is shared within a specific Scope.
    • Reuse.ScopedOrSingleton: A hybrid approach (details in subsequent sections).

    You can also implement the IReuse interface to create custom reuse logic.

    container.Register<IFoo, Foo>(Reuse.Transient);
    container.Register<IFoo, Foo>(Reuse.Singleton);
    container.Register<IFoo, Foo>(Reuse.Scoped);
  11. Use Func<A> for deferred service creation

    master

    The Func<A> wrapper delegates the creation of A to the user code.

    Key Behaviors:

    • Default Behavior: By default, it is injected as an inline service creation (e.g., new B(() => new A())). In this mode, A must be available at the moment Func<A> is resolved, and it does not permit recursive dependencies.
    • Resolution Call Mode: You can change the behavior to use a Resolve call, which permits recursive dependencies. This is done via the asResolutionCall setup option.
    • Registration Requirement: Like Lazy<A>, the wrapped type A must be registered before the Func<A> is resolved, unless using specific global rules or placeholders.
    // Default: inline creation (no recursion allowed)
    // container.Resolve<Func<A>>();
    
    // Alternative: uses Resolve call (recursion permitted)
    container.Register<A>(setup: Setup.With(asResolutionCall: true));
  12. Handling generic variance in collections

    master

    When resolving a collection of generic types (e.g., using ResolveMany<T>()), DryIoc includes variance-compatible types by default. For example, if IHandler<out T> is covariant, resolving IHandler<A> will return implementations of IHandler<B> where B is a subtype of A.

    If you want to disable this behavior and only receive exact matches, configure the container with rules.WithoutVariantGenericTypesInResolvedCollection().

    // Default behavior: includes variance-compatible types
    var container = new Container();
    container.Register<IHandler<A>, AHandler>();
    container.Register<IHandler<B>, BHandler>();
    
    var aHandlers = container.ResolveMany<IHandler<A>>();
    // Result contains both AHandler and BHandler if IHandler is covariant
    
    // To disable variance matching:
    var containerStrict = new Container(rules =>
        rules.WithoutVariantGenericTypesInResolvedCollection());
    
    var aHandlersStrict = containerStrict.ResolveMany<IHandler<A>>();
    // Result contains only AHandler