SAIF.NetCore.PingableService architecture¶
SAIF.NetCore.PingableService is a Framework.NCore implementation-layer library that adds a framework-standard, anonymous GET /Ping health endpoint to any ASP.NET Core API host; a request to that endpoint pings every DI-registered IPingableService implementation and every MVC controller that implements IPingableService, surfacing a 500 through PingServiceException if any resolution or ping fails. It depends only on the foundation package SAIF.NetCore.ServiceContracts, and it is consumed by SAIF.NetCore.ServiceExtensions and, as one of the three documented implementation-to-implementation exceptions, by SAIF.NetCore.Services.Messaging.
Responsibilities¶
- Expose an inbound, anonymous
GET /PingMVC endpoint (PingableController.Get) that aggregates the health of every pingable service and controller in the host process. - Define
IPingableService, a default-interface-method mixin that gives every implementer a standardPingAsyncorchestration while allowingPing,IsThrowablePingError,OnPingFailure, andOnPingSuccessto be overridden as hooks. - Provide two discovery channels for pingable components: explicit DI registration (
AddPingableTransient/Scoped/Singleton<TService,TImplementation>) and implicit MVC controller discovery (IControllerRetriever, opted into viaWithPingableController()). - Translate any ping failure — including a DI resolution failure for a misconfigured dependency — into a
PingServiceException(HTTP 500). A host built onBaseApiStartupformats that into a response throughSAIF.NetCore.ServiceExtensions'sDynamicErrorHandlermiddleware, which delegates to its injectedIErrorHandler(theErrorHandlerclass); the olderSAIF.NetCore.ErrorHandler's ownInvokemiddleware method is kept only for backwards compatibility and is not installed byBaseApiStartup.
Component boundaries¶
IPingableService: the mixin contract;PingAsyncis the only member with a non-trivial default implementation, runningPingon the thread pool viaTask.Runand invoking the failure/success hooks.PingableController: the hosted endpoint. It resolvesIServiceProviderandIControllerRetriever, runs all registered and controller-discovered pings concurrently withTask.WhenAll, and converts any failure into a thrownPingServiceException.ControllerRetriever/IControllerRetriever: the controller-discovery boundary — reflects over MVC application-part assemblies for types assignable to bothIPingableServiceandControllerBase, then resolves each from the DI container.IServiceCollectionExtensions: the DI-registration boundary for explicitly opted-in pingable services; each call registers the implementation under bothTServiceandIPingableServicewith the same lifetime.MvcBuilderExtensions.WithPingableController(): the host-wiring boundary — adds this library's assembly as an MVCAssemblyPartand registersIControllerRetrieveras a singleton. It is the supported convenience path for the controller-discovery channel; pairing it withAddControllersAsServices()lets pingable controllers resolve by concrete type (BR-14). An equivalent explicit application-part registration plus a directIControllerRetrieverregistration satisfies the same prerequisites without this call.PingServiceException: extendsServiceContracts'HttpStatusCodeException(itself extendingSAIFException); its constructor initializes the inheritedStatusCodeproperty to500, but that property keeps its public setter and remains mutable after construction.
Interaction model¶
- A host composes this library at startup by calling
services.AddControllers().WithPingableController().AddControllersAsServices()(or the equivalent inBaseApiStartup), and optionally registers application-specific pingable services withAddPingableTransient/Scoped/Singleton. This is the supported convenience path, not the only wiring that works: the/Pingroute needs onlyPingableController's assembly present as an MVC application part and anIControllerRetrieverregistered, and plain DI-registeredIPingableServiceservices need neitherWithPingableController()norAddControllersAsServices()at all — only pingable controllers resolved through the controller-discovery channel need theAddControllersAsServices()pairing (BR-13, BR-14). - At request time,
PingableController.Getresolves everyIPingableServicefrom DI (GetServices<IPingableService>()) and every controller-discovered type (ControllerRetriever.GetControllers), starts all pings concurrently via each implementation'sPingAsync, and awaits them withTask.WhenAll. The awaited aggregation (faulted vs. canceled), and a synchronous failure during resolution or duringTask.WhenAll's own enumeration of the projection, are distinct outcomes handled differently — see OP-01 steps 6–7. - A missing dependency, a constructor failure, a synchronous throw, a faulted awaited task, or a canceled awaited task all become a thrown
PingServiceException, which this library's own code throws rather than converts into an HTTP response. In hosts built onBaseApiStartup,SAIF.NetCore.ServiceExtensions'sDynamicErrorHandlermiddleware catches it and delegates to its injectedIErrorHandler(theErrorHandlerclass) to format the 500 response; the legacySAIF.NetCore.ErrorHandlerInvokemiddleware method is kept only for backwards compatibility and is not installed byBaseApiStartup.Origin: Converting the thrown exception into an HTTP response is implemented in Dynamic error handler invocation (
SAIF.NetCore.ServiceExtensions) and executed on behalf of this library's failure path. - This library makes no outbound calls of its own; it is purely an inbound, hosted health-check surface.
SAIF.NetCore.Services.Messagingconsumes it only through the controller-discovery channel (BackgroundConsumerServicesController : ControllerBase, IPingableService), not through any direct runtime call into this library's implementation code.
Architecture diagram¶
The following diagram shows how a host wires the ping endpoint and how it reaches registered and controller-discovered pingable components.
flowchart TB
Client["HTTP client"]
PC["PingableController<br>GET /Ping"]
CR["ControllerRetriever<br>(IControllerRetriever)"]
DI["IServiceProvider<br>(DI container)"]
SVC["Registered IPingableService<br>implementations"]
CTRL["MVC controllers implementing<br>IPingableService"]
EH["DynamicErrorHandler -><br>injected IErrorHandler<br>(host-installed via BaseApiStartup)"]
Client -->|GET /Ping| PC
PC -->|GetServices<IPingableService>| DI
DI --> SVC
PC -->|GetControllers| CR
CR -->|GetService by type| CTRL
PC -->|on failure: throw<br>PingServiceException| EH
Sequence diagram¶
The following diagram shows the primary request/response sequence for a ping request.
sequenceDiagram
participant Client
participant PingableController
participant DIContainer as DI container
participant ControllerRetriever
participant Pingables as IPingableService implementations
Client->>PingableController: GET /Ping?guid=<runId>
PingableController->>DIContainer: GetServices<IPingableService>()
DIContainer-->>PingableController: registered services
PingableController->>ControllerRetriever: GetControllers(provider)
ControllerRetriever-->>PingableController: controller-discovered services
PingableController->>Pingables: PingAsync(guid, logger) for each (parallel)
Pingables-->>PingableController: Task.WhenAll result
alt all awaited tasks succeed
PingableController-->>Client: 200 OK (guid)
else one or more awaited tasks faulted
PingableController->>PingableController: flatten AggregateException,<br>throw new PingServiceException
PingableController-->>Client: 500 (via DynamicErrorHandler -> IErrorHandler)
else awaited tasks canceled (whenAll.Exception is null)
PingableController->>PingableController: rethrow cancellation,<br>wrapped as PingServiceException
PingableController-->>Client: 500 (via DynamicErrorHandler -> IErrorHandler)
else synchronous failure (resolution, reflection, or pre-Task throw)
PingableController->>PingableController: caught directly,<br>wrapped as PingServiceException
PingableController-->>Client: 500 (via DynamicErrorHandler -> IErrorHandler)
end
Design constraints¶
WithPingableController()paired withAddControllersAsServices()is the supported path for the controller-discovery channel; withoutAddControllersAsServices()(or an equivalent explicit registration),ControllerRetriever.GetService(type)returnsnullfor any pingable controller, and the resultingNullReferenceExceptionis wrapped into aPingServiceExceptionrather than failing fast at startup*. This constraint applies only when the host has pingable controllers to discover — plain DI-registeredIPingableServiceservices are unaffected.- All pings for a single request start concurrently and are awaited together; one slow or hanging
PingAsyncimplementation delays the entire/Pingresponse. - The endpoint is anonymous (
[AllowAnonymous]) by design, since it is a health check; it carries no authentication or authorization of its own (see Security and authorization summary). - Converting a thrown
PingServiceExceptioninto a formatted HTTP 500 depends on the host havingDynamicErrorHandler(or an equivalentIErrorHandlerpipeline) installed, whichBaseApiStartup-derived hosts do by default; without it, ASP.NET Core's default unhandled-exception handling still yields a 500, but without SAIF formatting*.
Related documentation¶
- Index
- Overview
- Dependencies
- Operations and processes
- Business rules
- Security and authorization
- Service registration
- API reference
- SAIF.NetCore.ServiceContracts
- Dynamic error handler invocation (
SAIF.NetCore.ServiceExtensions)
Note: Statements marked with
*were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts.
Sources¶
src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/IPingableService.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Services/ControllerRetriever.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/IServiceCollectionExtensions.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/MvcBuilderExtensions.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Exceptions/PingServiceException.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/BaseApiStartup.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cssrc/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.csArchitecture.md