Skip to content

Business rules

15 business rules govern ping request composition, concurrency, failure classification, registration, and host-wiring prerequisites.

BR-01 Anonymous access to ping endpoint

Field Value
Statement The ping endpoint is reachable without authentication or authorization.
Trigger GET /Ping
Enforcement PingableController.Get, [AllowAnonymous]
Outcome Global authorization policies and fallback policies do not apply to /Ping.
Enforcing operation(s) OP-01
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs

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-02 Run id defaults to a generated GUID

Field Value
Statement If the caller supplies no run id, a new GUID string is generated and used for every hook, error message, and response.
Trigger guid == null
Enforcement PingableController.Get
Outcome The run id is always present in the 200 body or the 500 message.
Enforcing operation(s) OP-01
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs

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.
Trigger Each ping request
Enforcement PingableController.Get; ControllerRetriever.GetPingableControllerTypes
Outcome 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.
Enforcing operation(s) OP-01, OP-04
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs, src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Services/ControllerRetriever.cs

BR-04 Concurrent ping execution via Task.WhenAll

Field Value
Statement 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.
Trigger Each ping request
Enforcement PingableController.Get; PingableController.RunAllPings
Outcome 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.
Enforcing operation(s) OP-01
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs

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.
Enforcing operation(s) OP-01
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs

BR-06 PingServiceException is not wrapped twice

Field Value
Statement 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.
Trigger PingServiceException reaching Get's catch (PingServiceException p) { throw; } block
Enforcement PingableController.Get
Outcome 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.
Enforcing operation(s) OP-01
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs

BR-07 Ping exceptions are failures unless vetoed

Field Value
Statement 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.
Trigger Exception in Ping
Enforcement IPingableService.PingAsync; IPingableService.IsThrowablePingError (default true)
Outcome The task faults, which leads to BR-04.
Enforcing operation(s) OP-02
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/IPingableService.cs

BR-08 Non-throwable ping errors count as success

Field Value
Statement 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.
Trigger IsThrowablePingError(ex) returns false
Enforcement IPingableService.PingAsync (fall-through)
Outcome OnPingSuccess runs even though Ping threw.
Enforcing operation(s) OP-02
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/IPingableService.cs

BR-09 Successful run returns 200 with run id

Field Value
Statement A successful run returns HTTP 200 with the run id as the body. Each target's OnPingSuccess runs after its Ping.
Trigger All pings complete without a throwable error
Enforcement PingableController.Get; IPingableService.PingAsync
Outcome Ok(guid)
Enforcing operation(s) OP-01, OP-02
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs, src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/IPingableService.cs

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.ServiceContracts HttpStatusCodeException).
Enforcing operation(s) OP-01, OP-05
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Exceptions/PingServiceException.cs

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.
Enforcing operation(s) OP-03
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/IServiceCollectionExtensions.cs

BR-12 Logging is optional

Field Value
Statement Logging is optional. If no ISaifLogService is registered, hooks receive null.
Trigger Controller construction
Enforcement PingableController constructor (GetService<ISaifLogService>())
Outcome No failure when logging is absent.
Enforcing operation(s) OP-01
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs

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.*
Enforcing operation(s) OP-04
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/MvcBuilderExtensions.cs

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
Enforcement ControllerRetriever.GetControllers (provider.GetService(type))
Outcome 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).
Enforcing operation(s) OP-01, OP-04
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Services/ControllerRetriever.cs

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.*
Enforcing operation(s) OP-03, OP-01
Source src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/IServiceCollectionExtensions.cs

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.cs
  • src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/IPingableService.cs
  • src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Exceptions/PingServiceException.cs
  • src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/IServiceCollectionExtensions.cs
  • src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/MvcBuilderExtensions.cs
  • src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Services/ControllerRetriever.cs