Skip to content

Registration strategies

Choose which service types a class is registered as, and what happens when a registration already exists.

4 min read

Two independent knobs control registration. RegistrationStrategy decides which service types a class is registered as. DuplicateStrategy decides what happens when the collection already contains a registration for one of them.

ServiceType vs RegistrationStrategy

The non-generic attribute form offers both, and they are mutually exclusive. Setting both is INJ0003 — an error, and nothing is emitted.

// Register as exactly one service type
[RegisterScoped(ServiceType = typeof(ITodoRepository))]

// Register as a computed set of service types
[RegisterScoped(RegistrationStrategy = RegistrationStrategy.ImplementedInterfaces)]

// Error: INJ0003
[RegisterScoped(ServiceType = typeof(ITodoRepository), RegistrationStrategy = RegistrationStrategy.Self)]

ServiceType is the non-generic equivalent of [RegisterScoped<ITodoRepository>]. Prefer the generic form when the service type is known at compile time; ServiceType earns its keep for open generics, where typeof(IRepository<>) has no generic-attribute equivalent.

The four RegistrationStrategy members

MemberRegisters as
NoneThe ServiceType, if one is given; otherwise identical to Self
SelfThe attributed class itself
ImplementedInterfacesEvery interface the class implements — not the class itself
SelfAndImplementedInterfacesThe class itself and every interface it implements

None is the enum’s zero value and acts as the “nothing chosen” sentinel. You should not write it explicitly; leaving RegistrationStrategy unset produces the same result.

How the effective strategy is resolved

For a given attribute, in order:

  1. RegistrationStrategy on the attribute, if written.
  2. Otherwise None, if ServiceType is set on the attribute.
  3. Otherwise the assembly-wide RegistrationDefaults.RegistrationStrategy.
  4. Otherwise None.

Note step 2: an attribute that sets ServiceType never inherits the assembly default. That is what makes ServiceType usable in an assembly whose default is SelfAndImplementedInterfaces without the two fighting.

Self

Self.cs
using Immediate.Injections.Shared;

[RegisterScoped(RegistrationStrategy = RegistrationStrategy.Self)]
public sealed class UnitOfWork : IUnitOfWork;

Emits one registration, UnitOfWorkUnitOfWork. IUnitOfWork is not resolvable. This is also what a bare [RegisterScoped] does.

ImplementedInterfaces

ImplementedInterfaces.cs
using Immediate.Injections.Shared;

public interface IFirstService;
public interface ISecondService;

[RegisterSingleton(RegistrationStrategy = RegistrationStrategy.ImplementedInterfaces)]
public sealed class InterfacesOnlyService : IFirstService, ISecondService;

Emits IFirstServiceInterfacesOnlyService and ISecondServiceInterfacesOnlyService. GetService<InterfacesOnlyService>() returns null.

“Implemented interfaces” means every interface in the type’s transitive interface set, including ones inherited from base classes and from other interfaces — not only the ones named in the class declaration.

SelfAndImplementedInterfaces

SelfAndImplementedInterfaces.cs
using Immediate.Injections.Shared;

public interface ISharedService;

[RegisterSingleton(RegistrationStrategy = RegistrationStrategy.SelfAndImplementedInterfaces)]
public sealed class SharedService : ISharedService;

Emits SharedServiceSharedService first, then ISharedServiceSharedService. Still two descriptors, so still two singleton instances unless you add UseProxyFactory = true.

This is the strategy to set as an assembly default if you are migrating from a library whose default was “register as self and all interfaces” — see Migrating from other libraries.

The three DuplicateStrategy members

DuplicateStrategy picks which ServiceCollectionDescriptorExtensions method the generated code calls.

MemberGenerated callEffect
Append (default)services.Add(descriptor)Adds unconditionally; both registrations survive
Skipservices.TryAdd(descriptor)Adds only if no registration for that service type exists
Replaceservices.Replace(descriptor)Removes the first existing descriptor for that service type, then adds
DuplicateStrategy.cs
using Immediate.Injections.Shared;

public interface IEmailSender;

// Loses to anything already registered
[RegisterSingleton<IEmailSender>(DuplicateStrategy = DuplicateStrategy.Skip)]
public sealed class DefaultEmailSender : IEmailSender;

// Wins over whatever was registered first
[RegisterSingleton<IEmailSender>(DuplicateStrategy = DuplicateStrategy.Replace)]
public sealed class TestEmailSender : IEmailSender;

Three things worth knowing:

  • “Already exists” means at the moment AddXxxServices runs. Registrations added to the collection after that call are not affected by Skip or Replace.
  • Replace removes only the first match. If three descriptors already exist for IEmailSender, two of them survive alongside the new one. It is a swap, not a clear-and-set.
  • Order within the generated method is not something to rely on. Registrations are grouped by tag set and emitted per lifetime and per attribute arity, across several generated files.

The effective value is the attribute’s, falling back to the assembly-wide RegistrationDefaults.DuplicateStrategy, falling back to Append. Writing DuplicateStrategy = DuplicateStrategy.Append explicitly does override an assembly default of Replace.

Where to go next