NetCore Tutorials - NetCore 2.2 Console Hosted As A Windows Service

PeterKottas Edition!

The purpose of this handy guide is to get NetCore on premise as a service hosted like a Windows ServiceBase service. I found this to work better then the original dasMulli example.

  1. Create a Program.cs that starts the service similar to a ServiceBase inherited service.
  2. Demonstrate the template for running services.
  3. GlobalExceptionHandler wired up.
  4. UnobservedTaskExceptionHandler wired up.
  5. EventViewer logging and error logging.

I also recommend reading the tutorial for GlobalExceptionHandling and EventViewer Logging to understand some of the extra code I am using here.

Getting Started

NuGets

The main NuGet package is PeterKottas.DotNetCore.WindowsService. Its source can be found here with some basic documentation to get started.

The second Microsoft.Extensions.PlatformAbstractions is for EventViewer functionality. Drop this code if you are designing this for Linux.

The last three are for adding IConfiguration support for your application.

Program.cs

using Microsoft.Extensions.Configuration;
using PeterKottas.DotNetCore.WindowsService;
using PeterKottas.DotNetCore.WindowsService.Interfaces;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;

namespace NetCore.HostedAsService
{
    public static class Program
    {
        private static string DefaultErrorMessage { get; set; } = "Unhandled Error Occurred. No details are known.";
        private const string SourceName = "ApplicationName";
        private const string LogName = "Application";

        public static void Main(string[] args)
        {
            // Exception Handling Wiring
            AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler;
            TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler;

            ServiceRunner<LongRunningHostService>.Run(config =>
            {
                config.SetName("LongRunningHostService");
                config.Service(serviceConfig =>
                {
                    serviceConfig.ServiceFactory((extraArguments, controller) =>
                    {
                        return new LongRunningHostService();
                    });

                    serviceConfig.OnStart((service, extraParams) =>
                    {
                        Trace.WriteLine("Service {0} started...", "LongRunningHostService");
                        service.Start();
                        LogMessageToEventViewer("Application has started.");
                    });

                    serviceConfig.OnStop(service =>
                    {
                        Trace.WriteLine("Service {0} stopped...", "LongRunningHostService");
                        service.Stop();
                        LogMessageToEventViewer("Application has stopped.");
                    });

                    serviceConfig.OnError(e =>
                    {
                        LogExceptionToEventViewer(e);
                    });
                });
            });
        }

        #region Helpers

        // Cleanup
        private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            try
            {
                if (e.ExceptionObject != null && e.ExceptionObject is Exception uex)
                {
                    Trace.WriteLine(string.Format("{0} Exception: {1}", DefaultErrorMessage, uex.Message));
                    LogExceptionToEventViewer(uex);
                }
                else if (sender is Exception ex)
                {
                    Trace.WriteLine(string.Format("{0} Exception: {1}", DefaultErrorMessage, ex.Message));
                    LogExceptionToEventViewer(ex);
                }
                else { Trace.WriteLine(DefaultErrorMessage); }
            }
            catch { } // Swallow exception
        }

        // Cleanup
        private static void UnobservedTaskExceptionHandler(object sender, UnobservedTaskExceptionEventArgs e)
        {
            try
            {
                e?.SetObserved(); // Prevents the Program from terminating.

                if (e.Exception != null && e.Exception is Exception tuex)
                {
                    Trace.WriteLine(string.Format("{0} Exception: {1}", DefaultErrorMessage, tuex.Message));
                    LogExceptionToEventViewer(tuex);
                }
                else if (sender is Exception ex)
                {
                    Trace.WriteLine(string.Format("{0} Exception: {1}", DefaultErrorMessage, ex.Message));
                    LogExceptionToEventViewer(ex);
                }
                else { Trace.WriteLine(DefaultErrorMessage); }
            }
            catch { } // Swallow exception
        }

        // Cleanup
        public static void LogExceptionToEventViewer(Exception ex)
        {
            if (!EventLog.SourceExists(SourceName))
            { EventLog.CreateEventSource(SourceName, LogName); }

            var eventLogger = new EventLog(LogName)
            {
                Source = SourceName
            };

            if (ex is AggregateException)
            { (ex as AggregateException)?.Flatten(); }

            eventLogger?.WriteEntry($"Message: {ex.Message}\n\rStackTrace: {ex.StackTrace}", EventLogEntryType.Error);
        }

        // Cleanup
        public static void LogMessageToEventViewer(string message)
        {
            if (!EventLog.SourceExists(SourceName))
            { EventLog.CreateEventSource(SourceName, LogName); }

            var eventLogger = new EventLog(LogName)
            {
                Source = SourceName
            };

            eventLogger?.WriteEntry(message, EventLogEntryType.Information);
        }

        #endregion
    }

    public class LongRunningHostService : IMicroService
    {
        private static IConfigurationRoot _configurationRoot { get; set; }

        public void Start()
        {
            var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Debug";

            _configurationRoot = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: true)
                .Build();

            // OnStart Stuff!
        }

        public void Stop()
        {
            // Do Stuff To Stop Work (Graceful Shutdown)
        }
    }
}

Summary

What you see here is almost like a ServiceBase host service setup. LongRunningHostService has Start / Stop. Main only does one thing really and that is start/stop the service which should be built off of IMicroService.

Installing in Windows

There are two options, one via dotnet console and the other using powershell and SC.exe. More details are found at dasMulli's Github.

// dotnet install example
dotnet restore
dotnet run action:install description:"AppDescription" display-name:"AppName" start-immediately:true

// dotnet uninstall example
dotnet run action:stop
dotnet run action:uninstall

Final Thoughts

That's pretty much all there is to get it working. The library is not without issues but for quick and easy setups it should do the job for hosted systems of NetCore based applications. This goes well with the distributed hangfire processing. You can host your HangFire process in a Windows Service even though it is a NetCore application.