A lightweight, high-performance dependency injection container for .NET with source generator support for AOT-friendly, zero-reflection DI.
- Source Generator - Compile-time code generation replaces runtime reflection for NativeAOT compatibility
- High Performance - Zero-reflection instance creation via generated
ITypeInfoimplementations - Thread Safe - All public APIs are thread-safe using concurrent collections
- Hierarchical Containers - Parent-child relationships with proper disposal chains
- Flexible Registration - Instance, Transient, Singleton, Scoped, and Factory patterns
- Type-Safe Labels - Support for labeled dependencies with compile-time safety
- Generic Support - Open generic type registration and resolution
- Runtime library (
OneShot): netstandard2.1 — usable from any compatible TFM (net5.0+, Unity 2021 LTS+, etc.) - Source generator (
OneShot.Generator): targets netstandard2.0 (Roslyn analyzer requirement); requires the host project's Roslyn to be 4.12 or newer (.NET 8 SDK / Visual Studio 17.10+) - NativeAOT: net8.0 or newer
- C# language version: 14.0 (set in
Directory.Build.props)
Both packages from NuGet:
dotnet add package OneShot # runtime container
dotnet add package OneShot.Generator # incremental source generatorThe generator is a separate package because it ships as a Roslyn analyzer (netstandard2.0). Without it, the container falls back to throwing NotSupportedException for any type it can't find in the registry — runtime reflection is not used at all.
using OneShot;
// Create container
var container = new Container();
// Register types
container.Register<DatabaseService>().Singleton().AsInterfaces();
container.Register<UserRepository>().Scoped().AsSelf();
container.RegisterInstance<ILogger>(new ConsoleLogger()).AsSelf();
// Resolve dependencies
var repository = container.Resolve<UserRepository>();OneShot uses a C# incremental source generator to emit ITypeInfo implementations at compile time, replacing runtime reflection entirely. Types are discovered for generation through two mechanisms:
Types used via Register<T>() or Instantiate<T>() are automatically discovered — no attributes required:
// Source generator detects these call sites and generates ITypeInfo for MyService
container.Register<MyService>().Singleton().AsSelf();
container.Instantiate<MyService>();Types with [Inject] on any member (field, property, method, constructor, or parameter) are also discovered:
class MyService
{
[Inject] public ILogger Logger; // Triggers source generation for MyService
}When you call Register(Type) with a Type known only at runtime, the source generator has no syntactic site to scan from. Make sure the type is also referenced from a generic call site somewhere in the assembly (even in dead code), or carries [Inject] on at least one member:
// This alone won't trigger source generation:
container.Register(someType);
// Add a manifest method to anchor generation. The method never has to be called -
// the generator only cares about the call sites' syntax.
file static class _SourceGenManifest
{
static void Anchor(Container c)
{
c.Register<MyType>(); // generator sees this and emits ITypeInfo for MyType
c.Register<OtherType>();
}
}The runtime library is annotated so the AOT analyzer can tell callers which APIs are safe under trimming:
| API | AOT status |
|---|---|
Register<T>() / Instantiate<T>() + [Inject]-driven constructor/field/property/method injection |
Fully AOT-safe — flows through the source generator with no warnings |
GenericExtension.RegisterGeneric(Type, MethodInfo) |
[RequiresDynamicCode] + [RequiresUnreferencedCode] — callers get warnings |
LabelExtension.CreateLabelType |
[RequiresDynamicCode] — callers get warnings |
Container.TryResolve (label or array path), ResolverBuilder.As(contractType, label) |
Suppressed at the boundary; the MakeGenericType / Array.CreateInstance paths only fire when you opt in by passing a label or resolving an array type. No analyzer warnings, but callers using those code paths are responsible for keeping the requested types reachable for the trimmer. |
When publishing with PublishAot=true, the only diagnostics you'll see come from RegisterGeneric and explicit CreateLabelType calls — typical apps that stick to Register<T>() / Instantiate<T>() build clean.
See Test Cases for comprehensive examples
// Create root container
var container = new Container();
// Create child container (inherits parent registrations)
var child = container.CreateChildContainer();
// Create scoped container (auto-disposed)
using (var scope = container.BeginScope())
{
// Scoped registrations live here
}
// Performance Options
container.EnableCircularCheck = false; // Disable circular dependency checking (default: true in DEBUG)
container.PreventDisposableTransient = true; // Prevent memory leaks (default: false)// Transient - New instance each time (default)
container.Register<Service>().AsSelf();
// Singleton - Single instance per container hierarchy
container.Register<Service>().Singleton().AsSelf();
// Scoped - Single instance per container scope
container.Register<Service>().Scoped().AsSelf();
// Instance - Register existing instance
container.RegisterInstance<IConfig>(new AppConfig()).AsSelf();// Register as specific interface
container.Register<Service>().As<IService>();
// Register as all interfaces
container.Register<Service>().AsInterfaces();
// Register as all base classes
container.Register<Service>().AsBases();
// Register as self and interfaces
container.Register<Service>().AsSelf().AsInterfaces();// Factory registration
container.Register<Func<int>>((container, type) => () => 42).AsSelf();
// With specific constructor parameters
container.Register<Service>().With("config", 123).AsSelf();
// Generic type registration. Uses MakeGenericMethod at runtime, so this API is
// annotated [RequiresDynamicCode]/[RequiresUnreferencedCode] - not AOT-friendly.
container.RegisterGeneric(typeof(Repository<>), CreateRepository).AsSelf();// Basic resolution
var service = container.Resolve<IService>();
// Generic resolution
var repository = container.Resolve<Repository<User>>();
// Group resolution
var services = container.ResolveGroup<IService>();
// Create instance without registration
var instance = container.Instantiate<MyClass>();class Service
{
// Constructor injection (preferred)
[Inject]
public Service(IDatabase db, ILogger logger) { }
// Field injection
[Inject] private ICache _cache;
// Property injection
[Inject] public IConfig Config { get; set; }
// Method injection
[Inject]
public void Initialize(IEventBus eventBus) { }
}
// Manual injection
var service = new Service();
container.InjectAll(service); // Injects fields, properties, and methods// Define labels
interface PrimaryDb : ILabel<IDatabase> { } // Type-specific label
interface SecondaryDb : ILabel<IDatabase> { }
interface Cache<T> : ILabel<T> { } // Generic label
// Register with labels
container.Register<PostgresDb>().As<IDatabase>(typeof(PrimaryDb));
container.Register<MySqlDb>().As<IDatabase>(typeof(SecondaryDb));
container.Register<CachedRepository>().As<IRepository>(typeof(Cache<>));
// Use labeled dependencies
class Service
{
public Service(
[Inject(typeof(PrimaryDb))] IDatabase primary,
[Inject(typeof(SecondaryDb))] IDatabase secondary,
[Inject(typeof(Cache<>))] IRepository cached
) { }
}// Automatically detected and throws descriptive exception
container.Register<A>().AsSelf(); // A depends on B
container.Register<B>().AsSelf(); // B depends on A
var a = container.Resolve<A>(); // Throws CircularDependencyException// IDisposable instances are automatically disposed
using (var scope = container.BeginScope())
{
var service = scope.Resolve<DisposableService>();
} // service.Dispose() called automatically
// Child containers cascade disposal
container.Dispose(); // Disposes all child containers and registered IDisposables- Prefer Constructor Injection - Most explicit and testable
- Use Scoped for Request/Frame Lifetime - Ideal for per-request isolation
- Avoid Disposable Transients - Can cause memory leaks
- Use Labels for Multiple Implementations - Type-safe alternative to string keys
- Create Child Containers for Isolation - Test scenarios or modular features
dotnet buildTests use TUnit, which compiles into a self-running executable. Run them with dotnet run, not dotnet test:
dotnet run --project tests/OneShot.Tests # runtime container tests (75 tests)
dotnet run --project tests/OneShot.Generator.Tests # generator snapshot tests (14 tests)dotnet publish tests/OneShot.Tests -c Release # publishes a NativeAOT binary; CI runs thisdotnet run -c Release --project benchmarks/OneShot.BenchmarksContributions welcome! Please ensure:
- All tests pass
- No compiler warnings (warnings as errors enabled)
- Thread safety maintained
- NativeAOT compatibility preserved (no runtime reflection in new code)
MIT License - See LICENSE file for details