CSharpEssentials.DependencyInjection
5.2.1
See the version list below for details.
dotnet add package CSharpEssentials.DependencyInjection --version 5.2.1
NuGet\Install-Package CSharpEssentials.DependencyInjection -Version 5.2.1
<PackageReference Include="CSharpEssentials.DependencyInjection" Version="5.2.1" />
<PackageVersion Include="CSharpEssentials.DependencyInjection" Version="5.2.1" />
<PackageReference Include="CSharpEssentials.DependencyInjection" />
paket add CSharpEssentials.DependencyInjection --version 5.2.1
#r "nuget: CSharpEssentials.DependencyInjection, 5.2.1"
#:package CSharpEssentials.DependencyInjection@5.2.1
#addin nuget:?package=CSharpEssentials.DependencyInjection&version=5.2.1
#tool nuget:?package=CSharpEssentials.DependencyInjection&version=5.2.1
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
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. |
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<>)), which decorates the closed registrations. |
| 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:
- Add the attributes to the classes the
Scancall registered. Build: CSE2003 points at classes whose interfaces do not followI{TypeName}, and the code fix adds the explicit service type. - Replace the
Scancall withservices.Add{Assembly}Services(), orAddAllServices()in the application. - Move
Decoratecalls to[Decorates]attributes or keep them as runtime calls. - Remove the Scrutor package. Both libraries define
DecorateandTryDecoratein theMicrosoft.Extensions.DependencyInjectionnamespace, 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 (closed registrations can be decorated with Decorate(Type, Type)).
| Product | Versions 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. |
-
.NETStandard 2.1
-
net10.0
-
net11.0
- No dependencies.
-
net8.0
-
net9.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.