LightInject Documentation

repository·master·Indexed 20 days ago

https://github.com/seesharper/lightinject

A high-performance dependency injection container for .NET featuring dynamic code compilation and lock-free service lookups. It supports various service lifetimes (Transient, PerScope, PerContainer, PerRequest), named services, and async/await compatible scope management via PerLogicalCallContextScopeManagerProvider. The library can be installed as a binary or as a single-file source (LightInject.cs) via NuGet.

Tokens
10.5K
Snippets
34
Records
34
Agent score
21%

What's inside LightInject

  1. Optimize performance with container.Compile()

    master

    LightInject uses dynamic code compilation (Reflection.Emit or expression trees) to generate high-performance delegates for service creation. While services are compiled on first request, this can cause lock contention in highly concurrent environments during startup.

    To avoid this, use container.Compile() during application startup to pre-compile services.

    Important Considerations:

    • Root Services: Only services directly requested from the container (root services) get their own dedicated delegate. Dependencies of root services are embedded within the root service's delegate.
    • Selective Compilation: You can use a predicate to compile only specific services: container.Compile(sr => sr.ServiceType == typeof(Foo));.
    • Open Generics: You cannot compile open generic services (e.g., List<>) because the arguments are unknown. You must specify the arguments explicitly: container.Compile<Foo<int>>();.
    // Compile all registered services
    container.Compile();
    
    // Compile specific services using a predicate
    container.Compile(sr => sr.ServiceType == typeof(Foo));
    
    // Compile a specific closed generic service
    container.Compile<Foo<int>>();
  2. Use ICompositionRoot for organized registrations

    master

    An ICompositionRoot is an interface used to group service registrations naturally. When scanning an assembly, LightInject looks for implementations of ICompositionRoot and executes their Compose method.

    Important: Any services in the target assembly that are not explicitly registered within a composition root will NOT be registered.

    To improve scanning performance in large assemblies, you can use the CompositionRootType attribute to help LightInject locate the root.

    // Define a composition root
    public class SampleCompositionRoot : ICompositionRoot
    {
        public void Compose(IServiceRegistry serviceRegistry)
        {
            serviceRegistry.Register(typeof(IFoo), typeof(Foo));
        }
    }
    
    // Mark the assembly with the composition root type
    [assembly: CompositionRootType(typeof(SampleCompositionRoot))]
    
    // Explicitly execute a composition root
    container.RegisterFrom<SampleCompositionRoot>();
    // Or pass an instance
    container.RegisterFrom(new SampleCompositionRoot());
  3. Use the Composite pattern with IEnumerable<T> injection

    master

    The Composite pattern allows a class to implement an interface and delegate method calls to a collection of other classes implementing that same interface.

    LightInject supports this pattern by allowing you to inject IEnumerable<T> into a constructor. Crucially, if the composite class itself implements T, LightInject detects this recursive dependency and automatically excludes the composite instance from the injected collection to prevent infinite loops.

    public class FooWithEnumerableIFooDependency : IFoo
    {
        public IEnumerable<IFoo> FooList { get; private set; }
    
        public FooWithEnumerableIFooDependency(IEnumerable<IFoo> fooList)
        {
            FooList = fooList;
        }
    }
    
    // Registration
    container.Register(typeof(IFoo), typeof(Foo), "Foo");
    container.Register(typeof(IFoo), typeof(AnotherFoo), "AnotherFoo");
    container.Register(typeof(IFoo), typeof(FooWithEnumerableIFooDependency));
    
    // The FooList will contain Foo and AnotherFoo, but NOT FooWithEnumerableIFooDependency
    var instance = (FooWithEnumerableIFooDependency)container.GetInstance<IFoo>();
  4. Use Typed Factories for better expressiveness

    master

    A typed factory is a class that wraps a function factory. It provides better expressiveness and type safety for consumers.

    Best Practice: Register typed factories with PerContainerLifetime unless there is a specific reason to use another lifetime.

    Typed factories can also handle IDisposable services by providing a Release method, which avoids exposing the underlying IDisposable requirement directly in the service interface (preventing leaky abstractions).

    // Define the factory interface
    public interface IFooFactory
    {
        IFoo GetFoo(int value);
        void Release(IFoo foo);
    }
    
    // Implement the factory
    public class FooFactory : IFooFactory
    {
        private Func<int, IFoo> createFoo;
        public FooFactory(Func<int, IFoo> createFoo) => this.createFoo = createFoo;
    
        public IFoo GetFoo(int value) => createFoo(value);
    
        public void Release(IFoo foo)
        {
            if (foo is IDisposable disposable) disposable.Dispose();
        }
    }
    
    // Register and use
    container.Register<int, IFoo>((factory, value) => new Foo(value));
    container.Register<IFooFactory, FooFactory>(new PerContainerLifetime());
    var factory = container.GetInstance<IFooFactory>();
    var foo = factory.GetFoo(42);
    factory.Release(foo);
  5. How scopes and lifetimes work in LightInject

    master

    A Scope is used to track services created within a specific boundary. This is essential for managing service lifecycles, ensuring that certain services are shared within a scope and disposed of when the scope ends.

    Key Lifetimes

    • PerScopeLifetime: Ensures only a single instance of a service is created within a specific scope. Even if requested multiple times, the same instance is returned. The instance is automatically disposed when the scope is disposed.
    • PerRequestLifetime: Behaves like a transient service (a new instance is provided every time it is requested), but with the added benefit that instances are disposed when the scope ends. Note: This is a technical term and has no inherent relation to web requests.

    Managing Scopes

    In many frameworks (like ASP.NET Core), scopes are managed automatically (e.g., one scope per web request). However, you can manage them manually using BeginScope().

    Best Practice: Avoid using the container to resolve services inside a scope (which relies on an ambient/current scope). Instead, resolve services directly from the Scope instance for better performance and safety.

    Manual Scope Example:

    using (var scope = container.BeginScope())
    {
        var dbConnection = scope.GetInstance<IDbConnection>();  
    }
    // Registering a scoped service
    container.RegisterScoped<IDbConnection>(factory => new ProviderSpecificConnection());
    
    // Recommended way to use a scope
    using (var scope = container.BeginScope())
    {
        var dbConnection = scope.GetInstance<IDbConnection>();  
    }
  6. Apply the Decorator pattern using Decorate()

    master

    A decorator is a class that implements the same interface as the type it is decorating and accepts the target instance as a constructor argument. You apply decorators using the Decorate method.

    Key Behaviors:

    • Nesting: Decorators can be nested. They are applied in the same sequence as they are registered.
    • Predicates: If multiple services implement the same interface, you can use a predicate to apply a decorator only to specific implementations (e.g., matching a specific ServiceName).
    • Generics: Decorators can be applied to open generic types.
    • Dependencies: Decorators can have their own dependencies in addition to the target instance. These can be resolved implicitly by the container or explicitly using a function factory.
    // Basic usage
    container.Register<IFoo, Foo>();
    container.Decorate(typeof(IFoo), typeof(FooDecorator));
    
    // Nested decorators
    container.Register<IFoo, Foo>();
    container.Decorate(typeof(IFoo), typeof(FooDecorator));
    container.Decorate(typeof(IFoo), typeof(AnotherFooDecorator));
    
    // Decorating specific implementations via predicate
    container.Register<IFoo, Foo>();
    container.Register<IFoo, AnotherFoo>("AnotherFoo");
    container.Decorate(typeof(IFoo), typeof(FooDecorator), service => service.ServiceName == "AnotherFoo");
    
    // Open generic decorators
    container.Register(typeof(IFoo<>), typeof(Foo<>));
    container.Decorate(typeof(IFoo<>), typeof(FooDecorator<>));
  7. Understand service lifetimes in LightInject

    master

    By default, LightInject treats all registered objects as transients, meaning a new instance is created every time the service is requested. To change this behavior, you must specify a lifetime during registration using the container.Register<TService, TImplementation>(ILifetime lifetime) method.

    Default (Transient)

    container.Register<IFoo,Foo>();
    // Every GetInstance<IFoo>() returns a new instance
    container.Register<IFoo,Foo>();
    var firstInstance = container.GetInstance<IFoo>();
    var secondInstance = container.GetInstance<IFoo>();
    Assert.AreNotSame(firstInstance, secondInstance);
  8. Use Function Factories for service resolution

    master

    Function factories allow you to resolve a service as a function delegate (Func<T>). This acts as an alternative to the Service Locator pattern.

    Key features:

    • Named Factories: Resolve a specific named instance using container.GetInstance<Func<T>>("Name").
    • Parameterized Factories: Pass arguments to the factory to create services with specific values. The service must be explicitly registered using a factory delegate for this to work.
    • IDisposable: If using function factories for disposable objects, the consumer is responsible for disposing the returned instance.
    // Basic Function Factory
    container.Register<IFoo, Foo>();
    var func = container.GetInstance<Func<IFoo>>();
    var foo = func();
    
    // Named Function Factory
    container.Register<IFoo, Foo>("SomeFoo");
    var namedFunc = container.GetInstance<Func<IFoo>>("SomeFoo");
    var namedFoo = namedFunc();
    
    // Parameterized Function Factory
    container.Register<int, IFoo>((factory, value) => new Foo(value));
    var fooFactory = container.GetInstance<Func<int, IFoo>>();
    var fooWithParam = fooFactory(42);
  9. Resolve multiple implementations as IEnumerable<T>

    master

    When multiple services are registered for the same type, you can resolve them all at once using GetInstance<IEnumerable<T>>() or GetAllInstances<T>().

    Supported collection types include:

    • Array
    • ICollection<T>
    • IList<T>
    • IReadOnlyCollection<T>
    • IReadOnlyList<T>

    Variance Control

    By default, LightInject resolves all services compatible with the requested element type (e.g., if you request IEnumerable<Foo>, it will include DerivedFoo). You can disable this behavior using ContainerOptions.EnableVariance = false or filter variance using options.VarianceFilter.

    // Basic resolution
    container.Register<IFoo, Foo>();
    container.Register<IFoo, AnotherFoo>("AnotherFoo");
    var instances = container.GetInstance<IEnumerable<IFoo>>();
    
    // Using GetAllInstances
    var instances = container.GetAllInstances<IFoo>();
    
    // Controlling Variance
    var container = new ServiceContainer(new ContainerOptions { EnableVariance = false });
    container.Register<Foo>();
    container.Register<DerivedFoo>();
    var instances = container.GetAllInstances<Foo>(); // Returns only 1 instance
    
    // Custom Variance Filter
    options.VarianceFilter = (enumerableType) => enumerableType.GetGenericArguments()[0] == typeof(IFoo);
  10. Register services via Assembly Scanning

    master

    LightInject can automatically register services by scanning an assembly for types. You can register an entire assembly, filter registrations using a predicate, or scan for assembly files using a search pattern.

    By default, the service name used during scanning is the implementing type name. You can customize this behavior per registration or globally by implementing IServiceNameProvider.

    // Register all services in an assembly
    container.RegisterAssembly(typeof(IFoo).Assembly);
    
    // Register services in an assembly with a filter predicate
    container.RegisterAssembly(typeof(IFoo).Assembly, (serviceType, implementingType) => serviceType.NameSpace == "SomeNamespace");
    
    // Scan for assembly files using a pattern
    container.RegisterAssembly("SomeAssemblyName*.dll");
    
    // Customizing service names via IServiceNameProvider
    public class CustomServiceNameProvider : IServiceNameProvider
    {
        public string GetServiceName(Type serviceType, Type implementingType)
        {
            return "Provide custom service name here";  
        }
    }
    container.ServiceNameProvider = new CustomServiceNameProvider();
  11. Install LightInject via NuGet

    master

    LightInject is available via two distribution models on NuGet:

    1. Binary: Adds a reference to LightInject.dll in your project.
    2. Source: Installs a single file (LightInject.cs) directly into your project for a zero-dependency footprint.

    Use the Package Manager Console to install either version.

    # Install the binary version
    PM> Install-Package LightInject
    
    # Install the source version (LightInject.cs)
    PM> Install-Package LightInject.Source
  12. Implement Lazy Decorators using Lazy<T>

    master

    A lazy decorator postpones the creation of its target instance until one of its methods is invoked. Because LightInject has native support for Lazy<T>, you can implement this by injecting Lazy<T> into your decorator's constructor.

    public class LazyFooDecorator : IFoo
    {
        private Lazy<IFoo> lazyFoo;
        public LazyFooDecorator(Lazy<IFoo> lazyFoo)
        {
            this.lazyFoo = lazyFoo;
        }
    
        public void Execute()
        {
            lazyFoo.Value.Execute();
        }
    }
    
    // Registration
    container.Register(typeof(IFoo), typeof(Foo));
    container.Decorate(typeof(IFoo), typeof(LazyFooDecorator));