Everlong.DI 0.5.0

Prefix Reserved
dotnet add package Everlong.DI --version 0.5.0
                    
NuGet\Install-Package Everlong.DI -Version 0.5.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="Everlong.DI" Version="0.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Everlong.DI" Version="0.5.0" />
                    
Directory.Packages.props
<PackageReference Include="Everlong.DI" />
                    
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 Everlong.DI --version 0.5.0
                    
#r "nuget: Everlong.DI, 0.5.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 Everlong.DI@0.5.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=Everlong.DI&version=0.5.0
                    
Install as a Cake Addin
#tool nuget:?package=Everlong.DI&version=0.5.0
                    
Install as a Cake Tool

Everlong.DI

Everlong.DI is a lightweight attribute-based DI library for .NET, built on source generators — no reflection overhead at runtime. It covers two independent mechanisms:

  • Member Injection ([Inject] members; IAutoInject/IInjectable markers) — push services into an object after it exists.
  • Service Registration ([Singleton] / [Scoped] / [Transient] / [AlsoAs] / [ServiceRegistrar]) — declare which types go into the container.

They solve different problems and never interact: registering a class does not inject its members, and injecting members does not register anything. Pick the parts you need.

Release history: docs/CHANGELOG.md. ⚠️ Upgrading from ≤ v0.3.0? v0.4.0 (v2) removed [Injectable] and Reinjectable[Inject] members are the anchor now; the migration table lives in docs/CHANGELOG.md. Working example: examples/Everlong.DI.Dogfood (21 runtime checks).

dotnet add package Everlong.DI

Two mechanisms, two scenarios

Member Injection Service Registration
Problem An object already exists and its dependencies are null Types must be declared to the container at startup
Typical scene Manually created objects, framework-hosted objects, lazy/optional wiring Application composition root, plugin/batch registration
Core types [Inject], IInjectable, IAutoInject, IInjectorServiceProvider [Singleton<T>], [Scoped<T>], [Transient<T>], [AlsoAs<T>], [ServiceRegistrar]
What the generator produces Inject(IServiceProvider) bodies RegisterServices(IServiceCollection) bodies
Runtime hook Someone must call Inject() (manual, wrapper SP, or framework interceptor) services.AddServices(new MyRegistrar())

Part A — Member Injection

Quick start

using Everlong.DI;

// v2: there is no class-level attribute — a partial class with [Inject] members gets an
// Inject() implementation generated on its own. IAutoInject (the v2 marker, derives from
// IInjectable) is implemented by every generated chain-starting class; declare it on a
// memberless framework base to opt the hierarchy in.
public partial class MyService : IAutoInject
{
    [Inject] private ILogger _logger;                       // field injection
    [Inject] public ISomeService Service { get; set; }      // property injection
}

The generator produces:

public virtual void Inject(IServiceProvider services)
{
    _logger = services.GetRequiredService<ILogger>();
    Service = services.GetRequiredService<ISomeService>();
}

Who calls Inject()?

Pattern Usage
Manual var svc = sp.GetRequiredService<MyService>(); svc.Inject(sp); — console apps, workers, tests
Auto-inject wrapper services.AddInjector(); then resolve through IInjectorServiceProvider — every resolved IInjectable is injected automatically
Framework interceptor Your own IoC integration calls Inject() during activation

AddInjector() registers two services: IInjectorServiceProvider (scoped by default; pass ServiceLifetime.Singleton to change) and IInjector (forwarded to the wrapper). The wrapper implements IKeyedServiceProvider, so keyed resolves get injected too.

Details worth knowing

  • Nullable members are optional: [Inject] ILogger? _loggerGetService<T>() (returns null); non-nullable → GetRequiredService<T>() (throws). Fail fast by default.
  • Keyed injection: [Inject("cache")], [Inject(42)], [Inject(typeof(TKey))], [Inject(SomeEnum.X)]GetRequiredKeyedService<T>(key) (requires .NET 8+ container).
  • Idempotency is unconditional: Inject() wires an instance exactly once (guard field); later calls are no-ops. There is no opt-out — re-wiring the same instance across scopes would capture scoped services into a long-lived instance (a DI anti-pattern).
  • partial void OnInjected(): runs after every successful injection. It only exists on generated classes — implement IAutoInject on a memberless base if you want a hook with no members.
  • Inheritance: Inject() chains through the base chain (override + base.Inject(services)). Intermediate levels that declare no [Inject] members are transparent — no marker needed on them. Sealed: a sealed chain start gets a plain non-virtual public void Inject (nothing could override it — and C# forbids virtual in sealed classes); a sealed class in the middle of a chain still emits override.
  • Partial properties: [Inject] public partial ILogger Logger { get; } generates a backing field — requires <LangVersion>preview</LangVersion>.
  • Split partial classes: a type may be split across several partial files. [Inject] members may live on any part; generation is driven by members (or an IAutoInject marker), never by file path ordering.
  • XML docs on generated code: generated Inject() / RegisterServices() carry /// <inheritdoc/> that inherits the contract docs from IInjectable / IServiceRegistrar — projects enabling GenerateDocumentationFile get no CS1591 on Everlong.DI-generated files.
  • Scope detection: services.AddScopeMarker(); registers IScopeMarker (scoped). provider.IsScoped() tells you whether a provider is a child scope — false for the root provider, for providers without the marker, and under ValidateScopes; the marker itself exposes IsRootScope.

Part B — Service Registration

Quick start

using Everlong.DI;

[Singleton]                        // register the class itself
public partial class CacheService;

[Singleton<IOrderService>]         // register as a service type
public partial class OrderService : IOrderService;

[Scoped<IRepository>("tenant:eu")] // keyed registration
public partial class Repository : IRepository;

[ServiceRegistrar]                 // one per assembly — generator fills RegisterServices()
public partial class MyRegistrar : IServiceRegistrar;

Wire it up:

var services = new ServiceCollection();
services.AddServices(new MyRegistrar());

The generator emits for each annotated type a TryAdd/Add + ServiceDescriptor call — validity is checked at compile time (see below), so the generated method is plain descriptors.

Registration attributes

Attribute Meaning
[Singleton] / [Scoped] / [Transient] Register the class itself (self)
[Singleton<T>] / [Scoped<T>] / [Transient<T>] Register as service type T — repeatable, each registration gets its own instance
[AlsoAs<T>] Add a shared view of the single instance registered by the main [Singleton]/[Scoped] registration
[ServiceRegistrar] Marks the partial class that hosts the generated RegisterServices()

Constructor arguments, on every attribute: (key?, enumerable?).

[Singleton<IFoo>(enumerable: true)]          // multiple implementations, resolved as IEnumerable<IFoo>
[Singleton<IFoo>("tenant:eu")]               // keyed — resolve with GetRequiredKeyedService<IFoo>("tenant:eu")
[Singleton<IFoo>("tenant:eu", enumerable: true)]

Keyed and unkeyed registrations are independent spaces — [Singleton<IFoo>] + [Singleton<IFoo>("k")] on one class is legal.

[AlsoAs<T>] — one instance, several faces

[Singleton]                    // main registration — the single instance
[AlsoAs<ICache>]               // ICache resolves to the same instance
[AlsoAs<IMetric>("metrics")]   // keyed view of the same instance
public partial class Cache : ICache, IMetric;

var cache = sp.GetRequiredService<ICache>();
var metric = sp.GetRequiredKeyedService<IMetric>("metrics");
ReferenceEquals(cache, metric);  // true

The main registration can be [Singleton], [Scoped], or a single generic variant ([Singleton<T>], [Scoped<T>]). Lifetime follows the main.

Composition rules (enforced by the generator)

Rule Error
One lifetime per type — no cross-lifetime mixes DIG0016
Self and generic registrations are mutually exclusive within a lifetime — share via [AlsoAs], get independence via multiple [Singleton<T>] DIG0015
[AlsoAs] needs exactly one non-transient, non-enumerable main registration DIG0011–0014
[AlsoAs] type must be an interface the class implements DIG0017
Duplicate registrations are allowed (TryAdd first-wins, Add accumulates)

See docs/skills/everlong-di-workflow/SKILL.md for the full workflow, diagnostics reference, and red lines.


Requirements

  • .NET 8+

  • <LangVersion>preview</LangVersion> in the consuming project (partial-property injection only; field injection works on any modern LangVersion):

    <PropertyGroup>
      <LangVersion>preview</LangVersion>
    </PropertyGroup>
    
  • The source generator itself targets netstandard2.0 and works with any modern Roslyn version.


AOT / Trimming

[Singleton]/[Scoped]/[Transient] on a class the container could never build fails the build: an abstract or static implementation is DIG0020, and [Singleton<T>] whose class does not implement T is DIG0021. Generated injection resolves via direct GetRequiredService<T>() calls — no reflection in the hot path; registration by type is instantiated by the container's own factory path, which works under NativeAOT. ServiceRegistrarHelper remains public for hand-written registrars; the generator does not emit it.


Build & Pack

dotnet build -c Release          # MUST build Release first — the nupkg embeds the
                                 # Generators/CodeFixers DLLs from bin/Release via loose
                                 # file includes; `dotnet pack` alone would pack stale ones
dotnet pack src/Everlong.DI -c Release --no-build

Package is produced under src/Everlong.DI/bin/Release/.

Verify the packed artifact (package-level regression net): push the nupkg to any local folder feed, then run the SmokeTest — it consumes Everlong.DI as a real NuGet package (not a ProjectReference) and must print All OK!:

dotnet nuget push src/Everlong.DI/bin/Release/Everlong.DI.<version>.nupkg --source <feed-path>
rm -rf "$HOME/.nuget/packages/everlong.di"   # drop the cached copy first
# SmokeTest's committed NuGet.Config lists nuget.org only and clears other sources, so the
# pre-release feed is passed per restore (no config file is modified or committed):
cd tests/Everlong.DI.SmokeTest
dotnet run -p:RestoreAdditionalProjectSources="<feed-path>"   # -> All OK!

After a release, SmokeTest verifies the published nuget.org package with no extra config.


Project Structure

Everlong.DI/
├── EverlongDI.props
├── README.md
├── docs/
│   └── skills/                          # AI coding assistant skill definitions
├── src/
│   ├── Everlong.DI/                     # Contracts, attributes, service provider (packable)
│   ├── Everlong.DI.Generators/          # Source generator + analyzers (embedded)
│   └── Everlong.DI.CodeFixers/          # Roslyn code fixers (embedded)
└── tests/
    ├── Everlong.DI.Tests/               # Unit tests + snapshot tests (Verify)
    ├── AssemblyA/                       # Manual end-to-end verification project
    └── Everlong.DI.SmokeTest/           # Package-level consumer smoke (pack → consume)

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Everlong.DI:

Package Downloads
Everlong.CommandLine

Everlong.CommandLine — command-tree based CLI framework built on System.CommandLine: CommandBase, interactive subcommand menu, chain DI injection, and process/file/git/console helpers.

Everlong.Nester

Nester core — the shell assembly, the layer ledger, the intent chain, the routing engine, the presentation primitives and hosting, activation, threading and messaging.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.0 133 9/12/2026
0.4.1 100 9/2/2026
0.4.0 105 9/2/2026
0.3.0 98 8/28/2026
0.2.1 115 8/11/2026
0.2.0 100 8/7/2026
0.1.0 123 8/3/2026
0.0.5 115 7/30/2026
0.0.4 114 7/30/2026 0.0.4 is deprecated because it has critical bugs.
0.0.3 117 7/23/2026
0.0.2 108 7/23/2026
0.0.1 114 7/23/2026

0.5.0 — registration validity is now a compile-time error: an abstract or static implementation is DIG0020, and [Singleton<T>] whose class does not implement T is DIG0021 (both were startup ArgumentExceptions). ServiceRegistrarHelper stays public for hand-written registrars; the generator no longer emits it, so RegisterServices() is plain descriptors. Plus two generator fixes: keyed [AlsoAs<T>("key")] views generated code that did not compile (CS1660), and [Inject] on a nullable member kept no nullability — a nullable field produced CS8600 in every consumer build with nullable enabled (a build failure under TreatWarningsAsErrors), while a nullable value type on a partial property emitted the invalid int??. Unkeyed, enumerable and generic-main forwards are unchanged.