Skip to content
Introducing AI Conversations: Natural Language Interaction for Your Apps! Learn More

Advanced Registration

Register a class as itself rather than against its interface:

[Singleton(AsSelf = true)]
public class AppState : IStandardInterface;
// Registers as: services.AddSingleton<AppState>();
// NOT as: services.AddSingleton<IStandardInterface, AppState>();

When a class implements multiple interfaces and is registered as Singleton or Scoped, Shiny automatically registers it against all interfaces with shared instance resolution:

[Singleton]
public class MultiService : IServiceA, IServiceB { }
// Both IServiceA and IServiceB resolve to the same MultiService instance

To target a specific interface instead:

[Singleton(Type = typeof(IServiceA))]
public class MultiService : IServiceA, IServiceB { }
// Only registered against IServiceA

Register services with a key for disambiguation:

[Singleton(KeyedName = "primary")]
public class PrimaryCache : ICache { }
[Singleton(KeyedName = "fallback")]
public class FallbackCache : ICache { }

Prevent overwriting an existing registration:

[Singleton(TryAdd = true)]
public class DefaultLogger : ILogger { }
// Uses TryAddSingleton — won't replace an ILogger that's already registered

Classes attributed with [Singleton]/[Scoped]/[Transient]/[Service] are emitted in factory form — the generator expands the constructor and wires DI lookups at compile time. No reflection at runtime.

[Singleton]
public class MyService(IDep dep) : IMyService { }
// Generated:
// services.AddSingleton<IMyService>(sp => new MyService(sp.GetRequiredService<IDep>()));

Constructor selection matches ActivatorUtilities rules:

  • [ActivatorUtilitiesConstructor] is honored
  • Otherwise, the constructor with the most parameters is picked
  • [FromKeyedServices("key")] parameters resolve via GetRequiredKeyedService<T>("key")
  • IServiceProvider parameters are passed through directly

Multi-interface classes emit explicit forwarders rather than relying on reflection:

[Singleton]
public class MyService : IFoo, IBar { }
// Generated:
// services.AddSingleton<MyService>(sp => new MyService());
// services.AddSingleton<IFoo>(sp => sp.GetRequiredService<MyService>());
// services.AddSingleton<IBar>(sp => sp.GetRequiredService<MyService>());

Because every attributed registration is factory-based, resolve chains like OnResolved and BindOnResolve compose naturally.

Chain a callback onto the most recently registered factory-based service. The callback fires after the container constructs the instance but before it’s returned. Fires once per construction: singleton → once total, scoped → once per scope, transient → every resolve.

services
.AddSingleton<IFoo>(sp => new Foo())
.OnResolved<IFoo>((foo, sp) => foo.Configure(sp.GetRequiredService<IOptions>()));
// Action<TService> overload when the IServiceProvider isn't needed
services
.AddSingleton<IFoo>(sp => new Foo())
.OnResolved<IFoo>(foo => foo.Initialize());

The chain is AOT-clean — it wraps the factory directly. Type-based registrations (AddSingleton<TService, TImpl>()) and pre-built instance registrations are rejected at registration time; use a factory-based form instead (or rely on the source generator, which always emits factory form).