.net core .net 5 windows service

Create a Windows Service in .NET Core

Matt 2020/12/16 00:23:58
64

What is Windows Service?

Microsoft Windows services (old named NT services), enable us to create long-running executable applications that run in their own Windows sessions. These services can be automatically started when the computer (or a server) boots, also can be paused and restarted.

We can also run services in the security context of a specific user account different from the logged-on user or the default computer account.

We can use a ServiceController class to obtain services installed on computer.

using System;
using System.ServiceProcess;

namespace ServiceControllerDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            foreach (var sc in ServiceController.GetServices())
            {
                Console.WriteLine($"Display Name: {sc.DisplayName}");
                Console.WriteLine($"Service Name: {sc.ServiceName}");
                Console.WriteLine($"Status: {sc.Status}");
                Console.WriteLine($"Can Pause And Continue: {sc.CanPauseAndContinue}");
                Console.WriteLine($"Can Stop: {sc.CanStop}");
                Console.WriteLine($"Can Shutdown: {sc.CanShutdown}");
                Console.WriteLine("===\n");
            }
        }
    }
}

Service List demo

img "Service List"

Workshop

Now, let's try to build a windows service with generic host under .NET 5.

Worker class

First, we create a Worker class to handle our working job.

public class Worker : IWorker
{
    private readonly IConfiguration _configuration;
    private readonly ILogger<Worker> _logger;

    public Worker(IConfiguration configuration, ILogger<Worker> logger)
    {
        _configuration = configuration;
    }

    public void Execute()
    {
        var appSettings = _configuration.GetSection(nameof(AppSettings)).Get<AppSettings>();
		
        // TODO: our task here
        _ = new Timer((e) => {
            _logger.LogInformation($"Worker executed at {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
        }, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
    }

    public void Stop()
    {
        _logger.LogInformation($"Service Stoped at {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
    }
}

public interface IWorker
{
    void Execute();
    void Stop();
}

WorkingService class

Second, we create a WorkingService inherit from IHostedService as a hosted service, that would be added to our services.

public class WorkingService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private readonly IWorker _worker;
    private Timer _timer;

    public WorkingService(IWorker worker, ILogger<WorkingService> logger)
    {
        _logger = logger;
        _worker = worker;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation($"service start at {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
        
        Task.Run(() => {
            try
            {
                // restart service every 12 hours
                _timer = new Timer((e) => _worket.Execute(), null, TimeSpan.Zero, TimeSpan.FromHours(12));
            }
            catch
            {
                throw;
            }
        }, cancellationToken);

        return Task.CompletedTask;
    }
	
    public Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop timers, services
        _logger.LogInformation($"service stop at {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
        _worker.Stop();
        //_timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        // dispose of non-managed resources
        _timer?.Dispose();
    }
}

ServiceBaseLifetime, ServiceBaseLifetimeHostExtensions class

Next, create ServiceBaseLifetime, and ServiceBaseLifetimeHostExtensions these two classes to control windows service itself.

internal sealed class ServiceBaseLifetime : ServiceBase, IHostLifetime
{
    private readonly TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>();

    public ServiceBaseLifetime(IHostApplicationLifetime applicationLifetime)
    {
        ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
    }

    private IHostApplicationLifetime ApplicationLifetime { get; }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
    public Task WaitForStartAsync(CancellationToken cancellationToken)
    {
        cancellationToken.Register(() => _delayStart.TrySetCanceled());
        ApplicationLifetime.ApplicationStopping.Register(Stop);

        new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing.
        return _delayStart.Task;
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
    private void Run()
    {
        try
        {
            Run(this); // This blocks until the service is stopped.
            _delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
        }
        catch (Exception ex)
        {
            _delayStart.TrySetException(ex);
        }
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
    public Task StopAsync(CancellationToken cancellationToken)
    {
        Stop();
        return Task.CompletedTask;
    }

    // Called by base.Run when the service is ready to start.
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
    protected override void OnStart(string[] args)
    {
        _delayStart.TrySetResult(null);
        base.OnStart(args);
    }

    // Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync.
    // That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion.
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
    protected override void OnStop()
    {
        ApplicationLifetime.StopApplication();
        base.OnStop();
    }
}

internal static class ServiceBaseLifetimeHostExtensions
{
    public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
    {
        return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
    }

    public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
    {
        return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
    }
}

Final step, mixing these all parts into Program

We choose Serilog for our logging system.

internal static class Program
{
    private static async Task Main(string[] args)
    {
        var builder = CreateHostBuilder(args);
        var isService = !(Debugger.IsAttached || args.Contains("--console"));
        if (isService)
        {
            await builder.RunAsServiceAsync().ConfigureAwait(false);
        }
        else
        {
            await builder.RunConsoleAsync().ConfigureAwait(false);
        }
    }

    private static IHostBuilder CreateHostBuilder(string[] args)
    {
        return new HostBuilder()
            .UseContentRoot(Utilities.GetCurrentDirectory())
            .ConfigureAppConfiguration((hostingContext, configuration) =>
            {
                var envName = hostingContext.HostingEnvironment.EnvironmentName;
                configuration
                    .SetBasePath(Utilities.GetCurrentDirectory())
                    .AddJsonFile("appsettings.json", false, true)
                    .AddJsonFile($"appsettings.{envName}.json", true, true)
                    .AddEnvironmentVariables()
                    .Build();
            })
            .ConfigureServices((hostcontext, services) => {
                Directory.SetCurrentDirectory(Utilities.GetCurrentDirectory());
                var serilogLogger = new LoggerConfiguration()
                    .MinimumLevel.Information()
                    .WriteTo.RollingFile("logs/service.demo.log")
                    .CreateLogger();

                services.AddLogging(builder =>
                {
                    builder.SetMinimumLevel(LogLevel.Information);
                    builder.AddSerilog(logger: serilogLogger, dispose: true);
                });
                services.AddOptions();
                services.AddLogging();
                services.AddScoped<IWorker, Worker>();
                services.AddHostedService<WorkingService>();
            })
            .ConfigureLogging((hostingContext, logging) =>
            {
                logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                logging.AddConsole();
                logging.AddDebug();
                logging.AddEventLog();
            });
    }
}

public static class Utilities
{
    public static string GetCurrentDirectory()
    {
        return Directory.GetCurrentDirectory().Contains("c:\\windows\\system32", StringComparison.OrdinalIgnoreCase)
            ? "R:\\svc_demo" // a service root path
            : Directory.GetCurrentDirectory();
    }
}

There is an IMPORTANT thing we must take care of it. When a service started, the root path will always in c:\windows\system32, it beacuse of the cmd: sc. And, we need to set root path to our service application located path. It must do correctly, otherwise, the service wouldn't start correctly.

img "a service start incorrectly"

Run And Test

The application run on service mode wouldn't be started directly. To run or test it, we need to deploy this service application first.

Open a cmd line console (must be started in administrator mode), and use these commands to work with services.

-register a service

sc create svcdemo binPath="R:\svc_demo\WindowsServiceDemo.exe" start=delayed-auto

sc create [A_SERVICE_NAME] binPath="[SERVICE_APP_LOCATED_PATH]\[SERVICE_APP_NAME].exe" start=delayed-auto

-start a service

sc start svcdemo

sc start [A_SERVICE_NAME]

img "start a service"

To start a service, and service should be registered first. If a service started successful, we will see the svcdemo shown on MMC console. img "svcdemo-mmc"

-stop a service

sc stop svcdemo

sc stop [A_SERVICE_NAME]

To stop a service.

-delete a service

sc delete svcdemo

sc delete [A_SERVICE_NAME]

To delete a service, and it should be stopped first.

Also, the application could run on console mode. Using a cmd line below

[SERVICE_APP_LOCATED_PATH]\[SERVICE_APP_NAME].exe --console

and the app run.

img "app run on console mode"

Yes, we can now program a custom service on our own.

Enjoy it!


References:

教學課程:建立 Windows 服務應用程式

Host ASP.NET Core in a Windows Service

Introduction to Windows Service Applications

ServiceController class

.NET Generic Host in ASP.NET Core

Background tasks with hosted services in ASP.NET Core

Implement background tasks in microservices with IHostedService and the BackgroundService class

Matt