Skip to content

OP-03: Register a pingable service

Field Value
Operation ID OP-03
Trigger Host startup code calls AddPingableTransient<TService, TImplementation>(), AddPingableScoped<TService, TImplementation>(), or AddPingableSingleton<TService, TImplementation>().
Inputs IServiceCollection; generic type arguments TService, TImplementation.
Outputs Two DI registrations on the same IServiceCollection.
Primary components IServiceCollectionExtensions
Entry point AddPingableTransient / AddPingableScoped / AddPingableSingleton

Steps

  1. The generic constraints where TService : class, IPingableService and where TImplementation : class, TService, IPingableService are checked at compile time, so TService itself must be (or extend) IPingableService, and TImplementation must implement both TService and IPingableService (BR-11).
  2. Register TImplementation under TService with the requested lifetime (transient, scoped, or singleton) — e.g. services.AddTransient<TService, TImplementation>().
  3. Register the forwarding factory services.AddTransient<IPingableService, TImplementation>(x => (TImplementation)x.GetRequiredService<TService>()) (substituting AddScoped/AddSingleton for the other two methods), with the same lifetime as step 2, so Provider.GetServices<IPingableService>() in OP-01 step 3 can find it. The factory does not create a second instance — it resolves TService from the same provider/scope and casts the result to TImplementation (BR-15).

Process flow

flowchart TD
    A[Host calls AddPingableTransient/Scoped/Singleton] --> B["Compile-time constraints: TService : IPingableService;<br>TImplementation : TService, IPingableService"]
    B --> C[Register TImplementation under TService]
    C --> D["Register IPingableService with forwarding factory:<br>x =&gt; (TImplementation)x.GetRequiredService&lt;TService&gt;()"]
    D --> E[Both registrations share the same lifetime]

Failure and edge-case behavior

  • For Transient, the forwarding factory still produces a fresh TImplementation instance per resolution (each GetRequiredService<TService>() call creates a new one), so TService and IPingableService resolutions are independent instances, consistent with transient semantics.
  • For Scoped, every resolution of IPingableService within a given scope shares the identical TImplementation instance that TService resolves to in that same scope — the forwarding factory already provides this identity sharing; a host never needs to add its own factory for it to work.
  • For Singleton, every resolution of IPingableService for the lifetime of the application shares the identical singleton instance that TService resolves to.
  • If a caller replaces the TService registration independently (bypassing AddPingable*, e.g. by calling services.AddScoped<TService, OtherImplementation>() afterward), the forwarding factory's cast (TImplementation)x.GetRequiredService<TService>() fails at ping time with an InvalidCastException, which surfaces as a 500 via BR-05 (BR-15).

Sources

  • src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/IServiceCollectionExtensions.cs