No in-repo test exercises /Ping against an active authorization requirement; the test hosts named SecureStartup and SecureStartupWithScopedAndSingleton configure a secure configuration client, not authorization, so they do not corroborate that [AllowAnonymous] overrides an active authorization requirement. BR-01 remains verified directly from the [AllowAnonymous] attribute itself.
BR-03 Ping set defined by DI registration and MVC application parts¶
Field
Value
Statement
The ping set is (a) every DI registration resolvable under the IPingableService service type — whether made via an AddPingable* convenience extension or an equivalent direct registration such as services.AddScoped<IPingableService, T>() — plus (b) every type in the snapshotted MVC application parts that is both an IPingableService and a ControllerBase.
A service that implements IPingableService but is registered only under a different service type (for example services.AddScoped<IMyService, T>() with no registration under IPingableService itself) is not resolved by GetServices<IPingableService>() and so is not pinged, regardless of whether an AddPingable* extension was used.
All targets are pinged concurrently via Task.WhenAll. The request succeeds only if every awaited ping task completes successfully; any awaited task that faults fails the whole request.
When one or more awaited PingAsync tasks fault, RunAllPings flattens them into one PingServiceException via aex.Flatten() (BR-10). A synchronous failure during target resolution or during Task.WhenAll(tasks)'s own enumeration of the targets (rather than a faulted awaited task) is not part of this aggregation — it is caught and wrapped individually by Get's own catch (Exception ex) (BR-05); see OP-01 step 6 and OP-05.
BR-05 Resolution and construction failures count as ping failures¶
Field
Value
Statement
A failure to construct or resolve any pingable target (constructor exception, missing dependency), or a target that throws synchronously before returning a Task, is a ping failure.
Trigger
Resolution in GetServices/GetService, or synchronous enumeration of the PingAsync projection
Enforcement
PingableController.Get (catch (Exception ex))
Outcome
500 PingServiceException wrapping the resolution or synchronous exception individually — distinct from the Task.WhenAll aggregation path in BR-04.
A PingServiceException that reaches PingableController.Get's try/catch directly — the one RunAllPings constructs from a faulted Task.WhenAll aggregation, or one thrown synchronously and already of that type — is not wrapped again.
The original ping exception (with the flattened aggregate, if any) propagates unchanged. This does not apply to a PingServiceException thrown by a nested IPingableService/pingable-controller implementation that faults as part of the awaited batch — Task.WhenAll aggregates that into an AggregateException, and RunAllPings wraps the aggregate into a new outer PingServiceException.
By default, any exception thrown by Ping is a failure. Implementers can veto it by returning false from IsThrowablePingError. When it is a failure, OnPingFailure runs and, provided OnPingFailure completes without itself throwing, the original exception is rethrown unchanged; if OnPingFailure (or IsThrowablePingError) itself throws, that new exception propagates instead and the original exception is not rethrown.
An exception that IsThrowablePingError classifies as non-throwable is swallowed, and, provided OnPingSuccess completes without itself throwing, the ping counts as successful. OnPingSuccess is invoked; if OnPingSuccess itself throws, the task faults with that new exception despite the original Ping exception having been swallowed.
BR-10 Ping failure returns 500 with run id and message¶
Field
Value
Statement
A ping failure is reported with HTTP status 500 and the message "Ping error: {guid}\n{message}", with the original exception (or the flattened AggregateException) as the inner exception.
Trigger
Any failure path
Enforcement
PingServiceException constructor
Outcome
StatusCode = 500, ContentType = "text/plain" (inherited default from SAIF.NetCore.ServiceContractsHttpStatusCodeException).
BR-11 Pingable registration requires TService to extend IPingableService¶
Field
Value
Statement
A service can be registered as pingable under a given TService only if TService extends IPingableService and its implementation implements that service type. The AddPingable* extensions register it under both TService and IPingableService, both at the same lifetime (Transient, Scoped, or Singleton depending on which AddPingable* method is called); the IPingableService entry always resolves through the TService registration via a factory and casts the result to TImplementation. For Transient this yields two independent instances per resolution, not shared identity; for Scoped and Singleton the IPingableService entry resolves to the identical instance as TService within that scope or for the application's lifetime. An equivalent effect can also be achieved with direct registration calls, but only if the IPingableService registration reproduces this same forwarding-through-TService behavior (rather than merely registering the same implementation type under both services independently, which would not share identity at Scoped/Singleton lifetime and would defeat the purpose of the pattern).
Trigger
AddPingable* call, or equivalent direct dual registration that reproduces the forwarding behavior
Enforcement
IServiceCollectionExtensions.AddPingableTransient/Scoped/Singleton generic constraints and bodies
Outcome
Compile-time error if the AddPingable* constraints are violated. Two registrations on success (one under TService, one under IPingableService via the forwarding factory), sharing instance identity at Scoped/Singleton lifetime but not at Transient lifetime; see Service registration and OP-03.
BR-13 WithPingableController enables route and controller discovery¶
Field
Value
Statement
WithPingableController() is the supported convenience path for both enabling the /Ping route and registering IControllerRetriever for controller-based pinging. Only application parts present when it is called are scanned; this is a property of the application-part snapshot WithPingableController() reads, not of any particular convenience method used elsewhere to register IPingableService instances. An equivalent effect — adding PingableController's assembly as an MVC application part and registering an IControllerRetriever implementation directly — can also satisfy these same two prerequisites without calling WithPingableController().
Trigger
Host MVC configuration
Enforcement
MvcBuilderExtensions.WithPingableController
Outcome
Without WithPingableController() or an equivalent explicit application-part registration, /Ping is not routed and IControllerRetriever is not registered, so PingableController fails MVC activation. Application parts registered after the snapshot is taken are not scanned.*
BR-14 Controller discovery requires controllers registered as services¶
Field
Value
Statement
Pingable controllers are resolved from DI by concrete type, so the controller-discovery channel only works for a given host when the host has registered controllers as services (AddControllersAsServices(), or an equivalent explicit DI registration for each pingable controller's concrete type) — this is a prerequisite for the controller-discovery channel specifically, independent of whether any IPingableService was registered via AddPingable* or direct registration. If the host has no pingable controllers to discover, this prerequisite has no effect.
Trigger
Each ping request, when at least one pingable controller exists
For a host with at least one pingable controller, a missing AddControllersAsServices()/equivalent registration leads to a null entry and a NullReferenceException on PingAsync, which surfaces as a 500 (via BR-05).
BR-15 IPingableService resolution casts through TService registration¶
Field
Value
Statement
The IPingableService registration always resolves the instance through the TService registration and casts it to TImplementation. If a later registration replaces TService with a different implementation type, the cast fails at ping time. For Transient and Scoped lifetimes, the pinged instance is a separate (or scope-shared) instance resolved for the ping request.
Trigger
Ping-time resolution
Enforcement
IServiceCollectionExtensions factory lambdas
Outcome
InvalidCastException, which surfaces as a 500 via BR-05, if TService is later re-registered with a different implementation.*
Note: Statements marked with * were inferred from code structure, naming
conventions, or contextual evidence where direct confirmation was not available
in source artifacts.