{"id":256,"date":"2021-02-10T11:23:38","date_gmt":"2021-02-10T19:23:38","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/?p=256"},"modified":"2021-02-10T11:23:38","modified_gmt":"2021-02-10T19:23:38","slug":"creating-a-windows-service-with-c-net5","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/creating-a-windows-service-with-c-net5\/","title":{"rendered":"Creating a Windows Service with C#\/.NET5"},"content":{"rendered":"<h2>Windows Services<\/h2>\n<p><a href=\"https:\/\/docs.microsoft.com\/dotnet\/framework\/windows-services\/introduction-to-windows-service-applications\">Windows Services<\/a> have been a foundation of Windows since Windows NT. They help users, or system admins, manage long-running programs that execute in their own Windows sessions. It is fairly trivial to set them up and start whenever the computer boots, for example. Since they can be completely UI-less, they provide an interesting approach for tasks that don&#8217;t require user interaction, such as looking for directory or file updates, pooling a service, or logging data. Windows itself uses services for many common task that an operating system should do, like <code>Windows Defender<\/code>, and <code>Windows Update<\/code>.<\/p>\n<p>This blog post will demonstrate how to build a system file watcher as a Windows service that runs in the background, and classifies images using the WinML model we&#8217;ve previously set in <a href=\"https:\/\/aka.ms\/cmd-net5\">my previous blog post<\/a>.<\/p>\n<hr \/>\n<h2>ASP.NET similarities<\/h2>\n<p>If you ever created an <code>ASP.NET Core<\/code> project, you probably noticed that the default templates create a <code>Program.cs<\/code> file. This file contains the <em>worldly famous<\/em> <code>Main<\/code> method. Actually, most templates create this method, aside from class libraries, which don&#8217;t necessarily provide a starting point for your code to execute.<\/p>\n<p>In my <a href=\"https:\/\/aka.ms\/cmd-net5\">last blog post<\/a> we created a small program that parses the arguments passed down to it and processes one image file, using WinML. For a <em>Windows Service<\/em>, which must not end its execution straight away, we need to halt the termination of our process until the Windows Service manually is terminated, not only after a single image is processed. We don&#8217;t want the <code>Start<\/code>-&gt;<code>Process<\/code>-&gt;<code>End<\/code> flow to exist. To achieve this, instead of using the command line argument to inform a single image file path, we will change our software to expect a Directory path, which we will listen for file changes. Then, whenever a new file is created inside this folder we are watching, we will run our WinML code to move the file to a folder named after its category, recognized by our WinML model.<\/p>\n<p><code>ASP.NET<\/code> is the most common framework to provide a <strong>web service<\/strong> using <code>.NET<\/code>, but unlike a web service, a <strong>Windows Service<\/strong> is not necessarily accessed through the network. It is, still, quite simple to setup a Windows Service that hosts a web service. Since this is a possible scenario, Microsoft provides a NuGet package named <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Extensions.Hosting.WindowsServices\/\">Microsoft.Extensions.Hosting.WindowsServices<\/a> that let us host our <code>.NET<\/code> process, with or without <code>ASP.NET<\/code>, as a Windows Service. It automatically provides logging capabilities to the <a href=\"https:\/\/docs.microsoft.com\/windows\/win32\/events\/windows-events\">Windows Events<\/a>, the default output where <code>Windows Services<\/code> should log information to, as well as automatically logging <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/framework\/windows-services\/introduction-to-windows-service-applications#service-lifetime\">life-cycle events<\/a>, such as <code>Started<\/code>, <code>Stopping<\/code> and <code>Stopped<\/code> events. It also provides a helpful method to detect if your process is running as a windows service or not.<\/p>\n<hr \/>\n<h2>Coding, coding, coding!<\/h2>\n<p>Lets start with the <a href=\"https:\/\/github.com\/azchohfi\/ImageClassifierSample\/tree\/net5cmd\">code we ended up with at our last blog post<\/a>, and add the <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Extensions.Hosting.WindowsServices\"><code>Microsoft.Extensions.Hosting.WindowsServices<\/code> NuGet package<\/a>:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-NuGet.png\" alt=\"Adding the Microsoft.Extensions.Hosting.WindowsServices NuGet package\" \/><\/p>\n<p>With this NuGet installed inside our project, we can change our <code>Main<\/code> method to initialize a Host, which will initialize it as a Windows Service and run it. We&#8217;ll move all the <code>WinML<\/code> related code to a new Worker class, which will act as a hosted service without our host process. The code itself is simpler than it sounds:<\/p>\n<pre><code class=\"csharp\">using CommandLine;\r\nusing Microsoft.Extensions.DependencyInjection;\r\nusing Microsoft.Extensions.Hosting;\r\nusing Microsoft.Extensions.Logging;\r\nusing Microsoft.Extensions.Logging.EventLog;\r\nusing System.Threading.Tasks;\r\n\r\nnamespace ImageClassifier\r\n{\r\n    class Program\r\n    {\r\n        static async Task&lt;int&gt; Main(string[] args)\r\n        {\r\n            return await Parser.Default.ParseArguments&lt;CommandLineOptions&gt;(args)\r\n                .MapResult(async (opts) =&gt;\r\n                {\r\n                    await CreateHostBuilder(args, opts).Build().RunAsync();\r\n                    return 0;\r\n                },\r\n                errs =&gt; Task.FromResult(-1)); \/\/ Invalid arguments\r\n        }\r\n\r\n        public static IHostBuilder CreateHostBuilder(string[] args, CommandLineOptions opts) =&gt;\r\n            Host.CreateDefaultBuilder(args)\r\n                .ConfigureLogging(configureLogging =&gt; configureLogging.AddFilter&lt;EventLogLoggerProvider&gt;(level =&gt; level &gt;= LogLevel.Information))\r\n                .ConfigureServices((hostContext, services) =&gt;\r\n                {\r\n                    services.AddSingleton(opts);\r\n                    services.AddHostedService&lt;ImageClassifierWorker&gt;()\r\n                        .Configure&lt;EventLogSettings&gt;(config =&gt;\r\n                        {\r\n                            config.LogName = \"Image Classifier Service\";\r\n                            config.SourceName = \"Image Classifier Service Source\";\r\n                        });\r\n                }).UseWindowsService();\r\n    }\r\n}\r\n\r\n<\/code><\/pre>\n<p>As you can see, we are still using the <a href=\"https:\/\/www.nuget.org\/packages\/CommandLineParser\">CommandLineParser NuGet package<\/a> that we&#8217;ve used on the last blog post, since we still want to make this software act based on the parameters that we invoked it with. You&#8217;ll notice that the <code>CreateHostBuilder<\/code> method has both the string array parameter, as well as our already parsed and valid <code>CommandLineOptions<\/code>. We need both, since we want the parsed one and due to the fact that the <code>Host.CreateDefaultBuilder<\/code> method allows you to pass parameters from the command line to configure it, so we need to pass down the original string array to it.<\/p>\n<p>We start by creating this default builder. We then configure its logging to filter only if the log level is <code>Information<\/code> or higher, and then we add our hosted service. We also create a singleton with our parsed <code>CommandLineOptions<\/code>, which allows us to use it inside the <code>ImageClassifierWorker<\/code> class that is initialized for us by the default builder. We will expect a <code>CommandLineOptions<\/code> parameter in the constructor of our <code>ImageClassifierWorker<\/code> which will be automatically injected for us. We are also configuring our <code>EventLogSettings<\/code> with a <code>LogName<\/code> and a <code>SourceName<\/code>. These parameters let us choose where the Events from our logs will be stored. Last, but not least, we are initializing this host as a Windows Service. This last step is as easy as calling the <a href=\"https:\/\/docs.microsoft.com\/\/dotnet\/api\/microsoft.extensions.hosting.windowsservicelifetimehostbuilderextensions.usewindowsservice\">UseWindowsService()<\/a> method, which configures the host to use the logging properties we just set, as well as setting up automatic logging for the Windows Service lifetime events.<\/p>\n<p>Since this new code will listen to new files in the directory we specify, we need a way to filter the file extensions we want to allow processing. We could hard-code this, but lets simply add a parameter to our <code>CommandLineOptions<\/code> class, which will easily handle all the parsing for us:<\/p>\n<pre><code class=\"csharp\">using CommandLine;\r\n\r\nnamespace ImageClassifier\r\n{\r\n    public class CommandLineOptions\r\n    {\r\n        [Value(index: 0, Required = true, HelpText = \"Path to watch.\")]\r\n        public string Path { get; set; }\r\n\r\n        [Option(shortName: 'e', longName: \"extensions\", Required = false, HelpText = \"Valid image extensions.\", Default = new[] { \"png\", \"jpg\", \"jpeg\" })]\r\n        public string[] Extensions { get; set; }\r\n\r\n        [Option(shortName: 'c', longName: \"confidence\", Required = false, HelpText = \"Minimum confidence.\", Default = 0.9f)]\r\n        public float Confidence { get; set; }\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>See how easy it is to support our list of extensions? Since these are the most commons ones, we support <code>png<\/code>, <code>jpg<\/code>, and <code>jpeg<\/code>, and all the classes we use to load our image handle these extensions, so we are safe with them. We&#8217;ve also updated the HelpText of the <code>Path<\/code> argument, to better reflect what it actually means.<\/p>\n<p>Now that you understand the structure of our <code>Program.cs<\/code>, and we&#8217;ve updated our arguments, lets look at our new <code>ImageClassifierWorker<\/code> class, which inherits from <a href=\"https:\/\/docs.microsoft.com\/dotnet\/api\/microsoft.extensions.hosting.backgroundservice\"><code>BackgroundService<\/code><\/a>:<\/p>\n<pre><code class=\"csharp\">namespace ImageClassifier\r\n{\r\n    public class ImageClassifierWorker : BackgroundService\r\n    {\r\n        private SqueezeNetModel _squeezeNetModel;\r\n        private readonly List&lt;string&gt; _labels = new List&lt;string&gt;();\r\n\r\n        private readonly ILogger&lt;ImageClassifierWorker&gt; _logger;\r\n        private readonly CommandLineOptions _options;\r\n\r\n        public ImageClassifierWorker(ILogger&lt;ImageClassifierWorker&gt; logger, CommandLineOptions options)\r\n        {\r\n            _logger = logger;\r\n            _options = options;\r\n        }\r\n\r\n        protected override async Task ExecuteAsync(CancellationToken stoppingToken)\r\n        {\r\n            ...\r\n        }\r\n<\/code><\/pre>\n<p>This is the class that will run our <code>WinML<\/code> code. The <code>ExecuteAsync<\/code> method is going to be called automatically for us, so we need a mechanism to keep it from returning, to keep our process from ending. There are many ways to achieve that, specially since we need to return a <code>Task<\/code>. I&#8217;ve chosen a simple one that can be achieved in only 3 lines of code:<\/p>\n<pre><code class=\"csharp\">    ...\r\n\r\n    var tcs = new TaskCompletionSource&lt;bool&gt;();\r\n    stoppingToken.Register(s =&gt; ((TaskCompletionSource&lt;bool&gt;)s).SetResult(true), tcs);\r\n    await tcs.Task;\r\n\r\n    _logger.LogInformation(\"Service stopped\");\r\n}\r\n<\/code><\/pre>\n<p>This should be at the very end of our <code>ExecuteAsync<\/code> method. The <code>stoppingToken<\/code> is passed down to our <code>ExecuteAsync<\/code> method, and it is an instance of <a href=\"https:\/\/docs.microsoft.com\/dotnet\/api\/system.threading.cancellationtoken\"><code>CancellationToken<\/code><\/a> which will be cancelled when our service is requested to stop. See how we are using the <code>_logger<\/code> object that we stored in our constructor? That is a handy helper that lets us log whatever we want\/need. Remember that this code is headless (no user interface at all), so we need a way to know what our code is executing, and that is done through logs.<\/p>\n<p>At the start of our method, we should load our <code>WinML<\/code> model, pretty much the same way we were doing before:<\/p>\n<pre><code class=\"csharp\">protected override async Task ExecuteAsync(CancellationToken stoppingToken)\r\n{\r\n    _logger.LogInformation(\"Service started\");\r\n\r\n    if (!Directory.Exists(_options.Path))\r\n    {\r\n        _logger.LogError($\"Directory \"{_options.Path}\" does not exist.\");\r\n        return;\r\n    }\r\n\r\n    var rootDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);\r\n\r\n    \/\/ Load labels from JSON\r\n    foreach (var kvp in JsonSerializer.Deserialize&lt;Dictionary&lt;string, string&gt;&gt;(File.ReadAllText(Path.Combine(rootDir, \"Labels.json\"))))\r\n    {\r\n        _labels.Add(kvp.Value);\r\n    }\r\n\r\n    _squeezeNetModel = SqueezeNetModel.CreateFromFilePath(Path.Combine(rootDir, \"squeezenet1.0-9.onnx\"));\r\n\r\n...\r\n<\/code><\/pre>\n<p>The interesting code that we are adding here is this:<\/p>\n<pre><code class=\"csharp\">_logger.LogInformation($\"Listening for images created in \"{_options.Path}\"...\");\r\n\r\nusing FileSystemWatcher watcher = new FileSystemWatcher\r\n{\r\n    Path = _options.Path\r\n};\r\nforeach (var extension in _options.Extensions)\r\n{\r\n    watcher.Filters.Add($\"*.{extension}\");\r\n}\r\nwatcher.Created += async (object sender, FileSystemEventArgs e) =&gt;\r\n{\r\n    await Task.Delay(1000);\r\n    await ProcessFileAsync(e.FullPath, _options.Confidence);\r\n};\r\n\r\nwatcher.EnableRaisingEvents = true;\r\n<\/code><\/pre>\n<p>This creates a <a href=\"https:\/\/docs.microsoft.com\/dotnet\/api\/system.io.filesystemwatcher\">FileSystemWatcher<\/a> instance that will hook up to some Windows events for us, and automatically raise the <a href=\"https:\/\/docs.microsoft.com\/dotnet\/api\/system.io.filesystemwatcher.created\"><code>Created<\/code><\/a> event whenever there is a new file created on the folder we are watching. Notice that we are adding <a href=\"https:\/\/docs.microsoft.com\/dotnet\/api\/system.io.filesystemwatcher.filters\"><code>Filters<\/code><\/a> to it, which are mapping to the extension filters coming from our new command line argument.<\/p>\n<p>We are waiting 1000 milliseconds (1 second), before we process the file. The <code>FileSystemWatcher<\/code> is attached to the Windows events, and is so fast that it will raise the <code>Created<\/code> event even before the file is closed by whichever process is still creating it. If we didn&#8217;t add a Delay, our <code>ProcessFileAsync<\/code> method would throw an exception:<\/p>\n<pre><code>System.IO.IOException: The process cannot access the file 'Bla.jpg' because it is being used by another process.\r\n<\/code><\/pre>\n<blockquote><p>We could have created a retry logic if we caught this specific exception but, again, this solution is a simplification, and will keep our code simple for this sample.<\/p><\/blockquote>\n<p>The <code>ProcessFileAsync<\/code> method expects a full path to an image file and a confidence float, just like we had before. I&#8217;ve just extracted it to its own method. Also, I&#8217;ve changed the logic at the end to, instead of just printing the classification to the Console, to move the file to its correct folder:<\/p>\n<pre><code class=\"csharp\">...\r\n    if (results[0].p &gt;= confidence)\r\n    {\r\n        MoveFileToFolder(filePath, _labels[results[0].index]);\r\n\r\n        return true; \/\/ Success\r\n    }\r\n    ...\r\n    return false; \/\/ Not enough confidence or error\r\n} \/\/ End of ProcessFileAsync method\r\n...\r\n\r\nprivate void MoveFileToFolder(string filePath, string folderName)\r\n{\r\n    var directory = Path.GetDirectoryName(filePath);\r\n    var fileName = Path.GetFileName(filePath);\r\n    var destinationDirectory = Path.Combine(directory, folderName);\r\n\r\n    Directory.CreateDirectory(destinationDirectory);\r\n\r\n    File.Move(filePath, Path.Combine(destinationDirectory, fileName), false);\r\n}\r\n\r\n<\/code><\/pre>\n<p>Notice that the <code>folderName<\/code> argument passed in our <code>MoveFileToFolder<\/code> method is the label with highest confidence, returned by our WinML classification model for that one specific image. Therefore, we are moving that file inside a folder with its category name. The whole process is so fast that if you copy and paste an image file into the folder we are watching, it barely stays inside the folder, almost instantaneously being moved to a newly (or existing) folder.<\/p>\n<h2>Where is the service?<\/h2>\n<p>The next step is to <code>(1)<\/code>deploy this somewhere, <code>(2)<\/code>register our Windows Service, and <code>(3)<\/code>start it!<\/p>\n<p>The deployment step is very straight forward. Right-click in our <code>ImageClassifier<\/code> project, inside Visual Studio, and select <code>Publish<\/code>. This will let you select where you want to publish it, and since this is a simple .NET5 console application, we can choose between a variety of places. Lets select a simple <code>Folder<\/code>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-Publish.png\" alt=\"Publishing our project to a Folder\" \/><\/p>\n<p>On the next step, just select <code>Folder<\/code> again and click <code>Next<\/code>. On the last step, we need to select a path. I&#8217;ve chosen a simple folder inside my Desktop, but this could be anywhere you need it to, as long as the user you select to run the service have the proper permissions to execute it.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-PublishLastStep.png\" alt=\"Last step of the publishing process, where we select a Folder path\" \/><\/p>\n<p>Click on <code>Finish<\/code> to create the publish profile, and then on <code>Publish<\/code> to build our code and deploy it!<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-Publishing.png\" alt=\"Publish in progress\" \/><\/p>\n<p>You will see that all the files that we need are properly deployed in that folder, so we can now configure our service!<\/p>\n<blockquote><p>Another way to publish your project is using the <a href=\"https:\/\/docs.microsoft.com\/dotnet\/core\/tools\/dotnet-publish\"><code>dotnet publish<\/code><\/a> command.<\/p><\/blockquote>\n<h2>Service Controller<\/h2>\n<p>Windows Services are managed through a tool called <a href=\"https:\/\/docs.microsoft.com\/windows\/win32\/services\/controlling-a-service-using-sc\">Service Controller<\/a>, a.k.a. <code>SC<\/code>. We&#8217;ll use one simple command to <a href=\"https:\/\/docs.microsoft.com\/previous-versions\/windows\/it-pro\/windows-server-2012-r2-and-2012\/cc990289(v=ws.11)\">create<\/a> a windows service, and we&#8217;ll start it manually through the services tab. Remember that you need admin privileges to create a service on Windows, so run an elevated command line to run these commands.<\/p>\n<p>All we need is a name for our service, and which command should be executed (the binary path).<\/p>\n<pre><code>sc create \"Image Classifier Service\" binPath=\"C:\\Users\\alzollin\\Desktop\\ImageClassification\\bin\\ImageClassifier.exe C:\\Users\\alzollin\\Desktop\\Image\\ClassificationImages\"\r\n<\/code><\/pre>\n<p>Notice that we are passing the binPath argument in quotes. We are passing our arguments to the executable right after the path, still inside the quotes, which will be handled by our implementation of the <code>CommandLineParser<\/code>.<\/p>\n<p>Running this command should return a simple message, if successful:<\/p>\n<pre><code>[SC] CreateService SUCCESS\r\n<\/code><\/pre>\n<p>This means our service is created, but not yet running. To run it, lets call the <code>SC<\/code> command again, but now with different parameters:<\/p>\n<pre><code>sc start \"Image Classifier Service\"\r\n<\/code><\/pre>\n<p>This will log some properties on the console, and you will probably read <code>START_PENDING<\/code> in the <code>STATE<\/code>, since the <code>SC<\/code> command will return immediately. The simplest way to check the state of your service is at Windows&#8217;s <code>Task Manager<\/code>. If you never noticed, there is a <code>Services<\/code> tab:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-TaskManager.png\" alt=\"Task Manager showing our service running\" \/><\/p>\n<p>This means that our service is running! Lets see if it works!<\/p>\n<h2>Code in Action!<\/h2>\n<p>Since I&#8217;ve added the <code>C:\\Users\\alzollin\\Desktop\\ImageClassification\\Images<\/code> path parameter when I created the service, it will be listening to new files created at that folder. Here is the folder before I do anything:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-EmptyFolder.png\" alt=\"Empty Folder\" \/><\/p>\n<p>And I&#8217;m going to copy all these files at the same time inside this empty folder:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-CatsAndDogs.png\" alt=\"Images of Cats and Dogs\" \/><\/p>\n<p>The instant I copy these files over to the <code>Images<\/code> folder, there will be an intentional one second delay, followed by a refresh of explorer showing all the images already classified:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-ClassifiedImages.png\" alt=\"Images of Cats and Dogs\" \/><\/p>\n<p>Notice that some files were not moved, and that is due to our logic that moves them only if the <code>WinML<\/code> model classified them with a 90% confidence. But how do we know how these files were classified? The answer is the Windows Events.<\/p>\n<h2>Event Viewer<\/h2>\n<p>The Windows <code>Event Viewer<\/code> is a tool that helps you read the Windows Logs. Just search on Windows&#8217; start menu for <code>Event Viewer<\/code>, and the Windows search will show find it. There is also a neat shortcut that I often use: <code>Windows Key<\/code>+<code>X<\/code> then <code>V<\/code>.<\/p>\n<p>When you open the <code>Event Viewer<\/code> you will see a tree view on the left side, that categorizes the events. Remember when we configured our <code>LogName<\/code>, during our process initialization? This is where the log will be, inside the <code>Applications and Services Logs<\/code>:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-EventViewer.png\" alt=\"Event Viewer\" \/><\/p>\n<p>Selecting our <code>Image Classifier Service<\/code> will show all the events that your service logged under that category, with the Level, Date\/Time, and much more:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-content\/uploads\/sites\/61\/2021\/02\/Net5WinService-ImageClassifierLogs.png\" alt=\"Image Classifier Logs\" \/><\/p>\n<p>You can quickly see that the <code>Cat2.jpg<\/code> file was not moved around since its highest ranked classification was at only about 44% (0.44842827) of confidence, as a &#8220;tabby, tabby cat&#8221;. Since our default confidence is 90%, the file wasn&#8217;t moved. You can easily play around with this service, for example, by dragging images from the browser to that folder to see the logs of how they were classified. Just remember to refresh by pressing F5.<\/p>\n<h2>Conclusion<\/h2>\n<p>As we were able to show with our fun sample, <code>Windows Services<\/code> are not extremely complex, if used properly. They allow developers to implement complex scenario with a simple lifecycle management, and <code>C#\/.NET5<\/code> is a great technology to create them.<\/p>\n<p>Don&#8217;t forget to check out the <a href=\"https:\/\/github.com\/azchohfi\/ImageClassifierSample\/tree\/net5winservice\">full sample source here<\/a>.<\/p>\n<p>I hope you enjoyed this 3-part series. You can read the other 2 blog posts here:<\/p>\n<ol>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/pax-windows\/using-winml-in-net5\/\">Using WinML in .NET5<\/a><\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/pax-windows\/command-line-parser-on-net5\/\">Command Line Parser on .NET5<\/a><\/li>\n<\/ol>\n<p>See you next time!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Lets create a simple app with C#\/NET5 and see how we can register it as a Windows Service, logging information into the Windows Event Viewer, and moving some files around based on the outputs of a Machine Learning Model.<\/p>\n","protected":false},"author":40006,"featured_media":208,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[10,18,30,8,29],"class_list":["post-256","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ifdef-windows","tag-desktop","tag-net5","tag-service","tag-windows","tag-windowsservice"],"acf":[],"blog_post_summary":"<p>Lets create a simple app with C#\/NET5 and see how we can register it as a Windows Service, logging information into the Windows Event Viewer, and moving some files around based on the outputs of a Machine Learning Model.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/posts\/256","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/users\/40006"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/comments?post=256"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/posts\/256\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/media\/208"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/media?parent=256"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/categories?post=256"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ifdef-windows\/wp-json\/wp\/v2\/tags?post=256"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}