Davasorus.Utility.DotNet.Services 2026.2.2.4

There is a newer version of this package available.
See the version list below for details.
dotnet add package Davasorus.Utility.DotNet.Services --version 2026.2.2.4
                    
NuGet\Install-Package Davasorus.Utility.DotNet.Services -Version 2026.2.2.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Davasorus.Utility.DotNet.Services" Version="2026.2.2.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Davasorus.Utility.DotNet.Services" Version="2026.2.2.4" />
                    
Directory.Packages.props
<PackageReference Include="Davasorus.Utility.DotNet.Services" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Davasorus.Utility.DotNet.Services --version 2026.2.2.4
                    
#r "nuget: Davasorus.Utility.DotNet.Services, 2026.2.2.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Davasorus.Utility.DotNet.Services@2026.2.2.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Davasorus.Utility.DotNet.Services&version=2026.2.2.4
                    
Install as a Cake Addin
#tool nuget:?package=Davasorus.Utility.DotNet.Services&version=2026.2.2.4
                    
Install as a Cake Tool

Davasorus.Utility.DotNet.Services

Davasorus.Utility.DotNet.Services provides a set of service/client abstractions for managing Windows services (start, stop, load, manipulate) in .NET 8+ applications. It is designed for use with dependency injection (DI). End users should only interact with the service interfaces; the services handle all client interactions, error handling, and logging.

Note: Only interact with the service interfaces (e.g., IStartServiceService, IStopServicesService, ILoadServicesService, IManipulateServiceService) in your application code. The service classes manage all communication with their respective client classes internally.

Breaking Changes in This Release

The root namespace has been renamed from Tyler.Utility.DotNet.Services.* to Davasorus.Utility.DotNet.Services.* to align with the published NuGet package identity. Consumers upgrading from any prior 2026.2.1.x version must update their using directives:

// Before
using Tyler.Utility.DotNet.Services.Configuration;
using Tyler.Utility.DotNet.Services.Windows_Services.Start_Services.Service;

// After
using Davasorus.Utility.DotNet.Services.Configuration;
using Davasorus.Utility.DotNet.Services.Windows_Services.Start_Services.Service;

No public API signatures changed. This is purely a namespace rename.

B1 — API contract modernization (this version)

DI lifetimes changed to Singleton

All registrations in AddWindowsServices now use TryAddSingleton. Consumers depending on Scoped or Transient resolution semantics will see one instance for the lifetime of the host. Consumers wanting to override the default implementations (e.g., for testing) can pre-register their implementation before calling AddWindowsServicesTryAddSingleton honors consumer pre-registrations.

IDisposable removed from all 8 interfaces

ILoadServicesClient, ILoadServicesService, IManipulateServiceClient, IManipulateServiceService, IStartServiceClient, IStartServiceService, IStopServicesClient, IStopServicesService no longer declare void Dispose();. Consumers using using var x = service; patterns must drop the using keyword:

// Before
using var loadService = scope.ServiceProvider.GetRequiredService<ILoadServicesService>();
var list = await loadService.GetServiceList("HOST");

// After
var loadService = scope.ServiceProvider.GetRequiredService<ILoadServicesService>();
var list = await loadService.GetServiceList("HOST");
Method return types changed to ServiceResult<T> / ServiceResult

Every public method now returns a ServiceResult (non-generic) or ServiceResult<T> (generic) wrapping the operation's outcome. The streaming method GetServiceListStreaming is the one exception — it retains IAsyncEnumerable<ServiceObj> and exceptions propagate to the consumer's await foreach.

// Before
var list = await loadService.GetServiceList("HOST");
if (list != null) { ... }

// After
var result = await loadService.GetServiceList("HOST");
if (result.IsSuccess && result.Value != null)
{
    var list = result.Value;
    // ...
}
// or, to handle failure:
if (!result.IsSuccess)
{
    logger.LogError(result.Error, "List failed; reported to SQS: {Reported}", result.WasReportedToSqs);
}

ServiceResult carries IsSuccess, Error, and WasReportedToSqs. The WasReportedToSqs flag tells callers whether the package's catch-template successfully sent the error to SQS — useful for chain composition (don't double-report).

Manipulate methods now throw on null input (previously silent return false)

SetSpecifiedServiceOnMachineToEnabled, SetSpecifiedServiceOnMachineToDisabled, SetSpecifiedServiceOnMachineToManual and their Client-tier counterparts previously returned false silently when called with null or whitespace input. They now throw ArgumentException consistently with the other tiers. Inputs are validated via ArgumentException.ThrowIfNullOrWhiteSpace(...).

ServiceOptions changes
  • ServiceOperationTimeoutSeconds has been split into two fields:
    • GeneralOperationTimeoutSeconds (default 60s) for general service operations
    • WebOperationTimeoutSeconds (default 30s) for IIS / w3svc operations
  • EnableErrorLogging now actually does work — setting it to false skips SQS reporting (previously the field was read by nothing)
  • MaxRetryAttempts now actually drives retry behavior via Polly (previously the field was read by nothing)
ServiceOptionsBuilder method renames
// Before
services.AddWindowsServices(svc => svc.WithOperationTimeout(TimeSpan.FromSeconds(45)));

// After
services.AddWindowsServices(svc => svc.WithGeneralOperationTimeout(TimeSpan.FromSeconds(45)));
// or
services.AddWindowsServices(svc => svc.WithWebOperationTimeout(TimeSpan.FromSeconds(20)));
Polly retry on transient exceptions

Transient WMI / SCM / Registry exceptions (IOException, TimeoutException, SocketException, ServiceProcess.TimeoutException) now retry automatically per MaxRetryAttempts (default 3 retries). Non-transient exceptions (ArgumentException, UnauthorizedAccessException, etc.) do NOT retry. Worst-case wall-clock for a fully failing operation: (MaxRetryAttempts + 1) × timeout ≈ 4 × 60s = 240s for general operations under default settings.

If retry behavior is unwanted, set MaxRetryAttempts = 0 in options.

WaitForStatus is now an async polling loop

The blocking ServiceController.WaitForStatus(...) call inside Start/Stop has been replaced with an async polling loop (Internal/WaitForStatusAsyncHelper). The new loop polls every 500ms instead of blocking a thread-pool thread for the full timeout duration. Consumers observing thread-pool exhaustion under heavy Start/Stop load will see relief.

C2a — Telemetry semantic conventions migration

Two consumer-visible wire-format changes in this release:

Cache result tag changed from cache.hit (bool) to cache.result (enum)

Activity tags emitted by GetServiceList and GetServiceDescription on cache hit/miss now use the canonical OpenTelemetry-aligned enum-valued cache.result key instead of the bool cache.hit key. Dashboards observing the old key will return zero results.

# Before (dashboard query)
{cache.hit="true"}

# After
{cache.result="hit"}

# Before
{cache.hit="false"}

# After
{cache.result="miss"}

The cache.key tag is unchanged — only the bool→enum change applies.

Tag-key prefix normalization under windows.service.*

All Windows-service-related tag keys and event names now use the windows.service.* prefix per OpenTelemetry domain-prefix conventions. Dashboards observing the old (non-prefixed) keys will return zero results.

# Before                          → After
registry.key.path                  → windows.service.registry.key_path
service.timeout_seconds            → windows.service.timeout_seconds

# Event names
registry.operation.start           → windows.service.registry.operation_start
registry.set_value                 → windows.service.registry.set_value
registry.verified                  → windows.service.registry.verified
service.status.change              → windows.service.status_change

The existing windows.service.* keys (host, name, operation, type, success, count) are unchanged — they already used the prefix.

Migration: see the new "Observability" section below for the canonical constant locations.

C2b — Telemetry deepening

Five consumer-visible changes in this release. Most affect observability dashboards and APM topology; none affect functional behavior.

ActivityKind changed from Internal to Client on outbound operations

GetServiceList, GetServiceDescription, GetServiceListStreaming (WMI), EnableService/DisableService/SetServiceToManual (Registry), Start (SCM), Stop (SCM) now create activities with ActivityKind.Client instead of ActivityKind.Internal. Service-layer wrappers stay Internal. APM topology diagrams will start rendering Services as a "client" calling out to "Windows host" instead of as a leaf in the trace tree.

windows.service.success tag now emitted on all operations

Previously only Manipulate* operations emitted this tag. After C2b, Start, Stop, Load operations also emit it (true on happy path, false on caught exception). Dashboards can now filter on {windows.service.success="false"} across all operations uniformly.

Cache key suffix :v2 and envelope wire-format change

The Services pkg's internal cache entries now use the :v2 key suffix and a new CachedWithContext<T> envelope shape carrying both the cached value and a W3C traceparent string. Pre-C2b cache entries (key without :v2) are NOT read; this forces a one-time cache miss + WMI re-population on first read per host/operation after deploy. Operational impact: one burst of WMI queries during the deploy window. Once re-populated, the v2 entry is cached for the same 30-day-sliding/60-day-absolute TTL.

If you read this package's cache directly (you shouldn't), the stored value shape is now CachedWithContext<List<ServiceObj>> (or <string> for Description) instead of the bare collection/string.

When a GetServiceList or GetServiceDescription returns a cached value, the resulting activity carries one ActivityLink back to the trace context of the call that originally populated the cache. Dashboards visualizing trace relationships can now show "cache hit → original populate" connections.

If the stored traceparent fails to parse (e.g., corrupted cache value), the cache-hit emits a cache.activitylink.malformed event and continues normally — the cache-hit semantics never become a cache-miss due to link parsing failure.

Five new OpenTelemetry metrics under windows_service.*
windows_service.operations              Counter<long>     tags: operation, host, success
windows_service.operation.duration      Histogram<double> unit: ms; tags: operation, host
windows_service.cache.hits              Counter<long>     tags: operation, host
windows_service.cache.misses            Counter<long>     tags: operation, host
windows_service.errors                  Counter<long>     tags: operation, host, exception.type

All metrics live on a single Meter named Davasorus.Utility.Services.WindowsServices. They're auto-wired by AddDavasorusTelemetry's wildcard meter subscription (Davasorus.Utility.*). See the new "Observability" section below.

Features

  • Start, stop, and manipulate Windows services on remote or local hosts
  • Retrieve service lists and descriptions, with streaming support
  • Strongly-typed async APIs
  • OpenTelemetry distributed tracing via ActivitySource on all operations
  • Robust error handling and logging (ILogger + SQS integration)
  • Designed for DI: all services registered as Singleton via TryAddSingleton
  • Returns ServiceResult<T> for every public method, with explicit success/failure outcomes and SQS-report tracking
  • Automatic retry on transient WMI/SCM/Registry exceptions via Polly resilience pipeline
  • Async-polling wait loop replaces blocking WaitForStatus, avoiding thread-pool exhaustion under heavy load
  • Hybrid validation: programmer errors throw, operational outcomes return

Dependency Injection Setup

Register all services using the AddWindowsServices() extension method:

using Davasorus.Utility.DotNet.Services.Configuration;

services.AddWindowsServices();

Or with fluent configuration:

services.AddWindowsServices(svc => svc
    .WithGeneralOperationTimeout(TimeSpan.FromSeconds(60))
    .WithWebOperationTimeout(TimeSpan.FromSeconds(30))
    .WithRetryPolicy(3)
    .WithErrorLogging(true));

This registers the following services automatically:

Interface Implementation Lifetime
IStartServiceService StartServiceService Singleton
IStartServiceClient StartServiceClient Singleton
IStopServicesService StopServicesService Singleton
IStopServicesClient StopServicesClient Singleton
ILoadServicesService LoadServicesService Singleton
ILoadServicesClient LoadServicesClient Singleton
IManipulateServiceService ManipulateServiceService Singleton
IManipulateServiceClient ManipulateServiceClient Singleton
ResiliencePipeline (shared) (built from ServiceOptions) Singleton
ServiceOptions (via IOptions<> pipeline) Singleton

Example Usage

Start a Service

public class MyServiceStarter
{
    private readonly IStartServiceService _startService;
    public MyServiceStarter(IStartServiceService startService)
    {
        _startService = startService;
    }

    public async Task StartAsync(string host, string serviceName)
    {
        await _startService.StartService(host, serviceName);
    }
}

Stop a Service

public class MyServiceStopper
{
    private readonly IStopServicesService _stopService;
    public MyServiceStopper(IStopServicesService stopService)
    {
        _stopService = stopService;
    }

    public async Task StopAsync(string host, string serviceName)
    {
        await _stopService.StopService(host, serviceName);
    }
}

Load Services List

public class MyServiceLoader
{
    private readonly ILoadServicesService _loadService;
    public MyServiceLoader(ILoadServicesService loadService)
    {
        _loadService = loadService;
    }

    public async Task<List<ServiceObj>?> GetServicesAsync(string host)
    {
        return await _loadService.GetServiceList(host);
    }
}

Manipulate Service State

public class MyServiceManipulator
{
    private readonly IManipulateServiceService _manipulateService;
    public MyServiceManipulator(IManipulateServiceService manipulateService)
    {
        _manipulateService = manipulateService;
    }

    public async Task<bool> EnableServiceAsync(string host, string serviceName)
    {
        return await _manipulateService.SetSpecifiedServiceOnMachineToEnabled(host, serviceName);
    }
}

API Overview

IStartServiceService

  • Task StartService(string hostName, string serviceName, TimeSpan? timeout = null) Starts the specified Windows service on the given host. Defaults to 60 seconds if no timeout is specified.

  • Task StartWebServices(string hostName, TimeSpan? timeout = null) Starts all web-related Windows services on the specified host. Defaults to 30 seconds if no timeout is specified.

IStopServicesService

  • Task StopService(string hostName, string serviceName, TimeSpan? timeout = null) Stops the specified Windows service on the given host. Defaults to 60 seconds if no timeout is specified.

  • Task StopWebServices(string hostName, TimeSpan? timeout = null) Stops all web-related Windows services on the specified host. Defaults to 30 seconds if no timeout is specified.

ILoadServicesService

  • Task<List<ServiceObj>?> GetServiceList(string hostName) Retrieves a list of Windows services from the specified host. Returns a list of ServiceObj instances representing each service, or null if retrieval fails.

  • Task<string> GetServiceDescription(string hostName, string serviceName) Gets the description of a specific Windows service on the given host. Returns the service description as a string.

  • IAsyncEnumerable<ServiceObj> GetServiceListStreaming(string hostName) Streams Windows services from the specified host as they are discovered. Returns an async stream of ServiceObj instances.

IManipulateServiceService

  • Task<bool> SetSpecifiedServiceOnMachineToEnabled(string hostName, string serviceName) Enables the specified Windows service on the given host, setting its startup type to "Automatic". Returns true if successful.

  • Task<bool> SetSpecifiedServiceOnMachineToDisabled(string hostName, string serviceName) Disables the specified Windows service on the given host, setting its startup type to "Disabled". Returns true if successful.

  • Task<bool> SetSpecifiedServiceOnMachineToManual(string hostName, string serviceName) Sets the specified Windows service on the given host to "Manual" startup type. Returns true if successful.

Observability

This package emits OpenTelemetry activities and metrics. Both are auto-wired into the OTel pipeline by AddDavasorusTelemetry's default wildcard subscription (Davasorus.Utility.*). No additional registration extension is required.

Activities

Operation Activity name Kind
LoadServicesClient.GetServiceList Services.Client.Load.List Client
LoadServicesClient.GetServiceDescription Services.Client.Load.Describe Client
LoadServicesClient.GetServiceListStreaming Services.Client.Load.Stream Client
LoadServicesService.GetServiceList Services.Service.Load.List Internal
LoadServicesService.GetServiceDescription Services.Service.Load.Describe Internal
ManipulateServiceClient.EnableService Services.Client.Manipulate.Enable Client
ManipulateServiceClient.DisableService Services.Client.Manipulate.Disable Client
ManipulateServiceClient.SetServiceToManual Services.Client.Manipulate.Manual Client
(Service wrappers) Services.Service.Manipulate.* Internal
StartServiceClient.Start Services.Client.Start Client
StopServicesClient.Stop Services.Client.Stop Client

Tag keys come from Davasorus.Utility.DotNet.Telemetry's SemanticConventions.WindowsService group (and SemanticConventions.Cache for cache tags). Reference them in dashboard query / processing code:

using Davasorus.Utility.DotNet.Telemetry;

// Tag keys
var hostKey       = SemanticConventions.WindowsService.Host;       // "windows.service.host"
var operationKey  = SemanticConventions.WindowsService.Operation;  // "windows.service.operation"
var successKey    = SemanticConventions.WindowsService.Success;    // "windows.service.success"
var cacheKey      = SemanticConventions.Cache.Key;                 // "cache.key"
var cacheResult   = SemanticConventions.Cache.Result;              // "cache.result" — values: "hit" | "miss"

Metrics

Single Meter: Davasorus.Utility.Services.WindowsServices.

Metric Type Unit Tags
windows_service.operations Counter<long> (count) windows.service.operation, .host, .success
windows_service.operation.duration Histogram<double> ms windows.service.operation, .host
windows_service.cache.hits Counter<long> (count) windows.service.operation, .host
windows_service.cache.misses Counter<long> (count) windows.service.operation, .host
windows_service.errors Counter<long> (count) windows.service.operation, .host, exception.type

GetServiceList and GetServiceDescription cache-hit activities carry one ActivityLink back to the trace context of the call that originally populated the cache value (recorded in the cached envelope as a W3C traceparent). Visualize cache-hit relationships in your tracing UI.

If the stored traceparent is malformed (corrupted serialization, unexpected wire format), the cache-hit activity emits a cache.activitylink.malformed event and continues with the cached value. Malformed traceparents never downgrade a cache-hit to a cache-miss.

Example dashboard queries (PromQL-style)

# Per-host operation rate
rate(windows_service_operations_total{}[5m])

# Per-operation p99 latency (ms)
histogram_quantile(0.99, rate(windows_service_operation_duration_bucket{}[5m]))

# Cache hit ratio
rate(windows_service_cache_hits_total{}[5m]) / (rate(windows_service_cache_hits_total{}[5m]) + rate(windows_service_cache_misses_total{}[5m]))

# Error rate by exception type
rate(windows_service_errors_total{}[5m])

# All failed operations
rate(windows_service_operations_total{windows_service_success="false"}[5m])

See the Davasorus.Utility.DotNet.Telemetry documentation for the full semantic conventions catalog.

Error Handling

All errors are logged via ILogger and reported to SQS. Service methods return null, false, or the exception message as appropriate, and log details for diagnostics. All operations are instrumented with OpenTelemetry ActivitySource spans for distributed tracing.

Requirements

  • .NET 8.0 or .NET 10.0

License

MIT License

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2026.2.3.1 0 6/1/2026
2026.2.2.14 36 5/31/2026
2026.2.2.13 91 5/23/2026
2026.2.2.12 96 5/19/2026
2026.2.2.11 93 5/18/2026
2026.2.2.10 97 5/18/2026
2026.2.2.9 87 5/18/2026
2026.2.2.8 90 5/17/2026
2026.2.2.7 94 5/17/2026
2026.2.2.6 99 5/17/2026
2026.2.2.5 88 5/17/2026
2026.2.2.4 84 5/16/2026
2026.2.2.3 88 5/16/2026
2026.2.2.2 95 5/16/2026
2026.2.2.1 94 5/15/2026
2026.2.1.3 149 4/16/2026
2026.2.1.2 2,180 4/9/2026
2026.2.1.1 344 4/1/2026
2026.1.3.5 153 3/29/2026
2026.1.3.4 186 3/29/2026
Loading failed