How to expose an HTTP status endpoint from a .NET 8 Windows Service?
I’m developing a Windows Service in .NET 8 that periodically spawns long-running worker threads. I’ve successfully captured the status of these threads and pending jobs, but I need to expose this information through an HTTP endpoint for end users to view.
Here’s my current implementation:
Program.cs
public static void Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
// configuration setup
services.AddLogging();
services.AddHttpClient().RemoveAll<IHttpMessageHandlerBuilderFilter>();
// service registration
builder.Services.AddHostedService<Worker>();
// Build and configure the app
var app = builder.Build();
app.Run();
}
Worker.cs ExecuteAsync method
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Do stuff...
var delayMs = _options.PollingIntervalSeconds * 1000;
try
{
await Task.Delay(delayMs, stoppingToken);
}
catch (OperationCanceledException)
{
// Expected during shutdown
break;
}
}
}
What’s the best approach to add an HTTP endpoint to this Windows Service that can display the current status of worker threads and pending jobs?
Yes, you can expose an HTTP status endpoint from your .NET 8 Windows Service by embedding the Kestrel web server and adding a minimal API endpoint to display your worker status. This approach allows you to maintain your Windows Service functionality while providing HTTP access to monitoring data.
Contents
- Adding Kestrel to Your Windows Service
- Configuring HTTP Endpoints
- Creating a Status Monitoring Endpoint
- Complete Implementation Example
- Alternative Approaches
- Security Considerations
- Testing and Deployment
Adding Kestrel to Your Windows Service
To expose an HTTP endpoint from your Windows Service, you need to add the ASP.NET Core web server infrastructure to your existing service. The most straightforward approach is to use the built-in Kestrel web server that comes with ASP.NET Core.
Start by adding the necessary packages to your project:
dotnet add package Microsoft.AspNetCore.App dotnet add package Microsoft.Extensions.Hosting
Then modify your Program.cs to include web server functionality:
public static void Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
// Configure web server
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5000); // Listen on all interfaces, port 5000
});
// Rest of your existing configuration...
var services = builder.Services;
var configuration = builder.Configuration;
services.AddLogging();
services.AddHttpClient().RemoveAll<IHttpMessageHandlerBuilderFilter>();
// service registration
builder.Services.AddHostedService<Worker>();
// Build and configure the app
var app = builder.Build();
// Add your status endpoint
app.MapGet("/status", (Worker worker) => new
{
Status = "Running",
WorkerThreads = worker.GetWorkerThreadCount(),
PendingJobs = worker.GetPendingJobCount(),
LastUpdate = DateTime.UtcNow
});
app.Run();
}
Configuring HTTP Endpoints
You can configure Kestrel endpoints in several ways. The most common approaches are:
1. Code Configuration (Recommended for Windows Services)
builder.WebHost.ConfigureKestrel(options =>
{
// Listen on all interfaces for remote access
options.ListenAnyIP(5000);
// Or configure both HTTP and HTTPS
options.ListenAnyIP(5000, configure => configure.UseHttp());
options.ListenAnyIP(5001, configure => configure.UseHttps());
});
2. Configuration File (appsettings.json)
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://*:5000"
},
"Https": {
"Url": "https://*:5001",
"Certificate": {
"AllowInvalid": true
}
}
}
}
}
3. Environment Variables
Set these environment variables before starting the service:
set ASPNETCORE_URLS=http://*:5000
set ASPNETCORE_HTTP_PORTS=5000
set ASPNETCORE_HTTPS_PORTS=5001
According to the Microsoft documentation, environment variables with DOTNET_ or ASPNETCORE_ prefixes are supported.
Creating a Status Monitoring Endpoint
To expose your worker status, add methods to your Worker class to collect the status information and create an HTTP endpoint:
Modified Worker.cs
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly MyWorkerOptions _options;
private int _workerThreadCount = 0;
private int _pendingJobCount = 0;
private readonly object _statusLock = new object();
public Worker(ILogger<Worker> logger, IOptions<MyWorkerOptions> options)
{
_logger = logger;
_options = options.Value;
}
// Add these public methods for status reporting
public int GetWorkerThreadCount()
{
lock (_statusLock)
{
return _workerThreadCount;
}
}
public int GetPendingJobCount()
{
lock (_statusLock)
{
return _pendingJobCount;
}
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Simulate work and update status
await DoWorkAsync(stoppingToken);
var delayMs = _options.PollingIntervalSeconds * 1000;
try
{
await Task.Delay(delayMs, stoppingToken);
}
catch (OperationCanceledException)
{
// Expected during shutdown
break;
}
}
}
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
// Your existing work logic here
// Update thread and job counts as appropriate
lock (_statusLock)
{
// Example: update your status variables
_workerThreadCount = CalculateActiveThreads();
_pendingJobCount = CountPendingJobs();
}
// Rest of your work implementation...
}
}
Enhanced Status Endpoint
Add more detailed status information to your endpoint:
// In Program.cs, replace the simple status endpoint with this:
app.MapGet("/status", (Worker worker) =>
{
var status = new
{
ServiceStatus = "Running",
Timestamp = DateTime.UtcNow,
WorkerThreads = worker.GetWorkerThreadCount(),
PendingJobs = worker.GetPendingJobCount(),
Uptime = DateTime.UtcNow - worker.StartTime, // Add a StartTime property to Worker
Configuration = new
{
PollingIntervalSeconds = _options.PollingIntervalSeconds
}
};
return Results.Json(status);
});
app.MapGet("/health", () =>
{
return Results.Ok(new { Status = "Healthy", Timestamp = DateTime.UtcNow });
});
Complete Implementation Example
Here’s a complete, working example you can adapt:
// Program.cs
public static void Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
// Configure Kestrel
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5000);
});
// Add services
builder.Services.AddLogging();
builder.Services.Configure<MyWorkerOptions>(
builder.Configuration.GetSection("MyWorker"));
builder.Services.AddHostedService<Worker>();
var app = builder.Build();
// Status endpoint
app.MapGet("/status", (Worker worker, ILogger<Program> logger) =>
{
logger.LogInformation("Status endpoint called");
var status = worker.GetDetailedStatus();
return Results.Ok(status);
});
// Simple health check
app.MapGet("/health", () =>
Results.Ok(new { Status = "Healthy", Timestamp = DateTime.UtcNow }));
app.Run();
}
// Worker.cs
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly MyWorkerOptions _options;
private DateTime _startTime = DateTime.UtcNow;
private int _activeThreads = 0;
private int _pendingJobs = 0;
private readonly object _statusLock = new object();
public Worker(ILogger<Worker> logger, IOptions<MyWorkerOptions> options)
{
_logger = logger;
_options = options.Value;
}
public WorkerStatus GetDetailedStatus()
{
lock (_statusLock)
{
return new WorkerStatus
{
ServiceStatus = "Running",
StartTime = _startTime,
Uptime = DateTime.UtcNow - _startTime,
ActiveThreads = _activeThreads,
PendingJobs = _pendingJobs,
LastActivity = DateTime.UtcNow,
Configuration = _options
};
}
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker started at: {time}", DateTimeOffset.Now);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ExecuteWorkCycle(stoppingToken);
var delayMs = _options.PollingIntervalSeconds * 1000;
await Task.Delay(delayMs, stoppingToken);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Worker shutting down");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in worker execution");
}
}
}
private async Task ExecuteWorkCycle(CancellationToken stoppingToken)
{
lock (_statusLock)
{
_activeThreads = GetActualThreadCount();
_pendingJobs = GetActualJobCount();
}
// Your existing work logic here
await ProcessJobsAsync(stoppingToken);
}
}
// Supporting classes
public class MyWorkerOptions
{
public int PollingIntervalSeconds { get; set; } = 30;
public int MaxConcurrentThreads { get; set; } = 5;
public string JobQueueConnectionString { get; set; } = string.Empty;
}
public class WorkerStatus
{
public string ServiceStatus { get; set; } = string.Empty;
public DateTime StartTime { get; set; }
public TimeSpan Uptime { get; set; }
public int ActiveThreads { get; set; }
public int PendingJobs { get; set; }
public DateTime LastActivity { get; set; }
public MyWorkerOptions Configuration { get; set; } = new();
}
Alternative Approaches
1. Using Worker Service Template
Create a new project using the Worker Service template and add web functionality:
dotnet new worker -n StatusWebService dotnet add package Microsoft.AspNetCore.App
2. Reverse Proxy with IIS
For production environments, consider running Kestrel behind IIS:
builder.WebHost.UseIIS();
3. Separate Web API Project
For larger applications, create a separate Web API project that communicates with your Windows Service through:
- gRPC
- REST API calls
- Message queues (RabbitMQ, Azure Service Bus)
Security Considerations
When exposing HTTP endpoints from a Windows Service, consider these security measures:
// Configure HTTPS for production
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5000, configure => configure.UseHttp());
options.ListenAnyIP(5001, configure =>
{
configure.UseHttps(new HttpsConnectionAdapterOptions
{
ServerCertificate = new X509Certificate2("certificate.pfx", "password"),
ClientCertificateMode = ClientCertificateMode.AllowCertificate
});
});
});
// Add authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "your-issuer",
ValidAudience = "your-audience",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secret-key"))
};
});
// Restrict access to status endpoint
app.MapGet("/status", (Worker worker) =>
{
// Add authentication/authorization logic here
return Results.Ok(worker.GetDetailedStatus());
}).RequireAuthorization();
Testing and Deployment
Testing Your Service
- Run locally:
dotnet run
- Test endpoints:
curl http://localhost:5000/status curl http://localhost:5000/health
- Install as Windows Service:
sc.exe create "MyStatusWebService" binpath="C:\path\to\your\service.exe"
sc.exe start "MyStatusWebService"
Production Deployment
For production deployment, consider these best practices:
- Use HTTPS - Configure SSL certificates for secure communication
- Set proper permissions - Ensure the service account has necessary network access
- Configure firewall rules - Open only required ports
- Add logging - Implement comprehensive logging for debugging
- Health checks - Implement proper health check endpoints for monitoring
According to the research findings from Stack Overflow, many developers successfully use this approach to expose HTTP endpoints from Windows Services without any third-party dependencies.
Sources
- Microsoft Learn - Host ASP.NET Core in a Windows Service
- Microsoft Learn - Configure endpoints for ASP.NET Core Kestrel
- Stack Overflow - Expose a status endpoint from a C# Windows Service
- Medium - Add Web API Controllers to a Worker Service
- Microsoft Learn - Kestrel web server in ASP.NET Core
Conclusion
Exposing an HTTP status endpoint from your .NET 8 Windows Service is straightforward using the built-in Kestrel web server. The key steps are:
- Add Kestrel configuration to your Program.cs using
WebHost.ConfigureKestrel() - Add minimal API endpoints using
app.MapGet()to expose your status data - Implement thread-safe status reporting methods in your Worker class
- Configure appropriate ports and security settings for your deployment environment
This approach allows you to maintain the reliability of a Windows Service while providing modern HTTP access for monitoring and status checking. The solution is production-ready and can be easily extended with additional features like authentication, HTTPS, and more detailed status reporting.
For more complex scenarios, consider separating the web interface into a different project that communicates with your Windows Service through established communication patterns like gRPC or message queues.