Open-source project
seesharper/LightInject avatar
seesharper/LightInject

LightInject keeps the container small enough to read, and its resolution rules surprising

An ultra lightweight IoC container

634 stars118 forksC#MIT

At a glance

What is it?
LightInject is a .NET dependency injection container distributed either as a binary or as a single source file you drop into a project. Its resolution model has real opinions about variance, ordering and late binding that you should understand before adopting it.
Who is it for?
Adopt LightInject if you want a dependency injection container for a .NET project where keeping the dependency footprint small matters, or if you specifically want the source distribution so the container is part of your own assembly.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 23 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two NuGet packages, one of them a single file

LightInject is described as an ultra lightweight IoC container for .NET, and the distribution model is unusual enough to notice immediately. The project provides two packages. The binary package is installed with the NuGet console command Install-Package LightInject, and it adds a reference to the LightInject.dll in the target project. The source package is installed with Install-Package LightInject.Source, and it installs a single file, LightInject.cs, into the current project. That is the whole difference, and it is a meaningful one. The source distribution means the container code compiles into your assembly, so you can read it, step through it and change it, with no extra assembly at deploy time. The binary package is the conventional choice when you want the container to stay a separate dependency. The repository itself is laid out as a solution, LightInject.sln, with src/ and a build/ directory, a designpatterns.md and a contributing.md, so the container code is expected to be read.

The container is disposable, so scope it

Creating a container is a single line:

csharp
var container = new LightInject.ServiceContainer();

The container implements IDisposable and should be disposed after usage has completed, and the documentation also notes it can be used inside of a using statement for a constrained scope. That is a real design decision rather than boilerplate, because a container holds compiled delegates and registrations that live for the lifetime of the object. If you build one per request, you dispose it per request. If you build one per application, you keep it for the application lifetime. The documented API keeps the disposal contract in the foreground, which is more than many containers do.

Named registrations, and the single default rule

A registration is a service type and an implementation, and the name argument is optional:

csharp
container.Register<IFoo, Foo>();
var instance = container.GetInstance<IFoo>();
Assert.IsInstanceOfType(instance, typeof(Foo));

Give a registration a name and it becomes addressable by that name:

csharp
container.Register<IFoo, Foo>();
container.Register<IFoo, AnotherFoo>("AnotherFoo");
var instance = container.GetInstance<IFoo>("AnotherFoo");
Assert.IsInstanceOfType(instance, typeof(AnotherFoo));

The behaviour worth internalising is the fallback. If only one named registration exists for a service type, LightInject resolves it as the default service, so asking for IFoo without a name returns the only registered implementation even when it was registered under a name. That is convenient and it is also the kind of rule that changes behaviour when a second registration is added later, because the moment there are two named registrations, the unnamed request stops having an obvious answer. If you rely on named registrations, pin the names at every call site.

RegisterFallback lets the container invent what you never registered

LightInject can resolve services that are not registered with the container, using the RegisterFallback method:

csharp
var container = new ServiceContainer();
container.RegisterFallback((type, s) => true, request => new Foo());
var foo = container.GetInstance<IFoo>();

The first argument is the predicate that decides whether a service can be late-resolved, and the second is a ServiceRequest instance that provides the requested service type and service name. This is the feature that makes the container flexible and also the one that can hide mistakes. A fallback whose predicate accepts everything will happily construct a service you forgot to register, which means a missing registration stops being a startup error and becomes a runtime surprise. The pattern is genuinely useful for optional plugins and for types the container cannot know about. It is also the first thing to review when a dependency graph behaves unexpectedly, so keep fallbacks narrow and their predicate specific.

IEnumerable resolution resolves compatible types by default

When several services share a service type, they can be resolved as an IEnumerable of that type, either through GetInstance<IEnumerable<T>> or through the GetAllInstances method:

csharp
container.Register<IFoo, Foo>();
container.Register<IFoo, AnotherFoo>("AnotherFoo");
var instances = container.GetAllInstances<IFoo>();
Assert.AreEqual(2, instances.Count());

The default goes further than you might expect. By default, LightInject resolves all services that are compatible with the requested element type, so registering a base class and a derived class and asking for the base type returns two instances. That behaviour can be switched off with the EnableVariance container option:

csharp
var container = new ServiceContainer(new ContainerOptions { EnableVariance = false });
container.Register<Foo>();
container.Register<DerivedFoo>();
var instances = container.GetAllInstances<Foo>();
Assert.AreEqual(1, instances.Count());

Variance can also be applied selectively through a VarianceFilter that inspects the enumerable type, which is the setting to reach for when you want the base type exact in most places and compatible in one specific place. The container also supports Array, ICollection of T, IList of T, IReadOnlyCollection of T and IReadOnlyList of T as enumerable sub types, with the latter two noted as available on .NET 4.5 and Windows Runtime.

Ordering falls out of the service name

When the order of resolved services matters, LightInject orders them by their service name. That is a simple rule with two consequences. Names that sort lexicographically determine the order, so a registration named A resolves before one named B:

csharp
container.Register<IFoo, Foo1>("A");
container.Register<IFoo, Foo2>("B");
container.Register<IFoo, Foo3>("C");

var instances = container.GetAllInstances<IFoo>().ToArray();
Assert.IsType<Foo1>(instances[0]);

There is also a dedicated method for registering several implementations of a type in order, RegisterOrdered, which takes the service type, an array of implementation types and a lifetime factory. It gives each implementation a service name that can be used for ordering when the services are resolved, and by default that name is formatted like 001, 002 and so on. A format function can be passed to change the convention, for instance producing names of the form A001. The lifetime argument shows that lifetime management is part of the same registration model, with a PerContainerLifetime passed as the factory in the documented example.

Delegates are compiled once and looked up in a tree

The performance story in the project documentation is about how a service instance is actually created. LightInject uses dynamic code compilation, either in the form of System.Reflection.Emit or compiled expression trees. When a service is requested, the code needed to create the instance is generated and compiled, and a delegate for that code is stored for lookup later, so the code is only compiled once. Those delegates are stored in an AVL tree, and the documentation argues that looking them up is what sets the top performing containers apart. For an engineer this has one practical implication. The container is doing work at runtime that a hand written factory would do at compile time, which means the first resolution of a service is more expensive than the second, and it means the container is not a good fit for environments that forbid dynamic code generation. That last point is not discussed in the documentation here, and it is worth checking before you adopt the container in a locked down runtime.

Editorial conclusion

Adopt LightInject if you want a dependency injection container for a .NET project where keeping the dependency footprint small matters, or if you specifically want the source distribution so the container is part of your own assembly. Do not adopt it if your team expects the container to hide missing registrations, because RegisterFallback and the default variance behaviour will resolve things you did not register, and do not adopt it for ASP.NET integration without checking the documentation yourself, since the documentation covered here describes only the standalone container. Before you start, decide between the binary and source packages, read the variance option before you rely on IEnumerable resolution, and dispose the container when the scope ends.

Frequently asked questions

How do I install LightInject?

There are two NuGet packages. Install-Package LightInject adds a reference to the LightInject.dll in the target project, while Install-Package LightInject.Source installs a single file, LightInject.cs, into the current project so the container compiles into your own assembly.

How do I resolve a service that was never registered in LightInject?

Use the RegisterFallback method. The first argument is a predicate that decides whether the service can be late-resolved, and the second is a ServiceRequest that supplies the requested service type and service name.

Why does GetAllInstances return more instances than I registered in LightInject?

By default the container resolves all services that are compatible with the requested element type, so a derived registration is included when you ask for the base type. Set EnableVariance to false in ContainerOptions, or use a VarianceFilter to apply variance selectively.

How is the order of resolved services determined in LightInject?

Services are ordered by their service name. The RegisterOrdered method registers several implementations of a type and gives each a name such as 001 or 002, and a format function can be passed to change that convention, for example to produce names of the form A001.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. seesharper/LightInject on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/seesharper-lightinject.svg)](https://hysenlabs.com/projects/seesharper-lightinject)

Was this useful?