Skip to content

SAIF.NetCore.WindowsService architecture

SAIF.NetCore.WindowsService is an implementation-layer library (per Architecture.md) that provides a small hosting skeleton — a program runner and a timed background-worker base class — for building SAIF Windows Services on the .NET Generic Host, and it depends only on the foundation layer (SAIF.NetCore.ServiceContracts), leaving logging, error handling, and DI registration to the consumer's composition root.

Responsibilities

  • Build and run the .NET Generic Host for a Windows Service, including configuration sources, UseWindowsService(), an optional startup delay, and error-handler/logger configuration hooks (ServiceProgramRunnerBase).
  • Provide a fixed-delay background-task loop base class that runs a consumer-supplied IServiceTaskRunner repeatedly until cancellation (ServiceWorker<T>).
  • Apply a fail-stop error policy: catch every exception from the task runner, report it through IWindowsServiceErrorHandler, and stop the host unless the exception type is explicitly shutdown-exempt.
  • Track process-global host state (ServiceProgramRunnerBase.ServiceHost) so the worker's error-handling path can trigger a graceful host shutdown.

Component boundaries

  • ServiceProgramRunnerBase: owns the host lifecycle — building the IHostBuilder, applying the optional DelayStart startup delay, and invoking the ConfigureErrorHandler/ConfigureLogger hooks before the host runs.
  • ServiceWorker<T> (where T : IServiceTaskRunner): owns the recurring task loop, per-run error handling, and the shutdown-exemption check; it is a Microsoft.Extensions.Hosting.BackgroundService subclass.
  • Neither type has any abstract members; both are "abstract" only to prevent direct instantiation, so a consumer can derive with an empty override body*.
  • All service interfaces these base classes depend on (IServiceTaskRunner, IWindowsServiceErrorHandler, ISaifWindowsServiceLogService, ShutdownExemptExceptionType) are defined in SAIF.NetCore.ServiceContracts, not in this library.
  • This library ships no DI registration extension method. The companion registration, RegisterForWindowsService/RegisterForWindowsService<T>, is owned entirely by SAIF.NetCore.ServiceExtensions, and this library does not reference that package.

Interaction model

  • A consumer writes class Program : ServiceProgramRunnerBase, overrides CreateHostBuilder to call base.CreateHostBuilder(args).ConfigureServices with a lambda that registers its ServiceWorker<T> subclass with AddHostedService<> and calls SAIF.NetCore.ServiceExtensions.RegisterForWindowsService, then calls new Program().Run(args) from Main*.
  • Run builds the host once (Create), applies the optional DelayStart delay, invokes Configure (which calls the consumer's ConfigureErrorHandler hook and then ConfigureLogger), and finally blocks on ServiceHost.Run().
  • ConfigureLogger resolves ISaifWindowsServiceLogService, IEnvironmentClient, and IHttpJsonClient from the container with GetRequiredService and calls logger.Configure(envClient, httpClient); this library does not implement any of those three services itself.
  • ServiceWorker<T>.ExecuteAsync runs T.Run(stoppingToken), then waits TaskDelayMilliseconds, repeating until cancellation; any exception from the runner is caught, reported via IWindowsServiceErrorHandler.HandleError, and — unless shutdown-exempt — triggers ServiceHost.StopAsync(ShutdownTimeoutSeconds) through the static ServiceProgramRunnerBase.ServiceHost reference.
  • IWindowsServiceErrorHandler, ISaifWindowsServiceLogService, IEnvironmentClient, and IHttpJsonClient are all resolved from the DI container; none are referenced as compile-time package dependencies of this library — in practice a consumer satisfies them by referencing SAIF.NetCore.ServiceExtensions and calling RegisterForWindowsService, though nothing in this library enforces that specific call, and an equivalent manual registration of the same four contracts would satisfy the runtime requirement just as well*.

Architecture diagram

The following diagram shows this library's single NuGet dependency on SAIF.NetCore.ServiceContracts, the runtime services the consumer's composition root must supply through SAIF.NetCore.ServiceExtensions, and the resulting hosted-service process.

flowchart TD
    SC["SAIF.NetCore.ServiceContracts<br>(IServiceTaskRunner, IWindowsServiceErrorHandler,<br>ISaifWindowsServiceLogService)"]
    WS["SAIF.NetCore.WindowsService<br>ServiceProgramRunnerBase / ServiceWorker&lt;T&gt;"]
    SE["SAIF.NetCore.ServiceExtensions<br>RegisterForWindowsService"]
    EH["SAIF.NetCore.ErrorHandler<br>(IWindowsServiceErrorHandler impl)"]
    LOG["SAIF.NetCore.Logging<br>(ISaifWindowsServiceLogService impl)"]
    ENV["SAIF.NetCore.EnvironmentClient<br>(IEnvironmentClient impl)"]
    HTTP["SAIF.NetCore.HttpClients<br>(IHttpJsonClient impl)"]
    APP["Consuming Windows Service app<br>(Program : ServiceProgramRunnerBase)"]

    WS -.->|NuGet ref| SC
    APP -.->|NuGet ref| WS
    APP -.->|NuGet ref| SE
    SE -.->|NuGet ref| EH
    SE -.->|NuGet ref| LOG
    SE -.->|NuGet ref| ENV
    SE -.->|NuGet ref| HTTP
    APP -->|RegisterForWindowsService| SE
    WS -->|DI-resolved at host composition root| EH
    WS -->|DI-resolved at host composition root| LOG
    WS -->|DI-resolved at host composition root| ENV
    WS -->|DI-resolved at host composition root| HTTP

Sequence diagram

The following diagram shows the primary startup and task-loop sequence, including that every task error is reported through the error handler and that only a non-exempt error triggers the host-wide stop.

sequenceDiagram
    participant Program as "Program (consumer)"
    participant Runner as ServiceProgramRunnerBase
    participant Worker as "ServiceWorker&lt;T&gt;"
    participant TaskRunner as "IServiceTaskRunner"
    participant ErrorHandler as "IWindowsServiceErrorHandler"
    participant Host as "IHost (ServiceHost)"

    Program->>Runner: Run(args)
    Runner->>Runner: Create(args) -> CreateHostBuilder, DelayStartup
    Runner->>Runner: Configure(host) -> ConfigureErrorHandler, ConfigureLogger
    Runner->>Host: ServiceHost.Run()
    Host->>Worker: ExecuteAsync(stoppingToken)
    loop until cancellation
        Worker->>TaskRunner: Run(stoppingToken)
        alt task throws
            TaskRunner-->>Worker: Exception
            Worker->>ErrorHandler: HandleError(message, exception)
            alt exception is non-exempt
                Worker->>Host: ServiceHost.StopAsync(ShutdownTimeoutSeconds)
            else exception is shutdown-exempt
                Worker->>Worker: Task.Delay(TaskDelayMilliseconds)
            end
        else success
            TaskRunner-->>Worker: Completed
            Worker->>Worker: Task.Delay(TaskDelayMilliseconds)
        end
    end

Design constraints

  • The library takes no NuGet dependency on any other SAIF implementation library — only on SAIF.NetCore.ServiceContracts — consistent with the repo-wide rule that implementation libraries depend solely on the foundation layer.
  • ServiceProgramRunnerBase.ServiceHost is a static, process-global reference; only one host per process is supported, and a ServiceWorker<T> hosted by anything other than ServiceProgramRunnerBase will throw a NullReferenceException on its first non-exempt error*.
  • CreateHostBuilder appends AddJsonFile("appsettings.json", optional: true) and AddEnvironmentVariables() after the sources Host.CreateDefaultBuilder already registers, which reverses the expected configuration-source precedence (later sources win), so appsettings.json can override environment-specific settings and command-line arguments*.
  • DelayStartup blocks the main thread synchronously (Thread.Sleep) before the host starts; a DelayStart value longer than the Windows SCM start timeout would likely make the service fail to start*.
  • ServiceWorker<T>.ExecuteAsync is a fixed-delay loop (delay after each run, including the first), not a fixed-rate loop, despite the TaskDelayMilliseconds XML doc describing it as an initial-run delay.
  • This library has no in-repo consumer; it is referenced only by its own pipeline files and by the repo's architecture/legacy-repository documentation, so its composition pattern is otherwise unverified against a real consumer in this repository.
  • There is no test project for this library.

Note: Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts. Statements marked with ** could not be confirmed; treat as unknown until verified.

Sources

  • src/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService.csproj
  • src/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/ServiceProgramRunnerBase.cs
  • src/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/ServiceWorker.cs
  • Architecture.md