OP-01: Ping the API¶
| Field | Value |
|---|---|
| Operation ID | OP-01 |
| Trigger | GET /Ping or GET /Ping?guid=<runId>, anonymous ([AllowAnonymous]). |
| Inputs | Optional guid query parameter; constructor-injected IServiceProvider and IControllerRetriever; optional ISaifLogService. |
| Outputs | 200 OK with the run-id string as the body, or a thrown PingServiceException (status 500). |
| Primary components | PingableController |
| Entry point | PingableController.Get |
Steps¶
- MVC activates
PingableControllerwithIServiceProviderandIControllerRetriever; the constructor resolvesISaifLogServicewith a null-tolerantGetServicecall and assigns it only if non-null (BR-12, BR-13). IfIControllerRetrieveris not registered, MVC activation fails beforeGetruns.* - If
guid == null, setguid = Guid.NewGuid().ToString()(BR-02). - Inside a single
tryblock spanning steps 3–6: callProvider.GetServices<IPingableService>()to resolve every service registered throughAddPingable*(BR-03, BR-05); see OP-03. A DI activation failure here (for example, an unresolved constructor dependency on a registeredIPingableService) throws synchronously and is caught directly by step 7's exception handling. - Discover and resolve pingable controllers via
IControllerRetriever.GetControllers(Provider)and concatenate them onto the service list (BR-03). Locally,GetControllerscallsAssembly.LoadandGetTypes()over every application part snapshotted at registration time (no caching), filters for every type implementing bothIPingableServiceandControllerBase, and resolves an instance of each discovered concrete type from DI viaIServiceProvider.GetService(type), returning the resolved instances to this step (BR-14). A DI or reflection failure during this discovery/resolution work propagates the same way as step 3.Origin: The assembly-load/type-filtering/concrete-resolution substeps above are implemented in OP-04 steps 2–4 (
ControllerRetriever.GetControllers,SAIF.NetCore.PingableService), executed locally within this library on behalf of this operation. - Project the combined list to
services.Select(x => x.PingAsync(guid, SaifLogService)). This projection is lazy — thePingAsynccall on each target only actually executes when the resultingIEnumerable<Task>is enumerated in step 6, not at the point theSelectis written (BR-04). For a target whoseIPingableService.PingAsyncis the library's default interface implementation (not overridden), that call locally runsTask.Runto invokePingand then branches on the outcome (BR-07, BR-08): - If
Pingthrows andIsThrowablePingErrorclassifies the exception astrue, the default implementation invokesOnPingFailurewith the original exception and then reaches a barethrow;— the original exception is rethrown unchanged only ifOnPingFailureitself returns/completes normally; ifOnPingFailurethrows instead, its exception replaces the original and propagates in its place. - If
Pingthrows andIsThrowablePingErrorclassifies the exception asfalse, the default implementation swallows the exception without rethrowing and falls through to invokeOnPingSuccess. - If
Pingcompletes without throwing, the default implementation proceeds directly to invokeOnPingSuccess— the same hook reached by thefalse-classification branch above. - If
IsThrowablePingErroritself throws while classifying the exception, orOnPingFailureitself throws while handling atrue-classified failure, that new exception replaces the originally caught exception as the one that propagates out of theTask.Rundelegate. - If
OnPingSuccessthrows — whether reached via the clean-Pingpath or thefalse-classification path — that exception also propagates out of theTask.Rundelegate, faulting the resulting task even though no earlier step failed.
A target that overrides PingAsync bypasses this entire default implementation; the default's Task.Run/classifier/hook branching above does not apply, and behavior is instead supplied by the overriding implementation. See OP-02 steps 1–5 for the full substep sequence underlying the default implementation described above.
Origin: The default
Task.Run/Ping/classifier/hook-invocation substeps above are implemented in OP-02 (IPingableService.PingAsyncdefault interface implementation,SAIF.NetCore.PingableService), executed locally within this library on behalf of this operation. 6.await RunAllPings(guid, tasks): 1.RunAllPingscallsTask.WhenAll(tasks). Enumeratingtaskshere is what actually invokes each target'sPingAsync; if a target throws synchronously before returning aTask(rather than returning a task that later faults), that exception propagates out ofTask.WhenAll(tasks)itself, bypassingRunAllPings's owntry/catch, and is caught byGet's step 7 handling instead. 2.await whenAllcan throw for two distinct reasons, andRunAllPings'scatchblock distinguishes them by readingwhenAll.Exception: - One or more awaited tasks faulted.whenAll.Exceptionis non-null;RunAllPingsreads it as anAggregateExceptionand throwsnew PingServiceException(aex.Message, aex.Flatten(), guid)— this is the only placeRunAllPingsitself explicitly flattens a concurrent-failure aggregate viaaex.Flatten()into aPingServiceException's inner exception (BR-04, BR-10). It is not the only possible source of aPingServiceExceptionwhose inner exception happens to be anAggregateException— see the synchronous-failure case below, where a target that throws anAggregateExceptiondirectly (before returning aTask) is wrapped intact byGet's owncatch (Exception ex)without passing through this flattening step at all. - One or more awaited tasks were canceled (not faulted).whenAll.Exceptionisnull;RunAllPings'scatchblock falls through to its barethrow;, rethrowing the original cancellation exception (for exampleTaskCanceledException) unwrapped — this is not aggregated into aPingServiceExceptionbyRunAllPingsitself. 7. Back inGet,catch (PingServiceException p) { throw; }rethrows unchanged only thePingServiceExceptionproduced by step 6.2's faulted-aggregation branch (or one thrown synchronously and already of that type); it is not re-wrapped a second time. Any other exception type — a DI/reflection failure from steps 3–4, a synchronous throw from step 6.1, or the unwrapped cancellation exception from step 6.2's cancellation branch — is caught by the subsequentcatch (Exception ex)and wrapped individually asnew PingServiceException(ex.Message, ex, guid)(BR-05, BR-06, BR-10); see OP-05. 1. Converting the thrownPingServiceExceptioninto an HTTP response is host-dependent. In hosts built onBaseApiStartup,app.ConfigureErrorHandling()addsDynamicErrorHandlerto the pipeline.DynamicErrorHandler.InvokeAsynccatches theHttpStatusCodeExceptionand calls its ownLogAndReturnException, which: - invokes every registeredIErrorHandlerHookin turn, collecting any hook exception rather than letting it abort the response; - if one or more hooks failed, builds a newAggregateExceptionfrom those hook failures plus the originalPingServiceExceptionand passes that aggregate — instead of the original exception — to the injectedIErrorHandler; otherwise it passes the original exception through unchanged; - delegates to the injectedIErrorHandler(theErrorHandlerclass)'sLogAndReturnException, which first callsIsTooHotToHandleto checkcontext.Response.HasStarted— if the response has already started, it logs a Warning and re-throws the exception immediately, writing no body and skipping the remaining steps; otherwise it logs the error at Error level and captures the returnedErrorIDbefore touching the response, then clears the response, sets theError-ID/Server-Nameheaders and status/content type, and finally writes the body viaBuildHandledResponseMessage, which branches on the responsecontentType: when it contains"text/plain"(PingServiceException's inherited default) the body is"{ex.Message}\nPlease see error: {ErrorID}."; for any other content type the body isex.Messagealone, with no appended error-ID line. > Origin: The hook-invocation/failure-aggregation behavior above is implemented in Dynamic error handler invocation (SAIF.NetCore.ServiceExtensions); the already-started-response rejection and log-before-response sequence are implemented in Log an exception and write the HTTP error response (SAIF.NetCore.ErrorHandler). Both are executed on behalf of this operation. 8. On success (no exception reached step 7), returnOk(guid)—200 OKwith the run id as the body (BR-09).
Process flow¶
flowchart TD
A[GET /Ping received] --> B["Construct controller, resolve optional ISaifLogService"]
B --> C{guid provided?}
C -->|No| C1[Generate new GUID]
C -->|Yes| D[Use provided guid]
C1 --> E["Resolve DI-registered IPingableService targets"]
D --> E
E -->|"DI/reflection failure"| X1["Thrown synchronously -<br>caught by outer catch"]
E --> F["Discover + resolve pingable controllers"]
F -->|"DI/reflection failure"| X1
F --> G["Build lazy PingAsync projection"]
G --> H["Task.WhenAll(tasks) enumerates projection -<br>invokes each PingAsync"]
H -->|"synchronous throw before Task returned"| X1
H --> I["await whenAll"]
I --> J{"await whenAll threw?"}
J -->|No| K[Return 200 OK with guid]
J -->|"Yes, whenAll.Exception != null<br>(faulted)"| L["RunAllPings wraps aex.Flatten()<br>into new PingServiceException"]
J -->|"Yes, whenAll.Exception == null<br>(canceled)"| L2["RunAllPings rethrows the<br>cancellation exception unwrapped"]
L --> M{"Caught by Get as<br>PingServiceException?"}
X1 --> N{"Already a PingServiceException?"}
L2 --> N
M -->|Yes| O["catch (PingServiceException p) { throw; } -<br>rethrown unchanged"]
N -->|Yes| O
N -->|No| P["catch (Exception ex) -<br>wrapped individually, new PingServiceException(ex.Message, ex, guid)"]
O --> Q["DynamicErrorHandler: run hooks, aggregate failures"]
P --> Q
Q --> R{"Response already started?"}
R -->|Yes| S["ErrorHandler logs Warning,<br>re-throws, no body written"]
R -->|No| T["ErrorHandler logs Error, captures ErrorID,<br>writes headers + body"]
Failure and edge-case behavior¶
- Three distinct failure paths exist and must not be conflated:
- Faulted-task aggregation: when one or more of the awaited
PingAsynctasks completes Faulted,RunAllPingscatches theawait whenAllfault, readswhenAll.Exceptionas anAggregateException, and throwsnew PingServiceException(aex.Message, aex.Flatten(), guid). This is the only path whereRunAllPingsitself explicitly flattens concurrently-faulted targets into theAggregateExceptioninner exception viaaex.Flatten()— it is not the only path that can leave aPingServiceExceptionwith anAggregateExceptioninner exception; see Synchronous failures below. - Cancellation: when one or more of the awaited tasks is canceled rather than faulted,
whenAll.Exceptionisnull, andRunAllPings'scatchblock falls through to a barethrow;, rethrowing the cancellation exception unwrapped. This is not aPingServiceExceptionuntilGet's owncatch (Exception ex)wraps it. - Synchronous failures: a DI/reflection failure while resolving
IPingableServices or pingable controllers (steps 3–4), or a target throwing synchronously before returning aTaskduringTask.WhenAll(tasks)'s enumeration of the projection (step 6.1), bypassesRunAllPings'sawait-scopedcatchentirely and is caught directly byGet's owntry/catch. A synchronousAggregateExceptionthrown by a target before returning aTaskis caught the same way — passed intact toGet'scatch (Exception ex)and wrapped asnew PingServiceException(ex.Message, ex, guid)— rather than flattened byRunAllPings. - "An existing
PingServiceExceptionis never double-wrapped" is true only for thePingServiceExceptionthat step 6.2's faulted-aggregation branch (or an already-typed synchronous failure) produces and that reachesGet'scatch (PingServiceException p) { throw; }block directly (BR-06). It does not mean aPingServiceExceptionthrown by a nestedIPingableService/pingable-controller implementation is preserved standalone when it faults as part of the awaited batch: in that caseTask.WhenAllaggregates it into anAggregateException, andRunAllPingswraps that aggregate into a new outerPingServiceExceptionwhoseFlatten()-ed inner exception includes the original. - Without
DynamicErrorHandler(or an equivalentIErrorHandlerpipeline) registered, ASP.NET Core's default unhandled-exception handling still produces a 500, but without the SAIF-formatted body,Error-IDheader, orServer-Nameheader. - A hook registered with
DynamicErrorHandlerthat itself throws does not suppress the originalPingServiceException— both are aggregated and passed toErrorHandlertogether — and a response that has already started by the timeErrorHandler.LogAndReturnExceptionruns skips the Error log entry and body write entirely, logging only a Warning and re-throwing instead. - The host-visible response body for a handled exception depends on the response
contentType: for the"text/plain"default it isex.Messageplus an appended error-ID line; for any other content type it isex.Messagealone, with no error-ID line appended — see PingServiceException.
Related documentation¶
- Operations and processes
- Business rules
- Service registration
- Dynamic error handler invocation (
SAIF.NetCore.ServiceExtensions) - Log an exception and write the HTTP error response (
SAIF.NetCore.ErrorHandler)
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/Controllers/PingableController.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/BaseApiStartup.cssrc/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs