NeuroAgent

How to Add HTTP Status Endpoint to .NET 8 Windows Service

Learn how to expose HTTP status endpoints from .NET 8 Windows Services using Kestrel. Complete guide with code examples for monitoring worker threads and pending jobs.

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

csharp
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

csharp
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?

NeuroAgent

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

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:

bash
dotnet add package Microsoft.AspNetCore.App
dotnet add package Microsoft.Extensions.Hosting

Then modify your Program.cs to include web server functionality:

csharp
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)

csharp
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)

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:

bash
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

csharp
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:

csharp
// 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:

csharp
// 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:

bash
dotnet new worker -n StatusWebService
dotnet add package Microsoft.AspNetCore.App

2. Reverse Proxy with IIS

For production environments, consider running Kestrel behind IIS:

csharp
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:

csharp
// 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

  1. Run locally:
bash
dotnet run
  1. Test endpoints:
bash
curl http://localhost:5000/status
curl http://localhost:5000/health
  1. Install as Windows Service:
bash
sc.exe create "MyStatusWebService" binpath="C:\path\to\your\service.exe"
sc.exe start "MyStatusWebService"

Production Deployment

For production deployment, consider these best practices:

  1. Use HTTPS - Configure SSL certificates for secure communication
  2. Set proper permissions - Ensure the service account has necessary network access
  3. Configure firewall rules - Open only required ports
  4. Add logging - Implement comprehensive logging for debugging
  5. 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

  1. Microsoft Learn - Host ASP.NET Core in a Windows Service
  2. Microsoft Learn - Configure endpoints for ASP.NET Core Kestrel
  3. Stack Overflow - Expose a status endpoint from a C# Windows Service
  4. Medium - Add Web API Controllers to a Worker Service
  5. 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:

  1. Add Kestrel configuration to your Program.cs using WebHost.ConfigureKestrel()
  2. Add minimal API endpoints using app.MapGet() to expose your status data
  3. Implement thread-safe status reporting methods in your Worker class
  4. 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.