SsalKit.DependencyInjection 0.0.6

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

← SsalKit

English | 한국어 | 日本語

SsalKit.DependencyInjection

Compile-time DI auto-registration for .NET, powered by a Roslyn source generator — no reflection, no assembly scanning, no runtime cost. NuGet

Why SsalKit.DependencyInjection?

Most "automatic" DI registration libraries work by scanning loaded assemblies with reflection at startup. That approach has real costs: startup latency that grows with your assembly count, metadata that trimming and Native AOT can't reason about, and failures that only surface at runtime.

SsalKit.DependencyInjection takes a different approach:

  • Compile-time, not runtime. A source generator inspects your code during the build and emits the registration calls directly as C# — there is no scanning step to run when your app starts.
  • Zero reflection. The generated code is a plain, readable IServiceCollection extension method. Nothing is discovered dynamically.
  • AOT & trimming friendly. Because registrations are ordinary generated code rather than reflection-driven discovery, they survive trimming and Native AOT compilation without extra annotations.
  • Compile-time diagnostics. Mistakes like registering an abstract class or an unimplemented interface are caught by the compiler, not by a runtime exception in production.

Installation

dotnet add package SsalKit.DependencyInjection

The package contains both the [Service] attribute and the source generator — no separate analyzer package to install.

Quick Start

Decorate the classes (or record classes) you want registered:

using Microsoft.Extensions.DependencyInjection;
using SsalKit.DependencyInjection;

// Registered under every interface it implements (or itself, if it implements none).
[Service(ServiceLifetime.Singleton)]
public class CacheService : ICacheService { }

// Registered only as the given type.
[Service(ServiceLifetime.Scoped, As = typeof(IUserRepository))]
public class UserRepository : IUserRepository, IDisposable { }

// Defaults to ServiceLifetime.Singleton.
[Service]
public class EmailSender { }

// Registered as a keyed service (.NET 8+ keyed DI).
[Service(ServiceLifetime.Singleton, Key = "redis")]
public class RedisCache : ICache { }

// Only registered if IClock hasn't already been registered.
[Service(ServiceLifetime.Singleton, Mode = RegistrationMode.TryAdd)]
public class DefaultClock : IClock { }

At build time, the generator emits one extension method per assembly, named after the assembly itself. For example, an assembly called MyApp.Web gets AddMyAppWebServices(), roughly equivalent to:

// <auto-generated/>
namespace Microsoft.Extensions.DependencyInjection;

public static class MyAppWebServiceCollectionExtensions
{
    public static IServiceCollection AddMyAppWebServices(this IServiceCollection services)
    {
        services.AddSingleton<CacheService>();
        services.AddSingleton<ICacheService>(sp => sp.GetRequiredService<CacheService>());

        services.AddScoped<IUserRepository, UserRepository>();

        services.AddSingleton<EmailSender>();

        services.AddKeyedSingleton<ICache, RedisCache>("redis");

        services.TryAddSingleton<IClock, DefaultClock>();

        return services;
    }
}

Wire it up in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMyAppWebServices();

var app = builder.Build();

Multi-interface Singleton/Scoped registrations are generated as a concrete registration plus factory forwarding, so every interface resolves to the same shared instance.

Forwarding Registrations and Their Limits

A Singleton or Scoped service that implements multiple interfaces is registered once as its concrete type, with each interface forwarded to it via services.Add<TLifetime><TInterface>(sp => sp.GetRequiredService<TImpl>()). This guarantees every interface resolves to the same shared instance, but it's the standard forwarding pattern and comes with two well-known limitations:

  1. Dispose may run more than once. If the interfaces are resolved and disposed independently — for example across different scopes — the same underlying instance's Dispose() can be invoked once per forwarded registration. IDisposable.Dispose() is expected to be idempotent, so this is usually harmless, but a non-idempotent Dispose implementation should be avoided on services registered under multiple interfaces.
  2. The last registration wins. The forwarding factory looks up the concrete type from the container at resolution time rather than capturing an instance. If application code later re-registers the concrete type manually, every forwarded interface follows that new registration instead of staying pinned to the one the generator emitted.

TryAddEnumerable does not forward

Mode = RegistrationMode.TryAddEnumerable cannot use the forwarding pattern above, because Microsoft.Extensions.DependencyInjection has no way to inspect the implementation type behind a factory-based descriptor when checking for an existing duplicate. Instead, the generator emits an independent descriptor per interface (ServiceDescriptor.<Lifetime><TService, TImpl>) — which means interfaces registered this way do not share an instance.

Open generics

[Service] also works on open generic classes. Instead of the closed generic-argument overloads, the generator emits the Type-based (typeof(...)) registration overloads:

[Service(ServiceLifetime.Singleton)]
public class Repository<T> : IRepository<T> { }

emits:

services.AddSingleton(typeof(IRepository<>), typeof(Repository<>));

Exact-match rule

An open generic class C<T1, ..., Tn> can only be registered against:

  • itself (self registration, when it implements no interfaces), or
  • an implemented interface or base type S<T1, ..., Tn> whose type arguments are exactly C's own type parameters, in the same declaration order.

Anything else — a closed service type (IRepository<string>), a reordered (IPair<V, K>) or partial (IPair<T>) type argument list, a non-generic interface, or a service type that wraps the type parameter (IRepository<IEnumerable<T>>) — is rejected at compile time (SSAL009). As = typeof(IRepository<>) (an unbound generic typeof) is supported as an explicit override and is checked against the same rule.

Keyed services (Key = ...) and all four RegistrationMode values (Add, TryAdd, TryAddEnumerable, Replace) are supported for open generics exactly as for closed types.

Microsoft.Extensions.DependencyInjection cannot use a factory delegate for an open generic registration, so the forwarding pattern described above isn't available here. An open generic class registered under two or more service types as Singleton or Scoped gets one independent typeof-based registration per service type — resolving different service types does not yield the same shared instance (SSAL010 warns about this).

A class nested inside a generic type is not supported as a [Service] target, even when the nested class has no type parameters of its own (SSAL003).

Factory methods

[Service] can also construct the implementation instance through a static factory method instead of letting the container call a constructor directly — handy when construction needs to run a bit of logic, or when the class deliberately hides its constructor:

public interface IApiClient { }

[Service(ServiceLifetime.Scoped, Factory = nameof(Create))]
public sealed class ApiClient : IApiClient
{
    private ApiClient(HttpClient httpClient) => HttpClient = httpClient;

    public HttpClient HttpClient { get; }

    public static ApiClient Create(IServiceProvider sp) =>
        new(sp.GetRequiredService<HttpClient>());
}

emits:

services.AddScoped<IApiClient, ApiClient>(sp => ApiClient.Create(sp));

Use nameof(...) (e.g. Factory = nameof(Create)) instead of a string literal, so a rename of the method is caught by the compiler.

Method requirements

The named method must be:

  • declared directly on the decorated class — not inherited from a base class, even one in the same assembly;
  • static and non-generic;
  • either parameterless or taking a single IServiceProvider parameter (no ref/out/params);
  • returning exactly the decorated class — not a base type or an interface it implements;
  • at least internal — the generated registration code calls it from a separate file in the same assembly.

When both a parameterless overload and an IServiceProvider-accepting overload exist, the IServiceProvider-accepting one is used — deterministically, not as an ambiguity error.

Factory composes with every other [Service] option — Lifetime, Key, As, and every RegistrationMode — and only changes how the implementation instance is constructed. When the class implements 2+ interfaces and would otherwise share one instance across them via the forwarding pattern described above, the factory is invoked exactly once (by the self-registration statement); every forwarded interface resolves to that same, factory-constructed instance.

Factory is not supported on an open generic decorated class: Microsoft.Extensions.DependencyInjection has no factory-based registration API for open generics (SSAL013).

Enum-keyed service factories

Keyed services solve "several implementations of one interface", but resolving them means reaching for IServiceProvider (or [FromKeyedServices]) at every call site. [ServiceFactory] turns that into an ordinary, strongly typed interface — and generates the implementation for you:

public enum PaymentMethod { Card, Bank }

[Service(ServiceLifetime.Singleton, As = typeof(IPaymentProcessor), Key = PaymentMethod.Card)]
public sealed class CardPaymentProcessor : IPaymentProcessor { }

[Service(ServiceLifetime.Singleton, As = typeof(IPaymentProcessor), Key = PaymentMethod.Bank)]
public sealed class BankPaymentProcessor : IPaymentProcessor { }

[ServiceFactory]
public interface IPaymentProcessorFactory
{
    IPaymentProcessor Create(PaymentMethod method);
}

You write no implementation at all. The generator emits one, into the reserved SsalKit.DependencyInjection.Generated namespace:

// <auto-generated/>
namespace SsalKit.DependencyInjection.Generated.MyApp
{
    internal sealed class IPaymentProcessorFactoryImplementation : IPaymentProcessorFactory
    {
        private readonly IServiceProvider _provider;

        public IPaymentProcessorFactoryImplementation(IServiceProvider provider) => _provider = provider;

        public IPaymentProcessor Create(PaymentMethod method)
            => _provider.GetRequiredKeyedService<IPaymentProcessor>(method);
    }
}

and adds it to the assembly's Add{Assembly}Services() method as a singleton, registered as the interface:

services.AddSingleton<IPaymentProcessorFactory, SsalKit.DependencyInjection.Generated.MyApp.IPaymentProcessorFactoryImplementation>();

so IPaymentProcessorFactory can be constructor-injected anywhere, with no wiring of your own.

Interface requirements

A [ServiceFactory] interface must:

  • be non-generic, and not nested inside a generic type (SSAL019);
  • declare exactly one member — one more method, a property, an event, or a nested type is rejected (SSAL017);
  • and that member must be an ordinary, non-static, non-generic method taking exactly one by-value enum parameter and returning a non-void, non-ref service type (SSAL018);
  • inherit no interface that has implementable members of its own (SSAL017), since the generated class would have to implement those too. A marker base interface is fine, as is one whose every member is static, a nested type, or a default implementation.

The interface, the enum, and the return type must all be at least internal and not file-local, because the generated implementation names all three (SSAL020). The generated class itself is always internal sealed, so a public factory interface is fine — nothing outside the assembly ever names the implementation.

Anything the generator can't honour is reported as an error and produces no implementation for that interface; other factories and every [Service] registration in the assembly are unaffected.

Unregistered keys

The generated method delegates straight to GetRequiredKeyedService<TReturn>(key) — no lookup table, no caching, no fallback. Calling it with an enum value nothing is registered under throws exactly what GetRequiredKeyedService throws (an InvalidOperationException for the built-in container), from the Create call itself. A fallback/TryCreate shape may be added in a later version; today the exception is the contract.

By the same token, the factory adds no lifetime of its own: it is a stateless wrapper registered as a Singleton, and whether two calls with the same key return the same instance is decided entirely by the keyed registration's own lifetime.

Cross-assembly registrations

No diagnostic is reported when the compilation that declares the factory contains no [Service(Key = SomeEnum.X)] registration for a given enum value. Declaring the factory interface in one assembly and registering the keyed implementations in another (or by hand, in Program.cs) is a supported, ordinary arrangement — the generator has no way to see the other assembly's registrations, so it does not guess. The trade-off is that a genuinely missing registration surfaces at resolution time rather than at compile time.

Registering every implementation of a contract

Some registrations read better as a rule than as an attribute repeated on every class: every IRequestHandler<,> is Scoped, every IStartupTask is a Singleton. [assembly: RegisterImplementationsOf] states that rule once, and the generator resolves it at compile time — still no reflection, still nothing to scan at startup:

using Microsoft.Extensions.DependencyInjection;
using SsalKit.DependencyInjection;

[assembly: RegisterImplementationsOf(typeof(IRequestHandler<,>), ServiceLifetime.Scoped)]
[assembly: RegisterImplementationsOf(typeof(IStartupTask))]

// None of these carries an attribute of its own — the two lines above register all three.
public sealed class PingHandler : IRequestHandler<Ping, Pong> { }
public sealed class MigrateDatabase : IStartupTask { }
public sealed class WarmCaches : IStartupTask { }

emits:

services.TryAddEnumerable(ServiceDescriptor.Scoped<IRequestHandler<Ping, Pong>, PingHandler>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupTask, MigrateDatabase>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupTask, WarmCaches>());

so IEnumerable<IStartupTask> can be constructor-injected directly.

The scan only sees the current compilation

The scan runs over the types declared in the assembly the attribute is applied to. Classes in referenced assemblies are never discovered, even through a project reference — the generator compiles one assembly at a time and cannot reach into another's source. To register a referenced assembly's implementations, declare the attribute in that assembly too and call its own generated Add{Assembly}Services() method.

What matches

The contract must be an interface (SSAL021).

  • A non-generic or closed generic contract (typeof(IStartupTask), typeof(IRequestHandler<Ping, Pong>)) matches every class that implements it.
  • An unbound generic contract (typeof(IRequestHandler<,>)) matches every class implementing any instantiation of it, and registers one (instantiation, class) pair per implemented instantiation — a class implementing both IRequestHandler<A, B> and IRequestHandler<C, D> is registered twice, once under each.
  • An open generic class (Validator<T> : IValidator<T>) is registered as the typeof-based (IValidator<>, Validator<>) pair, provided it satisfies the same exact-match rule that governs an open generic [Service]. A partially-applied shape such as Handler<T> : IHandler<T, Unit> cannot be expressed as an open generic registration and is skipped.

Inherited implementations count: a class matches when the contract is anywhere in its interface set, including via a base class.

What is skipped, silently

A convention scan describes a shape, not a specific type, so a class that simply does not fit is passed over rather than reported: abstract and static classes, types that are not classes at all, classes not accessible from the generated code (a private nested or file-local class, and anything nested inside one), classes nested inside a generic type (see SSAL003), and open generic classes whose implemented instantiation is not an exact match.

Only mistakes in the declaration are diagnosed — including a contract that ends up matching nothing (SSAL022), so a typo or a namespace mix-up cannot silently register nothing at all.

[Service] opts a class out of the scan — it does not outrank it

A class carrying at least one [Service] attribute is excluded from every convention scan in the assembly, so that class is never registered twice. That doubles as the per-class opt-out: give the class the [Service] registration you actually want — a different lifetime, As type, Key, or Mode — and the scan leaves it alone.

[assembly: RegisterImplementationsOf(typeof(IStartupTask))]

public sealed class WarmCaches : IStartupTask { }          // TryAddEnumerable Singleton, from the scan

[Service(ServiceLifetime.Transient)]
public sealed class PersistStep : IStartupTask { }         // Transient Add, from [Service] alone

What it is not is a resolution-priority rule. The exclusion is per class, so a contract still matches every other implementation of the same service type — and because the convention block is emitted after the [Service] block (see Emission order), Microsoft.Extensions.DependencyInjection's last-registration-wins rule hands a single-instance resolution to the convention, not to the explicit registration:

[assembly: RegisterImplementationsOf(typeof(IClock), Mode = RegistrationMode.TryAdd)]

[Service]
public sealed class ExplicitClock : IClock { }   // emitted first

public sealed class ScannedClock : IClock { }    // emitted second — this is what IClock resolves to

SSAL027 reports exactly this overlap. It is silent for the default Mode = TryAddEnumerable, which is additive by construction and never shadows anything, and for a keyed [Service], which is resolved through a different lookup than the (never keyed) convention registration.

Mode = RegistrationMode.Replace deserves its own warning: IServiceCollection.Replace removes every existing descriptor for the service type before adding its own, so a Replace contract does not merely out-rank an earlier [Service] registration of the same service type — it deletes it. Combined with the emission order, that means a single assembly-level attribute can silently unregister an explicit registration elsewhere in the assembly.

Emission order

The generated Add{Assembly}Services() method writes its registrations in three blocks, always in this order:

  1. every [Service] registration, ordered by implementation type name;
  2. every convention registration, ordered by contract, then implementation type, then service type;
  3. every [ServiceFactory] singleton, ordered by interface name.

This order is part of the package's contract, not an implementation detail: with last-registration-wins, it is what decides which registration a single-instance resolution returns whenever two blocks bind the same service type. Within a block, ordering is by name rather than by source position, so renaming a class can change the winner — which is what SSAL015 and SSAL027 exist to point out.

Why the default Mode is TryAddEnumerable

Registering "every implementation of X" is by nature a multi-implementation pattern whose result is consumed as IEnumerable<X>. TryAddEnumerable is the mode that makes that work without implementations shadowing one another, and it is the one mode SSAL015 never reports as a conflict — so it is this attribute's default, unlike [Service], whose default is Add. Set Mode explicitly for the rarer case where the scan is meant to bind a single implementation:

[assembly: RegisterImplementationsOf(typeof(IClock), Mode = RegistrationMode.TryAdd)]

Each matched service type gets its own independent registration. Unlike a multi-interface [Service], a convention-scanned class is never registered once as its concrete type with the service types forwarded to it, so two service types matched on the same class do not resolve to a shared instance.

Attribute Reference

[Service(lifetime, As = ..., Mode = ..., Key = ..., Factory = ...)]

Property Type Default Description
Lifetime ServiceLifetime ServiceLifetime.Singleton Constructor argument. Singleton, Scoped, or Transient.
As Type? null Registers only as this type instead of every implemented interface (or itself).
Mode RegistrationMode RegistrationMode.Add Add, TryAdd, TryAddEnumerable, or Replace — how the call is applied to the collection.
Key object? null Registers as a keyed service (.NET 8+) using AddKeyed* instead of Add*.
Factory string? null Names a static factory method (declared on the decorated class) that constructs the implementation instance. See Factory methods.

A class can carry multiple [Service] attributes to register it several ways (different lifetimes, As targets, or keys) at once.

[ServiceFactory] takes no arguments and can only be applied to an interface, at most once. See Enum-keyed service factories.

[assembly: RegisterImplementationsOf(contract, lifetime, Mode = ...)]

Property Type Default Description
Contract Type (required) Constructor argument. The interface to scan for implementations of — non-generic, closed generic, or an unbound typeof(IHandler<,>).
Lifetime ServiceLifetime ServiceLifetime.Singleton Constructor argument. The lifetime every matched implementation is registered with.
Mode RegistrationMode RegistrationMode.TryAddEnumerable How each matched registration is applied — note the default differs from [Service]'s.

The attribute is assembly-scoped and can be applied any number of times, once per contract. See Registering every implementation of a contract.

Known limitations

The generated class and method are named after the assembly, with non-identifier characters removed. MyApp.Web becomes MyAppWebServiceCollectionExtensions.AddMyAppWebServices(), which means two assemblies whose names differ only in separators — Foo.Bar and FooBar, or Foo-Bar — produce the same extension class in the same Microsoft.Extensions.DependencyInjection namespace. A project referencing both gets CS0101 (duplicate type) or, if only the method is called, an ambiguous invocation.

This is not worked around, and deliberately so: the generated names are the package's user-facing API, so disambiguating them (with a hash, a counter, or the full assembly name) would rename the method every existing consumer already calls. Rename one of the two assemblies, or call the method through its fully qualified type name.

SSAL015 compares service types as written. An open generic registration (IRepo<>) and a closed one (IRepo<int>) are counted as two different service types, even though a request for IRepo<int> matches both at runtime and the closed registration wins. Merging them into one group would report every assembly that deliberately specializes a single instantiation, so the narrower grouping is kept and the open/closed race is left to the author.

Diagnostics

The generator validates your [Service], [ServiceFactory], and [assembly: RegisterImplementationsOf] usage at compile time:

ID Severity Description
SSAL001 Error [Service] was applied to an abstract or static class.
SSAL002 Error The type specified in As is not implemented by the decorated class.
SSAL003 Error [Service] was applied to a class nested inside a generic type; open generic support requires all of a class's generic context to be its own type parameters.
SSAL004 Warning The same service registration appears to be duplicated.
SSAL005 Error Key was combined with RegistrationMode.TryAddEnumerable, which is not a supported combination.
SSAL006 Error RegistrationMode.TryAddEnumerable cannot register a type as itself — MS DI can't tell duplicates apart without a distinct service type; implement an interface or set As explicitly.
SSAL007 Error Every type the generated code would need to reference — the decorated class, its service type(s), a typeof(...) Key value, and any generic type arguments, including all containing types — must be at least internal and not file-local. Also rejected: a type only reachable through an extern alias (no global alias), and another assembly's protected internal type without [InternalsVisibleTo].
SSAL008 Error An undefined ServiceLifetime or RegistrationMode value (e.g. (ServiceLifetime)42) was supplied to [Service].
SSAL009 Error An open generic class can only be registered as itself or as an implemented interface/base type whose type arguments are exactly its own type parameters, in order; closed, reordered, partial, or wrapped type arguments are rejected.
SSAL010 Warning An open generic class is registered under two or more service types as Singleton or Scoped; each service type resolves to a separate instance because open generic registrations can't use forwarding factories.
SSAL011 Error 'Factory' method not found — no ordinary method with the name given to Factory is declared directly on the decorated class (this also covers an empty-string Factory value).
SSAL012 Error 'Factory' method has an unusable signature — one or more methods with that name exist, but none is static, non-generic, parameterless-or-single-IServiceProvider-parameter, and returning exactly the decorated class.
SSAL013 Error 'Factory' cannot be used on an open generic class — Microsoft.Extensions.DependencyInjection has no factory-based registration API for open generics.
SSAL014 Error 'Factory' method is not accessible to generated code — the chosen factory method must be at least internal so the generated registration code (in a different file, same assembly) can call it.
SSAL015 Warning The same service type (and Key) is registered with two or more different implementation types. A single-instance resolution returns the last registration, and the generator emits registrations ordered by implementation type name — so renaming a class can silently change the winner. Use RegistrationMode.TryAddEnumerable on every implementation if they are meant to be injected together as IEnumerable<T> (such a group is never reported), give them distinct Key values, or suppress the warning if one deliberately overrides the other. Only [Service] registrations are compared, and open and closed generic service types are counted separately — see Known limitations; the [Service]-versus-convention overlap is SSAL027's.
SSAL016 Error [ServiceFactory] was applied to something other than an interface. Normally pre-empted by the compiler's own CS0592 (the attribute's AttributeUsage is Interface); reported as a defence in depth.
SSAL017 Error A [ServiceFactory] interface must declare exactly one member, and that member must be an ordinary, non-static method. Zero members, a second method, a property, an event, or a nested type are all rejected — as is inheriting an interface that has implementable members of its own, which the generated class would have to implement too. A marker base interface is allowed.
SSAL018 Error The [ServiceFactory] method's signature is unusable: it is generic, takes anything other than exactly one by-value enum parameter (ref/out/in included), returns void, or returns by reference.
SSAL019 Error [ServiceFactory] was applied to a generic interface, or to one nested inside a generic type — the generated implementation is a non-generic singleton registered against one closed service type.
SSAL020 Error The factory interface, its enum key type, or its return type is not accessible from the generated implementation (a separate file in the SsalKit.DependencyInjection.Generated namespace, same assembly); each must be at least internal and not file-local.
SSAL021 Error The [assembly: RegisterImplementationsOf] contract is not an interface. A class, struct, enum, delegate, or array type has no set of "implementations" to discover.
SSAL022 Warning An [assembly: RegisterImplementationsOf] contract matched no class at all, and therefore registered nothing. Usual causes: a misspelled or wrong-namespace interface, an expectation that a referenced assembly's classes would be discovered (they are not), or every candidate having been skipped for being abstract, static, inaccessible, nested inside a generic type, or already decorated with [Service].
SSAL023 Error The same contract is declared by two or more [assembly: RegisterImplementationsOf] attributes. Only the first is used; two declarations of one contract have no combined meaning.
SSAL024 Error An undefined ServiceLifetime or RegistrationMode value (e.g. (ServiceLifetime)42) was supplied to [assembly: RegisterImplementationsOf].
SSAL025 Error The contract (or one of its generic type arguments) is not accessible from the generated registration code; it must be at least internal and not file-local. A file-local interface can be named at the attribute application site but never from the generated file.
SSAL026 Warning Two overlapping contracts — typically an unbound typeof(IHandler<>) alongside a closed typeof(IHandler<int>) — match the same class under the same service type but disagree on Lifetime/Mode, so both registrations are emitted and which one wins is decided by Microsoft.Extensions.DependencyInjection rather than by the declarations. Overlapping contracts that agree are collapsed into a single registration and reported nothing.
SSAL027 Warning A [Service] registration and an [assembly: RegisterImplementationsOf] contract bind the same (non-keyed) service type, and the contract's Mode is not TryAddEnumerable. [Service] excludes its own class from the scan but does not outrank it, and the convention block is emitted last — so the convention's registration is what a single-instance resolution returns, and a Replace contract removes the explicit registration outright. See [Service] opts a class out of the scan.
SSAL028 Warning A class registered by [Service] or by a convention scan declares no public constructor. Microsoft.Extensions.DependencyInjection activates an implementation type through its public constructors only, so resolving the service throws at runtime. Make a constructor public, use [Service(Factory = ...)] (never reported, since the generated code calls the method instead of a constructor), or suppress the warning if a third-party container that supports non-public constructors resolves it.

License

MIT — see LICENSE.


AI disclosure: This project was built with AI assistance (Claude).

Product Compatible and additional computed target framework versions.
.NET 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. 
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
0.0.6 118 8/9/2026
0.0.5 116 7/31/2026
0.0.4 117 7/26/2026
0.0.3 109 7/25/2026
0.0.2 118 7/24/2026
0.0.1 114 7/20/2026