PicoDI.Gen 2026.6.0

dotnet add package PicoDI.Gen --version 2026.6.0
                    
NuGet\Install-Package PicoDI.Gen -Version 2026.6.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PicoDI.Gen" Version="2026.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PicoDI.Gen" Version="2026.6.0" />
                    
Directory.Packages.props
<PackageReference Include="PicoDI.Gen" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PicoDI.Gen --version 2026.6.0
                    
#r "nuget: PicoDI.Gen, 2026.6.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PicoDI.Gen@2026.6.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PicoDI.Gen&version=2026.6.0
                    
Install as a Cake Addin
#tool nuget:?package=PicoDI.Gen&version=2026.6.0
                    
Install as a Cake Tool

PicoDI

Zero-reflection, AOT-native dependency injection for .NET.

NuGet License .NET

English | 简体中文 | 繁體中文 | Deutsch | Español | Français | 日本語 | Português (Brasil) | Русский

┌─────────────────────────────────────────────────────────────┐
│  PicoDI: compile-time dependency injection                │
│  ✓ Zero reflection at runtime                              │
│  ✓ Native AOT compatible                                   │
│  ✓ ~2.8x faster than Microsoft.DI in Native AOT runs       │
│  ✓ Low-overhead resolution on hot paths                    │
└─────────────────────────────────────────────────────────────┘

Why PicoDI?

Feature PicoDI Microsoft.DI
Reflection ❌ None ✅ Heavy
AOT Support ✅ Native ⚠️ Limited
Factory Generation Compile-time Runtime
Circular Dependency Detection Compile-time error Runtime crash
Resolution Overhead Low, generated fast paths Higher runtime overhead

Design priority: PicoDI prioritizes runtime throughput and AOT compatibility over source-code aesthetics in generated output. Some internal patterns (manual string building in generators, duplicated cold-path error sites) are deliberate trade-offs — they reduce allocations and keep hot paths compact, not oversights to be "cleaned up."

The Problem with Traditional DI

// Microsoft.DI at runtime:
// 1. Reflection to find constructors
// 2. Expression tree compilation
// 3. Dynamic delegate generation
// 4. Runtime type checking
// Result: runtime setup and resolution overhead stay on the hot path

The PicoDI Solution

// PicoDI at compile-time:
// Source Generator scans your code → Generates static factories
// Result: hot paths stay precomputed and Native AOT-friendly
// Current comparable Native AOT runs show ~2.83x-2.84x average speedup

Quick Start

Installation

dotnet add package PicoDI

Installing PicoDI also brings in the source generator and abstractions transitively.

Package Architecture

PicoDI ships as three NuGet packages with a clean dependency chain:

PicoDI  →  PicoDI.Gen  →  PicoDI.Abs
(runtime)   (source gen)    (interfaces)
Package Install when… What you get
PicoDI You're building an application Runtime container + source generator + abstractions (all transitive)
PicoDI.Gen You're building a library/extension that needs compile-time code generation Source generator + abstractions (no runtime container)
PicoDI.Abs You're building a library/extension that only needs the interfaces Interfaces and base types only

Basic Usage

using PicoDI;
using PicoDI.Abs;

// Define your services
public interface IGreeter { string Greet(string name); }
public class Greeter : IGreeter 
{
    public string Greet(string name) => $"Hello, {name}!";
}

// Register services - Source Generator does the heavy lifting
var container = new SvcContainer();
container.RegisterSingleton<IGreeter, Greeter>();

// Resolve
await using var scope = container.CreateScope();
var greeter = scope.GetService<IGreeter>();
Console.WriteLine(greeter.Greet("World")); // Hello, World!

Registration Methods

var container = new SvcContainer();

// Lifetime options (compile-time markers scanned by PicoDI.Gen)
container.RegisterTransient<IService, ServiceImpl>();     // New instance every time
container.RegisterScoped<IService, ServiceImpl>();        // One per scope
container.RegisterSingleton<IService, ServiceImpl>();     // One for app lifetime

// Factory registration (real runtime registration)
container.RegisterSingleton<IService>(sp => new ServiceImpl(
    sp.GetService<IDependency>()
));

// Instance registration (real runtime registration)
container.RegisterSingle<IConfig>(new AppConfig());

// Open generics (runtime registration of the generic definition)
container.RegisterTransient(typeof(IRepository<>), typeof(Repository<>));

// Batch registration
container.RegisterRange(descriptors);

container.Build(); // Optional: freeze & optimize now

Dependency Injection

public class OrderService(
    IOrderRepository repo,      // ← Injected
    ILogger logger,             // ← Injected
    IPaymentGateway gateway)    // ← Injected
{
    public void ProcessOrder(Order order) { /* ... */ }
}

// Source Generator automatically detects constructor parameters
// and generates: sp => new OrderService(
//     sp.GetService<IOrderRepository>(),
//     sp.GetService<ILogger>(),
//     sp.GetService<IPaymentGateway>())

Constructor Selection

public sealed class CheckoutService : ICheckoutService
{
    public CheckoutService() { }

    [SvcConstructor]
    public CheckoutService(IPaymentGateway gateway, ILogger logger)
    {
        // Preferred constructor for PicoDI source generation
    }
}
  • PicoDI uses the single public constructor when there is only one.
  • If there are multiple public constructors, PicoDI prefers the one marked with [SvcConstructor].
  • If no constructor is marked, PicoDI falls back to the widest public constructor.
  • Marking more than one public constructor with [SvcConstructor] is a compile-time error (PICO005).

Benchmarks

Environment: .NET 10.0.5 | Windows 10.0.26100 | x64 | Native AOT | 100 samples × 10,000 iterations

Overall Summary

╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║  PicoDI wins:  20 / 20 scenarios                                                                             ║
║  Average speedup: ~2.83x-2.84x faster                                                                        ║
║  Maximum speedup: ~4.00x-4.09x faster                                                                        ║
╚═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝

These numbers come from the comparable Native AOT publish flow:

dotnet publish "benchmarks/PicoDI.Benchmarks/PicoDI.Benchmarks.csproj" \
  -c Release \
  -p:UseProjectReferences=true \
  -p:PicoDiBenchmarksAotSingleFileWinX64=true

./bin/Release/publish/win-x64-aot-singlefile/PicoDI.Benchmarks.exe --all

Representative highlights from the latest validated runs:

  • NoDependency: ~3.46x average speedup
  • SingleDependency: ~2.71x average speedup
  • MultipleDependencies: ~2.58x average speedup
  • DeepChain: ~2.81x average speedup
  • Resolution scenarios: ~3.04x average speedup
  • Infrastructure overhead: ~1.85x average speedup

Representative scenario wins from the latest Native AOT run:

  • NoDependency × Singleton: ~3.89x faster
  • DeepChain × Transient: ~4.00x faster
  • MultipleResolutions × Singleton: ~3.53x faster
  • ContainerSetup: ~2.59x faster

Use the Native AOT publish flow above when comparing PicoDI performance. A plain dotnet run benchmark is useful for local iteration, but it is not the comparable baseline used for published numbers.

PicoDI generates inlined factory chains at compile-time. No reflection, no expression trees, and no runtime code generation on the resolution path.

How It Works

Compile-Time Magic

Your Code                    Source Generator Output
─────────────────────────    ─────────────────────────────────────
container.RegisterSingleton  [ModuleInitializer]
  <IFoo, Foo>();             static void AutoRegister() =>
container.RegisterScoped       SvcContainerAutoConfiguration
  <IBar, Bar>();                .RegisterConfigurator(
scope.GetService<IFoo>();         "GeneratedServiceRegistrations_MyApp",
                                   Configure);

                             public static ISvcContainer
                               ConfigureGeneratedServices(
                                 this ISvcContainer c)
                             {
                               Configure(c);
                               SvcContainerAutoConfiguration
                                 .MarkGeneratedConfigurationApplied(c);
                               return c;
                             }

                             static void Configure(ISvcContainer c)
                             {
                               c.Register(new SvcDescriptor(
                                 typeof(IFoo),
                                 static sp => new Foo(),
                                 SvcLifetime.Singleton));
                               c.Register(new SvcDescriptor(
                                 typeof(IBar),
                                 static sp => new Bar(),
                                 SvcLifetime.Scoped));
                             }

Resolution Flow

GetService<IFoo>()
       │
       ▼
┌──────────────────────┐
│ Frozen registration  │  O(1) lookup by service type
│ cache                │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ SvcRuntimeRegistration[] │
│ Last entry wins for      │  GetService<T>() / GetService(Type)
│ single-service resolution│
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ Lifetime-specific    │
│ caches               │  singleton/scoped state is runtime-owned
└──────────┬───────────┘
           │
           ▼
    Return instance

Advanced Usage

Open Generics

// Registration
container.RegisterTransient(typeof(IRepository<>), typeof(Repository<>));

// Usage - PicoDI.Gen closes the generic for the concrete usages it can see
// in resolution sites, constructor graphs, and referenced PicoDI-enabled assemblies.
var userRepo = scope.GetService<IRepository<User>>();      // ✓ Generated
var orderRepo = scope.GetService<IRepository<Order>>();    // ✓ Generated
  • When multiple open-generic mappings target the same service type, PicoDI preserves the first matching open-generic mapping for closed registration materialization.
  • Invalid open-generic implementation types produce the same activation diagnostics as normal registrations.

Circular Dependency Detection

// This won't compile - Source Generator catches it!
public class A(B b) { }
public class B(A a) { }  // Error PICO002: Circular dependency detected: A -> B -> A

Other Compile-Time Diagnostics

  • PICO003: abstract type or interface used as an implementation
  • PICO004: implementation type has no public constructor
  • PICO005: multiple public constructors marked with [SvcConstructor]

These diagnostics are enforced consistently for normal registrations and type-based open-generic registrations in both the analyzer and the source generator.

Testing

[Test]
public async Task OrderService_Should_ProcessOrder()
{
    // Skip auto-registration, use mocks
    await using var container = new SvcContainer(autoConfigureFromGenerator: false);
    
    container.RegisterSingleton<IOrderRepository>(_ => new MockOrderRepo());
    container.RegisterSingle<ILogger>(new TestLogger());
    container.RegisterSingle<IPaymentGateway>(new FakeGateway());
    container.RegisterSingleton<IOrderService>(sp => new OrderService(
        sp.GetService<IOrderRepository>(),
        sp.GetService<ILogger>(),
        sp.GetService<IPaymentGateway>()));

    // Optional: freeze registrations now.
    // CreateScope() auto-builds on first use if you skip this.
    container.Build();
    
    await using var scope = container.CreateScope();
    var service = scope.GetService<IOrderService>();
    // Assert...
}

Multiple Implementations

container.RegisterTransient<IPlugin, PluginA>();
container.RegisterTransient<IPlugin, PluginB>();
container.RegisterTransient<IPlugin, PluginC>();

// Get all implementations
var plugins = scope.GetServices<IPlugin>(); // [PluginA, PluginB, PluginC]

// Get last registered (override pattern)
var plugin = scope.GetService<IPlugin>();   // PluginC

Generated typed resolvers follow the same single-service override rule: for a given service type, the generated resolver uses the same effective registration that runtime GetService<T>() would use.

Hosting

PicoDI provides a minimal hosting model with IHostedSvc, BackgroundSvc, and SvcHostBuilder — enough for background workers and console applications without dragging in Microsoft.Extensions.Hosting.

Hosted Services

// IHostedSvc — simple start/stop lifecycle
public class CleanupService(ILogger<CleanupService> logger) : IHostedSvc
{
    public Task StartAsync(CancellationToken ct)
    {
        logger.Log("Cleanup service starting");
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken ct)
    {
        logger.Log("Cleanup service stopping");
        return Task.CompletedTask;
    }
}

// BackgroundSvc — long-running background work
public class Worker(IGreeter greeter) : BackgroundSvc
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine(greeter.Greet("World"));
            await Task.Delay(2000, stoppingToken);
        }
    }
}

SvcHostBuilder

var builder = new SvcHostBuilder();

builder.ConfigureServices(container =>
{
    container.RegisterSingleton<IGreeter, Greeter>();
    container.RegisterHostedSvc<CleanupService>(
        sp => new CleanupService(sp.GetService<ILogger<CleanupService>>()!)
    );
    container.RegisterHostedSvc<Worker>(
        sp => new Worker(sp.GetService<IGreeter>()!)
    );
});

// BuildAsync creates the container, builds it, and starts all hosted services.
await using var host = await builder.BuildAsync();

// Host runs until disposed — hosted services are stopped in reverse order.

IHostedLifecycleSvc

For fine-grained control, implement IHostedLifecycleSvc (extends IHostedSvc) with six phases:

Phase When
StartingAsync Before any service starts
StartAsync Service starts
StartedAsync After all services started
StoppingAsync Before any service stops
StopAsync Service stops
StoppedAsync After all services stopped

Container-First Alternative

SvcHostBuilder is a convenience wrapper. You can skip it entirely:

var container = new SvcContainer();
container.RegisterHostedSvc<Worker>(sp => new Worker(sp.GetService<IGreeter>()!));
container.Build();

var host = new SvcHost(container);
await host.StartAsync();

// ... application runs ...

await host.StopAsync();
await container.DisposeAsync();

See samples/PicoDI.Sample.Host for a complete working example.

Packages

Package Target Audience Dependencies
PicoDI Application developers PicoDI.GenPicoDI.Abs
PicoDI.Gen Library / extension authors needing code generation PicoDI.Abs
PicoDI.Abs Library / extension authors needing only interfaces None

For most users: just install PicoDI — it transitively brings in the source generator (PicoDI.Gen) and abstractions (PicoDI.Abs).

For library authors: reference PicoDI.Gen if you need compile-time source generation, or PicoDI.Abs if you only need the DI interfaces. This avoids pulling the runtime container into your library's dependency tree.

Requirements

  • .NET 10.0+ (uses C# 14 extension types)
  • Roslyn 5.x+ (or a modern .NET SDK with source generator support)

Important: Understanding autoConfigureFromGenerator

The Two Registration Modes

// Mode 1: Auto-configuration (default)
var container = new SvcContainer();  // autoConfigureFromGenerator = true

// Mode 2: Manual configuration
var container = new SvcContainer(autoConfigureFromGenerator: false);

How Registration Actually Works

Placeholder methods (depend on Source Generator):

container.RegisterTransient<IFoo, Foo>();      // ⚠️ Compile-time marker
container.RegisterScoped<IBar, Bar>();         // ⚠️ Requires generated registrations on this container
container.RegisterSingleton<IBaz, Baz>();      // ⚠️ PicoDI.Gen scans this and emits factory code

Factory methods (real runtime registration):

container.RegisterTransient<IFoo>(sp => new Foo());      // ✅ Real registration
container.RegisterScoped<IBar>(sp => new Bar());         // ✅ Works without Source Generator
container.RegisterSingleton<IBaz>(sp => new Baz());      // ✅ Always works
container.RegisterSingle<IConfig>(new Config());         // ✅ Instance registration

The Limitation

autoConfigureFromGenerator Placeholder Methods Factory Methods
true (default) ✅ Work (via Source Generator) ✅ Work
false Fail fast until generated registrations are applied ✅ Work

Generated configurators are discovered globally, but the applied state is tracked per container. That means ConfigureGeneratedServices() and automatic startup application affect the specific container instance you call them on, not every container in the process.

When to Use false

using PicoDI.Generated;

// ✅ Unit testing with mocks - use factory methods only
await using var container = new SvcContainer(autoConfigureFromGenerator: false);
container.RegisterSingleton<IRepo>(sp => new MockRepo());  // Factory method
container.RegisterSingleton<IService, MyService>();        // ❌ Fail-fast without generated registrations
container.Build();

// ✅ Integration testing - apply generated config then override
await using var container = new SvcContainer(autoConfigureFromGenerator: false);
container.ConfigureGeneratedServices();                    // Apply generated registrations to this container
container.RegisterSingleton<IExternal>(sp => new Mock()); // Override specific services

TL;DR

If autoConfigureFromGenerator = false, use factory/instance registration, or manually call ConfigureGeneratedServices() first. Placeholder methods like RegisterTransient<IFoo, Foo>() are compile-time markers and fail fast until generated registrations are applied to that specific container.

Contributing

Please ensure:

  • All tests pass (dotnet test -c Release)
  • Native AOT benchmark comparisons stay healthy (dotnet publish the benchmark project, then run the produced binary)
  • AOT compatibility maintained

License

MIT License - See LICENSE


<p align="center"> <b>PicoDI</b> — compile-time dependency injection for .NET </p>

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on PicoDI.Gen:

Package Downloads
PicoDI

A lightweight, zero-reflection, AOT-compatible dependency injection container for .NET with compile-time source generation

PicoLog.DI

PicoDI integration for PicoLog registration (AddPicoLog, WriteTo, ReadFrom).

PicoCfg.DI

PicoDI registration helpers for PicoCfg roots, snapshots, and generated bound POCOs.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2026.6.0 306 5/9/2026
2026.5.0 137 5/7/2026
2026.4.0 154 5/2/2026
2026.3.4 113 4/27/2026
2026.2.3 161 4/25/2026
2026.2.2 272 4/15/2026
2026.2.1 299 4/2/2026
2026.2.0 167 4/1/2026