McpNetwork.WinNuxService
11.2.3
dotnet add package McpNetwork.WinNuxService --version 11.2.3
NuGet\Install-Package McpNetwork.WinNuxService -Version 11.2.3
<PackageReference Include="McpNetwork.WinNuxService" Version="11.2.3" />
<PackageVersion Include="McpNetwork.WinNuxService" Version="11.2.3" />
<PackageReference Include="McpNetwork.WinNuxService" />
paket add McpNetwork.WinNuxService --version 11.2.3
#r "nuget: McpNetwork.WinNuxService, 11.2.3"
#:package McpNetwork.WinNuxService@11.2.3
#addin nuget:?package=McpNetwork.WinNuxService&version=11.2.3
#tool nuget:?package=McpNetwork.WinNuxService&version=11.2.3
McpNetwork.WinNuxService
McpNetwork.WinNuxService is a lightweight .NET library that simplifies building cross-platform services that run as:
- Windows Services
- Linux systemd services
- Console applications (for debugging)
- macOS background processes (via launchd)
Built on top of the .NET Generic Host, it provides a clean abstraction to build modular services and plugin-based architectures.
Compatible with .NET 10+.
Features
- Run the same application as Windows Service, Linux systemd service, or console app
- Built on Microsoft.Extensions.Hosting
- Full Dependency Injection
- Built-in Logging integration
- Multiple services in one host
- Plugin architecture with startup and runtime loading
- Dynamic plugin loading at runtime (load, start, stop, reload, unload)
- Dependency-isolated plugins via
AssemblyLoadContext - Optional plugin hot-reload without restarting the host
- Named plugin instances with per-instance configuration via
IConfigurablePlugin - Define and access service metadata at startup (
ServiceName,Environment,Version, custom properties) - Embedded HTTP server and SignalR support via
WithWebHost()
Installation
dotnet add package McpNetwork.WinNuxService
Quick Start
using McpNetwork.WinNuxService;
await WinNuxService
.Create()
.WithName("MyService")
.WithEnvironment("Staging")
.WithVersion("1.2.3")
.AddProperty("GitCommit", "abc123def")
.AddService<TestService>()
.RunAsync();
Your application automatically runs correctly as:
- Windows Service
- Linux systemd service
- Console application
Service Metadata and Build-Time Info
WinNuxService allows you to define service metadata at startup. This information is stored in a WinNuxServiceInfo object, which is available via Dependency Injection in all your services.
Configuring Metadata
var host = WinNuxService
.Create()
.WithName("MyService") // Sets the service name
.WithEnvironment("Staging") // Sets the environment
.WithVersion("1.2.3") // Sets the version
.AddProperty("GitCommit", "abc123def") // Add custom key/value
.AddService<TestService>()
.Build();
WithName(string)– sets the service nameWithEnvironment(string)– sets the environment (Development, Staging, Production)WithVersion(string)– sets the service versionAddProperty(string key, string value)– adds any custom property
Accessing Metadata in Services
public class TestService : WinNuxServiceBase
{
private readonly WinNuxServiceInfo _info;
public TestService(WinNuxServiceInfo info)
{
_info = info;
}
protected override async Task ExecuteAsync(CancellationToken token)
{
Console.WriteLine($"Starting {_info.ServiceName} ({_info.Environment}) v{_info.Version}");
foreach (var prop in _info.Properties)
Console.WriteLine($" {prop.Key} = {prop.Value}");
await Task.Delay(Timeout.Infinite, token);
}
}
Running the Host
await host.RunAsync(); // blocking run
// or start/stop programmatically
await host.StartAsync();
await host.StopAsync();
RunAsync()– runs the service host (blocking)StartAsync()– starts the host and waits until all services have fully completedOnStartAsyncbefore returningStopAsync()– stops the host, callingOnStopAsyncon all servicesConfigureServices(Action<IServiceProvider>)– post-build configuration, for setup that requires the fully constructed service provider
Creating a Service
Recommended: inherit WinNuxServiceBase
WinNuxServiceBase is the recommended way to implement a service. It handles all cancellation
boilerplate for you — including safe shutdown regardless of host type (plain, web, Windows Service,
systemd). You only need to implement ExecuteAsync.
public class HeartbeatService : WinNuxServiceBase
{
private readonly ILogger<HeartbeatService> _logger;
public HeartbeatService(ILogger<HeartbeatService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
_logger.LogInformation("Heartbeat at {Time}", DateTime.Now);
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
}
}
There is no need to manage CancellationTokenSource, try/catch for OperationCanceledException,
or override OnStartAsync / OnStopAsync. The base class takes care of all of it.
Unexpected exceptions thrown from ExecuteAsync are surfaced via the OnUnhandledException hook,
which rethrows by default. Override it to log and swallow instead:
protected override void OnUnhandledException(Exception ex)
{
_logger.LogCritical(ex, "Unhandled error in {Service}", GetType().Name);
// swallow — host keeps running
}
Advanced: override OnStartAsync / OnStopAsync
Two cases legitimately require overriding the lifecycle methods.
Case 1 — You need to acquire or release external resources (connections, file handles, etc.):
public class DatabasePollerService : WinNuxServiceBase
{
private SqlConnection? _connection;
public override async Task OnStartAsync(CancellationToken cancellationToken)
{
_connection = new SqlConnection("...");
await _connection.OpenAsync(cancellationToken);
await base.OnStartAsync(cancellationToken); // always call base
}
public override async Task OnStopAsync(CancellationToken cancellationToken)
{
await base.OnStopAsync(cancellationToken); // always call base first
await _connection!.DisposeAsync();
}
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// poll database...
await Task.Delay(TimeSpan.FromSeconds(10), token);
}
}
}
Important: always call
base.OnStartAsync()andbase.OnStopAsync(). Omitting either will break the internal cancellation lifecycle.
Case 2 — You need custom exception handling per service:
public class ResilientService : WinNuxServiceBase
{
private readonly ILogger<ResilientService> _logger;
public ResilientService(ILogger<ResilientService> logger) => _logger = logger;
protected override void OnUnhandledException(Exception ex)
{
_logger.LogCritical(ex, "ResilientService crashed — host stays alive");
// swallow intentionally
}
protected override async Task ExecuteAsync(CancellationToken token) { ... }
}
Low-level: implement IWinNuxService directly
For maximum control, you can implement the interface directly. This is rarely needed.
public class TestService : IWinNuxService
{
public Task OnStartAsync(CancellationToken token)
{
// start your work here
return Task.CompletedTask;
}
public Task OnStopAsync(CancellationToken token)
{
// stop your work here
return Task.CompletedTask;
}
}
Warning: when implementing
IWinNuxServicedirectly, you are responsible for safe cancellation. The token passed toOnStartAsyncis controlled by the host and its lifecycle varies depending on the host type (plain vs web). Use a linkedCancellationTokenSourceowned by your service to avoid shutdown hangs.
Running Multiple Services
await WinNuxService
.Create()
.AddService<ServiceA>()
.AddService<ServiceB>()
.AddService<ServiceC>()
.RunAsync();
All services run inside the same host process.
Dependency Injection
Standard .NET DI works out of the box.
.ConfigureServices((ctx, services) =>
{
services.AddSingleton<IDatabase, Database>();
})
Services receive dependencies through constructor injection.
Logging
.ConfigureLogging(logging =>
{
logging.AddConsole();
})
Compatible with:
- Serilog
- NLog
- Application Insights
- any
Microsoft.Extensions.Loggingprovider
Embedded HTTP Server and SignalR
WinNuxService supports embedding an ASP.NET Core HTTP server and/or a SignalR hub directly
inside the host process — alongside your background services — via the WithWebHost() method.
This is useful for exposing health endpoints, REST APIs, or real-time communication without running a separate web process.
How it works
WithWebHost() accepts two optional delegates:
configureBuilder— runs during the builder phase: register ASP.NET Core services such asAddSignalR(),AddControllers(), etc.configureApp— runs during the app phase: map routes, hubs, and middleware withMapGet(),MapHub(), etc.
Your background services registered with AddService<T>() continue to run alongside the HTTP layer
in the same process. All existing features — DI, logging, metadata, plugins — work unchanged.
Health and info endpoints
await WinNuxService
.Create()
.WithName("MyApiService")
.WithVersion("2.0.0")
.WithWebHost(
configureApp: app =>
{
app.MapGet("/health", () => Results.Ok(new { status = "alive" }));
app.MapGet("/info", (WinNuxServiceInfo info) => Results.Ok(info));
}
)
.AddService<HeartbeatService>()
.RunAsync();
With SignalR
await WinNuxService
.Create()
.WithName("MyRealtimeService")
.WithWebHost(
configureBuilder: builder =>
{
builder.Services.AddSignalR();
},
configureApp: app =>
{
app.MapHub<NotificationHub>("/notifications");
app.MapGet("/health", () => "OK");
}
)
.AddService<HeartbeatService>()
.RunAsync();
The hub itself is a standard ASP.NET Core Hub — no WinNuxService-specific code required:
public class NotificationHub : Hub
{
public async Task SendMessage(string message) =>
await Clients.All.SendAsync("ReceiveMessage", message);
}
Full example with SignalR and background services
// Program.cs
await WinNuxService
.Create()
.WithName("WinNuxService-WebDemo")
.WithEnvironment("Demo")
.WithVersion("1.0.0")
.ConfigureLogging(logging =>
{
logging.AddConsole();
logging.AddDebug();
})
.WithWebHost(
configureBuilder: builder =>
{
builder.Services.AddSignalR();
},
configureApp: app =>
{
app.MapGet("/health", () => Results.Ok(new { status = "alive" }));
app.MapGet("/info", (WinNuxServiceInfo info) => Results.Ok(info));
app.MapHub<NotificationHub>("/notifications");
}
)
.AddService<HeartbeatService>()
.RunAsync();
// HeartbeatService.cs
public class HeartbeatService : WinNuxServiceBase
{
private readonly WinNuxServiceInfo _info;
private readonly ILogger<HeartbeatService> _logger;
public HeartbeatService(WinNuxServiceInfo info, ILogger<HeartbeatService> logger)
{
_info = info;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
_logger.LogInformation("Heartbeat from {Name} at {Time}", _info.ServiceName, DateTime.Now);
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
}
}
Middleware
When using WithWebHost(), the full ASP.NET Core middleware pipeline is available. Middlewares are
registered inside configureApp using the standard UseMiddleware<T>() or Use() API, and must
be added before route mappings so they wrap all incoming requests.
Typed middleware — implement a class with an InvokeAsync method:
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly string _expectedKey;
public ApiKeyMiddleware(RequestDelegate next, IConfiguration config)
{
_next = next;
_expectedKey = config["ApiKey"] ?? throw new InvalidOperationException("ApiKey not configured");
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("X-Api-Key", out var key) || key != _expectedKey)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized");
return;
}
await _next(context);
}
}
Register it via UseMiddleware<T>() in configureBuilder / configureApp:
.WithWebHost(
configureBuilder: builder =>
{
builder.Services.AddSignalR();
},
configureApp: app =>
{
// Middlewares first — they wrap everything below
app.UseMiddleware<ApiKeyMiddleware>();
// Then routes and hubs
app.MapGet("/health", () => Results.Ok(new { status = "alive" }));
app.MapHub<NotificationHub>("/notifications");
}
)
Inline middleware — for simple, one-off cases:
configureApp: app =>
{
app.Use(async (context, next) =>
{
context.Response.Headers["X-Powered-By"] = "WinNuxService";
await next(context);
});
app.MapGet("/health", () => "OK");
}
Fluent shortcut — UseMiddleware<T>() is also available directly on the builder for a cleaner
registration style alongside AddService<T>():
await WinNuxService
.Create()
.WithName("MyApiService")
.UseMiddleware<RequestLoggingMiddleware>() // registered here
.UseMiddleware<ApiKeyMiddleware>() // in order
.WithWebHost(
configureApp: app =>
{
app.MapGet("/health", () => "OK");
app.MapHub<NotificationHub>("/notifications");
}
)
.AddService<HeartbeatService>()
.RunAsync();
Middleware order matters. The pipeline executes top to bottom. A typical sensible order is: exception handling → request logging → authentication → authorization → endpoints.
Minimal Example (~60 lines)
Program.cs
using McpNetwork.WinNuxService;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
await WinNuxService
.Create()
.WithName("HeartbeatHost")
.WithVersion("1.0.0")
.AddService<HeartbeatService>()
.AddService<TimeService>()
.ConfigureServices((ctx, services) =>
{
services.AddSingleton<IMessenger, ConsoleMessenger>();
})
.ConfigureLogging(logging => logging.AddConsole())
.RunAsync();
public interface IMessenger { void Send(string message); }
public class ConsoleMessenger : IMessenger
{
public void Send(string message) => Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
}
HeartbeatService.cs
public class HeartbeatService : WinNuxServiceBase
{
private readonly IMessenger _messenger;
public HeartbeatService(IMessenger messenger) { _messenger = messenger; }
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
_messenger.Send("HeartbeatService alive");
await Task.Delay(3000, token);
}
}
}
TimeService.cs
public class TimeService : WinNuxServiceBase
{
private readonly IMessenger _messenger;
public TimeService(IMessenger messenger) { _messenger = messenger; }
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
_messenger.Send($"Current time: {DateTime.Now}");
await Task.Delay(5000, token);
}
}
}
Plugin Architecture
Plugins can be loaded dynamically from external assemblies.
Example directory structure:
MyService/
│
├── MyService.exe
│
└── plugins/
├── PluginA/
│ ├── PluginA.dll
│ └── dependencies
│
└── PluginB/
├── PluginB.dll
└── dependencies
Each plugin is loaded using its own AssemblyLoadContext, allowing:
- dependency isolation
- different dependency versions
- safe unloading
- runtime reloading
Plugin Reloading
Plugins can be reloaded without restarting the main host.
Reload sequence:
- Stop plugin
- Cancel running tasks
- Unload AssemblyLoadContext
- Load new assembly
- Restart plugin
This enables live updates in production environments.
Plugin Configuration and Named Instances
When the same plugin DLL needs to run as multiple independent instances — each with its own
settings — implement IConfigurablePlugin alongside IWinNuxService.
Implementing IConfigurablePlugin
public class SensorPlugin : WinNuxServiceBase, IConfigurablePlugin
{
private string _instanceName = string.Empty;
private string _endpoint = string.Empty;
public void Configure(string instanceName, IConfiguration configuration)
{
_instanceName = instanceName;
_endpoint = configuration["Endpoint"]
?? throw new InvalidOperationException("Endpoint not configured");
}
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
Console.WriteLine($"[{_instanceName}] Polling {_endpoint}");
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
}
}
Loading and configuring multiple instances
ConfigurePlugin must be called after LoadPlugin and before StartPlugin:
var host = WinNuxService
.Create()
.WithName("SensorHost")
.Build();
var config = host.Services.GetRequiredService<IConfiguration>();
// First instance
var sensorA = host.Plugins.LoadPlugin("plugins/SensorPlugin/SensorPlugin.dll", host.Services);
host.Plugins.ConfigurePlugin(sensorA, "Sensor-A", config.GetSection("Sensors:A"));
await host.Plugins.StartPlugin(sensorA);
// Second instance — same DLL, different name and config
var sensorB = host.Plugins.LoadPlugin("plugins/SensorPlugin/SensorPlugin.dll", host.Services);
host.Plugins.ConfigurePlugin(sensorB, "Sensor-B", config.GetSection("Sensors:B"));
await host.Plugins.StartPlugin(sensorB);
await host.RunAsync();
Inspecting instance names at runtime
LoadedPlugin.InstanceName is populated by ConfigurePlugin and available on every entry
returned by IPluginManager.Plugins:
foreach (var plugin in host.Plugins.Plugins)
{
Console.WriteLine($"{plugin.Name} / {plugin.InstanceName ?? "(no instance name)"} — {plugin.State}");
}
Note: plugins that do not implement
IConfigurablePluginare completely unaffected.ConfigurePluginis a no-op for them and can safely be omitted.
Platform Support
| Platform | Support |
|---|---|
| Windows | Windows Service |
| Linux | systemd |
| macOS | Console / launchd |
WinNuxService detects the runtime environment automatically. The same binary runs as a console application when launched interactively, and as a native service when started by the OS service manager — no code change required.
Deploying as a Windows Service
1. Publish the application
dotnet publish -c Release -r win-x64 --self-contained true -o C:\Services\MyService
2. Create the Windows Service
Open a command prompt as Administrator:
sc create MyService binPath="C:\Services\MyService\MyService.exe" start=auto
sc description MyService "My WinNuxService background service"
3. Start the service
sc start MyService
4. Manage the service
sc stop MyService # graceful stop
sc delete MyService # uninstall
sc query MyService # check status
You can also use the Services panel (services.msc) or PowerShell:
Start-Service MyService
Stop-Service MyService
Get-Service MyService
Logging on Windows: when running as a Windows Service, console output is suppressed. Use
AddEventLog()to write to the Windows Event Log, or configure a file-based logger such as Serilog with a rolling file sink.
Deploying as a Linux systemd Service
1. Publish the application
dotnet publish -c Release -r linux-x64 --self-contained true -o /opt/myservice
chmod +x /opt/myservice/MyService
2. Create the systemd unit file
Create /etc/systemd/system/myservice.service:
[Unit]
Description=My WinNuxService background service
After=network.target
[Service]
Type=notify
ExecStart=/opt/myservice/MyService
WorkingDirectory=/opt/myservice
Restart=on-failure
RestartSec=5
User=myservice
Group=myservice
# Logging — captured by journald automatically
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myservice
# Environment
Environment=DOTNET_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target
Type=notifytells systemd to wait for the process to signal readiness before considering the service started. WinNuxService emits this signal automatically viaUseSystemd().
3. Create a dedicated user (recommended)
useradd -r -s /bin/false myservice
chown -R myservice:myservice /opt/myservice
4. Enable and start the service
systemctl daemon-reload
systemctl enable myservice # start automatically on boot
systemctl start myservice
5. Manage and inspect the service
systemctl status myservice # current status
systemctl stop myservice # graceful stop
systemctl restart myservice # stop then start
journalctl -u myservice -f # follow live logs
journalctl -u myservice --since "1 hour ago" # recent logs
Logging on Linux:
journaldcaptures everything written to stdout/stderr, sologging.AddConsole()is sufficient. No file sink required unless you need log rotation outside of journald.
macOS (launchd)
On macOS the application runs as a console process or can be registered with launchd using a
.plist file. For most use cases, running it directly or via a process supervisor such as
supervisord is simpler.
When Should You Use WinNuxService?
Background Processing Server
Examples:
- queue consumers
- batch processing
- scheduled jobs
IoT Gateway
Examples:
- device communication
- telemetry processing
- protocol plugins
Plugin-Based Enterprise Services
Examples:
- dynamically extend server capabilities
- load new modules without redeploying
- isolate external dependencies
Hybrid Service + API
Examples:
- background worker exposing a
/healthendpoint - real-time telemetry pushed over SignalR
- internal REST API alongside scheduled jobs
Requirements
- .NET 10 or later
- Windows / Linux / macOS
License
McpNetwork.WinNuxService is licensed under the MIT License. See LICENSE for more information.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- Microsoft.AspNetCore.TestHost (>= 10.0.9)
- Microsoft.Extensions.Hosting.Systemd (>= 10.0.9)
- Microsoft.Extensions.Hosting.WindowsServices (>= 10.0.9)
- Newtonsoft.Json (>= 13.0.4)
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 | |
|---|---|---|---|
| 11.2.3 | 67 | 6/16/2026 | |
| 11.2.2.8 | 130 | 4/10/2026 | |
| 11.2.1.7 | 109 | 4/7/2026 | |
| 11.2.0.5 | 114 | 4/6/2026 | |
| 11.1.1 | 109 | 4/1/2026 | |
| 11.1.0 | 120 | 3/13/2026 | |
| 11.0.0 | 112 | 3/11/2026 | |
| 10.0.0 | 135 | 1/16/2026 | |
| 8.1.0 | 411 | 12/8/2023 | |
| 7.1.0 | 232 | 12/8/2023 | |
| 7.0.0 | 537 | 11/13/2022 | |
| 6.1.0 | 256 | 12/8/2023 | |
| 6.0.0 | 1,345 | 11/19/2021 | |
| 1.1.1 | 521 | 5/24/2021 | |
| 1.0.3 | 800 | 5/16/2021 |