CSharpEssentials.DependencyInjection 6.5.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package CSharpEssentials.DependencyInjection --version 6.5.2
                    
NuGet\Install-Package CSharpEssentials.DependencyInjection -Version 6.5.2
                    
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="CSharpEssentials.DependencyInjection" Version="6.5.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CSharpEssentials.DependencyInjection" Version="6.5.2" />
                    
Directory.Packages.props
<PackageReference Include="CSharpEssentials.DependencyInjection" />
                    
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 CSharpEssentials.DependencyInjection --version 6.5.2
                    
#r "nuget: CSharpEssentials.DependencyInjection, 6.5.2"
                    
#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 CSharpEssentials.DependencyInjection@6.5.2
                    
#: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=CSharpEssentials.DependencyInjection&version=6.5.2
                    
Install as a Cake Addin
#tool nuget:?package=CSharpEssentials.DependencyInjection&version=6.5.2
                    
Install as a Cake Tool

CSharpEssentials.DependencyInjection

Attribute-based service registration and decoration for Microsoft.Extensions.DependencyInjection. Mark a class, and it is registered with key-aware strategies. Decorate any registration, keyed or not, without touching its consumers.

Installation

dotnet add package CSharpEssentials.DependencyInjection

Target frameworks: net11.0, net10.0, net9.0, net8.0, netstandard2.1. The generic attribute forms ([RegisterScoped<TService>], [Decorates<TService>]) need a net7.0 or later target; on netstandard2.1 use the typeof(...) forms. The source generator, analyzer and code fix are bundled in the package.

Registering services

using CSharpEssentials.DependencyInjection;

[RegisterScoped]                                   // IOrderService, by the I{TypeName} rule
public sealed class OrderService : IOrderService { }

[RegisterSingleton(typeof(IClock))]                // explicit service type
public sealed class SystemClock : IClock { }

[RegisterSingleton<IPaymentGateway>(Key = "stripe")] // generic form (C# 11+), keyed
public sealed class StripeGateway : IPaymentGateway { }

[RegisterTransient(typeof(IRepository<>))]         // open generics
public sealed class Repository<T> : IRepository<T> { }

Without a service type and without As, a class registers as the implemented interface named I{TypeName} (same generic arity), otherwise as itself.

Property Values
Key Any attribute constant. null registers a non-keyed service.
As Self, SelfWithInterfaces (interfaces forward to one shared instance), ImplementedInterfaces.
Strategy Add (default), TryAdd, TryAddEnumerable, Replace, Throw. All are key-aware.

[ExcludeFromRegistration] on a class or an assembly opts out.

With Add, a generated registration and a manual services.AddScoped<IOrderService, OrderService>() both stay in the collection: GetService<T>() resolves the last one added and GetServices<T>() returns both. Use TryAdd or Throw on the attribute, or the test below, when that is not what you want.

Decorating services

[Decorates(typeof(IOrderService), Order = 1)]
public sealed class LoggingOrderService(IOrderService inner, ILogger<LoggingOrderService> logger) : IOrderService { }

Or at runtime:

services.Decorate<IOrderService, LoggingOrderService>();             // throws InvalidOperationException when nothing matches
services.Decorate<IPaymentGateway, RetryingGateway>(serviceKey: "stripe");
services.Decorate<IClock>((inner, sp) => new CachedClock(inner));
services.TryDecorate<IOrderService, AuditingOrderService>();         // returns false when nothing matches
services.Decorate(typeof(IRepository<>), typeof(CachedRepository<>)); // decorates closed registrations such as IRepository<Order>

Decorate(Type, Type) decorates every registration of a closed type constructed from the open generic service type, closing the decorator over the same type arguments. It throws InvalidOperationException when a registration of the open generic type itself exists (for example [RegisterTransient(typeof(IRepository<>))]) or when there is no closed registration to decorate. It uses reflection and is marked [RequiresUnreferencedCode] and [RequiresDynamicCode].

Attribute decorators are applied in ascending Order. Every matching registration is decorated, and its lifetime and key are kept. The original is moved to a hidden registration under a private key and a service type other than T and object, so it never shows up in GetServices<T>(), GetKeyedServices<T>(KeyedService.AnyKey) or GetKeyedServices<object>(KeyedService.AnyKey). The container disposes both the decorator and the original. If decoration fails, the collection is left unchanged.

A decorator's other constructor parameters are resolved like the container does: [ServiceKey] receives the decorated key, [FromKeyedServices("k")] resolves the k registration, [FromKeyedServices(null)] the non-keyed one, and the parameterless [FromKeyedServices] (.NET 10) inherits the decorated key.

Generated registry

The package ships a source generator. Each assembly with marked classes gets an Add{Assembly}Services method (dots removed, so Sample.Billing becomes AddSampleBillingServices). It registers the assembly's services, then applies its decorators. It uses no reflection, so it is trim and Native AOT safe.

builder.Services.AddSampleBillingServices();
builder.Services.AddSampleBillingServices(logger); // logs duplicate (service, key) registrations at Debug

Applications (Exe/WinExe, not test projects) also get an internal AddAllServices(). It registers the services of every referenced assembly that has a generated registry, and of the application itself, before it applies any decorator, so a decorator can wrap a service from another assembly. Decorators of all assemblies are applied together in ascending Order; decorators with the same Order follow assembly order (referenced assemblies by name, the application last), then type name. AddServicesFromAssemblies uses the same rule with the assemblies in the order you pass them. Registration is idempotent per assembly: calling Add{Assembly}Services twice, or after AddAllServices, does nothing, and AddAllServices and AddServicesFromAssemblies skip assemblies that are already registered. The generated and reflection paths share one marker per assembly in the collection, so mixing them does not register a service or apply a decorator twice. The marker is written only after the assembly's registrations and decorators succeed, so a call that throws (for example a Throw strategy conflict) can be retried. A retry on the same collection is not clean, though: descriptors added before the exception stay in the collection, so a retry can add them again, and because AddAllServices marks its assemblies only after all of them succeed, one failing registry also leaves the registries that succeeded unmarked. The application fails to start in that case anyway, so fix the cause and build a new service collection instead of retrying on the same one. The marker is a singleton descriptor of an internal type, so it shows up in services.Count and when enumerating the collection; it is never resolved.

Decorators across assemblies

Add{Assembly}Services applies the assembly's decorators as soon as it has registered its services. A decorator whose service is registered by another assembly therefore needs that registration to exist first; otherwise Decorate throws InvalidOperationException naming the decorator. With per-assembly calls, register dependencies first:

builder.Services.AddSampleBillingServices();   // registers IPaymentGateway
builder.Services.AddSampleAuditServices();     // decorates IPaymentGateway

AddAllServices() has no such ordering requirement, because it registers every assembly's services before it applies any decorator. Prefer it in applications that decorate across assemblies.

Assembly attribute Effect
[assembly: ServiceRegistryName("Billing")] Renames the method to AddBillingServices.
[assembly: GenerateServiceAggregate] Generates AddAllServices() in a library or test project.
[assembly: DisableServiceAggregate] Turns off AddAllServices() in an application.
[assembly: ServiceModule(typeof(...))] Emitted by the generator to mark an assembly that has a registry; do not write it yourself.

The analyzer reports invalid declarations at compile time:

ID Severity Rule
CSE2001 Error The class does not implement the service type.
CSE2002 Error A RegistrationStrategy.Throw registration whose service type and key an earlier registration in the same assembly already registers. The generated registry registers types in metadata name order, so a Throw registration that comes first does not fail.
CSE2003 Info The class has interfaces, but none matches I{TypeName}, so it is registered as itself.
CSE2004 Error The decorator has no constructor parameter of the decorated type, or more than one.
CSE2005 Error The decorator does not have exactly one public constructor.
CSE2006 Info Captive dependency: a singleton takes a scoped or transient service (or a scoped one takes a transient) registered by attribute in the same project. Checked for single-constructor classes only.
CSE2007 Error The class cannot be constructed by generated code (abstract, static, file-local, inaccessible, or nested in a generic type).
CSE2008 Warning Open-generic decorators are not generated; call services.Decorate(typeof(IRepository<>), typeof(CachedRepository<>)) at runtime, which decorates closed registrations such as IRepository<Order>.
CSE2009 Warning Two referenced assemblies produce the same registry name, for example Foo.Api and FooApi both produce FooApiServiceRegistry. AddAllServices skips both registries so the project still compiles. The project's own registry counts too: when it has the same name as a referenced one, only the own registry is called. Give one of them a distinct name with [assembly: ServiceRegistryName("...")]. Reported only in projects that generate the aggregate.

The bundled code fix for CSE2003 offers one action per implemented interface, which adds typeof(IFoo) (typeof(IFoo<>) for a generic class) to the attribute, and one action that adds As = ServiceAs.Self to keep the registration as is and silence the diagnostic.

Reflection fallback

services.AddServicesFromAssemblies(typeof(Program).Assembly);
services.AddServicesFromAssemblies(logger, typeof(Program).Assembly); // warns on unloadable types, logs duplicates at Debug

The fallback uses reflection, so it is marked [RequiresUnreferencedCode] and [RequiresDynamicCode]. Use the generated Add{Assembly}Services() method for trimmed and Native AOT applications.

Testing for duplicate registrations

The default Add strategy allows two registrations of the same service and key. A unit test catches unintended duplicates. Only the registry under test goes into the collection; decorated originals are kept under private keys, so they never count as duplicates.

public static class ServiceCollectionAssertions
{
    public static void ShouldHaveNoDuplicateRegistrations(this IServiceCollection services, params Type[] allowMultiple)
    {
        List<string> duplicates = services
            .Where(descriptor => !allowMultiple.Contains(descriptor.ServiceType))
            .GroupBy(descriptor => (descriptor.ServiceType, descriptor.ServiceKey))
            .Where(group => group.Count() > 1)
            .Select(group => $"{group.Key.ServiceType} (key: {group.Key.ServiceKey ?? "none"}) registered {group.Count()} times")
            .ToList();

        if (duplicates.Count > 0)
        {
            throw new InvalidOperationException("Duplicate registrations:" + Environment.NewLine + string.Join(Environment.NewLine, duplicates));
        }
    }
}

// In a test ([Fact] for xUnit, [Test] for NUnit):
public void Services_Should_NotRegisterTheSameServiceTwice()
{
    var services = new ServiceCollection();
    services.AddSampleBillingServices();

    services.ShouldHaveNoDuplicateRegistrations(allowMultiple: typeof(INotificationChannel));
}

Pass service types that are meant to have several implementations (TryAddEnumerable registrations, for example) as allowMultiple.

Migrating from Scrutor

Scrutor scans assemblies at runtime. This package replaces scanning with an attribute on each class and a generated, reflection-free registry.

Scrutor CSharpEssentials.DependencyInjection
services.Scan(s => s.FromAssemblyOf<T>().AddClasses(...)...) [RegisterScoped]/[RegisterSingleton]/[RegisterTransient] on each class, then services.Add{Assembly}Services()
.AsMatchingInterface() Default rule: the interface named I{TypeName}
.As<IFoo>() [RegisterScoped(typeof(IFoo))] or [RegisterScoped<IFoo>]
.AsSelf() / .AsSelfWithInterfaces() / .AsImplementedInterfaces() As = ServiceAs.Self / ServiceAs.SelfWithInterfaces / ServiceAs.ImplementedInterfaces
.WithScopedLifetime() The attribute name carries the lifetime
RegistrationStrategy.Append / Skip / Replace() / Throw Strategy = RegistrationStrategy.Add / TryAdd / Replace / Throw (all key-aware)
[ServiceDescriptor(typeof(IFoo), ServiceLifetime.Scoped)] with .UsingAttributes() [RegisterScoped(typeof(IFoo))]
services.Decorate<IFoo, FooDecorator>() Same call, or [Decorates(typeof(IFoo))] on the decorator; both also work for keyed services
services.TryDecorate<IFoo, FooDecorator>() Same call, returns bool
.FromApplicationDependencies() AddAllServices(), generated in applications from compile-time references
Namespace or type filters [ExcludeFromRegistration] on a class or an assembly

Steps:

  1. Add the attributes to the classes the Scan call registered. Build: CSE2003 points at classes whose interfaces do not follow I{TypeName}, and the code fix adds the explicit service type.
  2. Replace the Scan call with services.Add{Assembly}Services(), or AddAllServices() in the application.
  3. Move Decorate calls to [Decorates] attributes or keep them as runtime calls.
  4. Remove the Scrutor package. Both libraries define Decorate and TryDecorate in the Microsoft.Extensions.DependencyInjection namespace, so the calls are ambiguous while both are referenced.

Not supported: convention-based "register every class", custom interface-matching predicates, and decorating open-generic registrations (registrations of closed types can be decorated with Decorate(Type, Type)).

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 is compatible.  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 is compatible.  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.  net11.0 is compatible. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
7.0.0 39 10/20/2026
6.5.2 84 10/10/2026
6.5.1 89 10/10/2026
6.5.0 90 10/9/2026
6.4.0 84 10/9/2026
6.3.0 82 10/8/2026
6.2.0 82 10/8/2026
6.1.0 84 10/7/2026
6.0.0 85 10/7/2026
5.2.1 86 10/7/2026
5.2.0 79 10/7/2026
5.1.0 86 10/7/2026
5.0.0 82 10/6/2026
4.1.0 89 10/6/2026