1, Introduction
Recently, a project will use windows services as the carrier of a software project. I thought about it. It has come to the era of cross platform. Will there be a technology to replace Windows services? So, after a crazy search on the Internet, the real emperor paid off his efforts and found an answer, that is, Worker Service. It is said that in the era of NET Core 3.0, a new project template of Worker Service has been added, which can write long-running background services and can be easily deployed as windows services or linux daemons. If vs2019 is installed in Chinese, the project name of the Worker Service becomes the auxiliary role service. I didn't study too deeply, but showed my own use process. This article will demonstrate how to create a Worker Service project and deploy it to run as a Windows service or linux daemon; No more nonsense, let's start.
II. Start actual combat
1. Start creating worker service project
1.1. Create a new project - select Worker Service
1.2. Give the project a name: DemoWorkerService
After the project is created successfully, you will see that two classes are created: program CS , and worker cs.
1.3,Program. Type of CS program entry.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace DemoWorkerService
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
}
Program class and ASP Net core web applications are very similar, except that there is no startup class, and the worker service is added to the IOC container.
1.4,Worker.cs the specific type of work to undertake the task.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DemoWorkerService
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
}
Worker.cs is just a simple class that inherits from the BackgroundService, which in turn implements the IHostedService interface.
2. Dependency injection (DI)
In the ConfigureServices method of the Program class, we can configure the dependent types used by the constructor of the Worker class, which is called "constructor injection". If we now have IContainer interface and MyContainer class, they are a group of types:
public interface IContainer
{
string ContainerName
{
get;
set;
}
}
public class MyContainer : IContainer
{
protected string containerName = "Custom my container";
public string ContainerName
{
get
{
return containerName;
}
set
{
containerName = value;
}
}
}
2.1. In the ConfigureServices method of the Program class, we can use the services parameter (this parameter is the object of the IServiceCollection interface, which is the IOC container) to configure the correspondence between the IContainer interface and the MyContainer class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DemoWorkerService.Model;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace DemoWorkerService
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
//Configure the dependency injection relationship between IContainer interface and MyContainer class
services.AddSingleton<IContainer,MyContainer>();
services.AddHostedService<Worker>();
});
}
}
2.2. Then, in the constructor of the Worker class, use its type through constructor injection. The Worker Service will automatically inject IContainer type parameters using DI (dependency injection):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DemoWorkerService.Model;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DemoWorkerService
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IContainer _container;
//Worker Service Automatic dependency injection Worker Constructor IContainer container parameter
public Worker(ILogger<Worker> logger, IContainer container)
{
_logger = logger;
_container = container;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
}
·2.3. Note that the constructor of the Worker class above uses NET Core's own log component interface object ILogger, which is also injected into the Worker type through the constructor, and ASP The constructor injection of Controller in NET Core is similar (about ILogger, See here for more information ), we can also define a parameterless default constructor for the Worker class without using the log component.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
namespace DemoWorkerService
{
public class Worker : BackgroundService
{
public Worker()
{
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000, stoppingToken);
}
}
}
}
3. Rewrite the StartAsync, ExecuteAsync and StopAsync methods of BackgroundService class
We can use the override ExecuteAsync method to accomplish what we want to do. This method actually belongs to the BackgroundService class. We just override it in the Worker class. This method is the logic we need to deal with. In principle, this logic should be in an dead loop, and whether the loop should be ended is judged through the canceltoken parameter object passed in by the ExecuteAsync method. For example, if the Windows service is stopped, the IsCancellationRequested property of the canceltoken class in the parameter will return true, so we should stop the loop in the ExecuteAsync method to end the execution of the whole service process:
//rewrite BackgroundService.ExecuteAsync Method, encapsulation windows Service or linux Processing logic in Daemons
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//If the service is stopped, the following IsCancellationRequested Will return true,We should end the cycle
while (!stoppingToken.IsCancellationRequested)
{
//Simulate the processing logic in the service. Here we only output one log and wait for 1 second
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
In addition, we can override BackgroundService. In the worker class StartAsync() method and BackgroundService StopAsync () method (pay attention to rewrite, don't forget to call base.StartAsync() and base. in Worker class. Stopasync() method, because the StartAsync() method and StopAsyn() method of BackgroundService class will execute some core code of Worker Service). When starting and ending the Worker Service (for example, starting and stopping the windows service), execute some processing logic. In this example, we output a log respectively:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DemoWorkerService
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
//rewrite BackgroundService.StartAsync Method, execute some processing logic when starting the service. Here we only output a log
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker starting at: {time}", DateTimeOffset.Now);
await base.StartAsync(cancellationToken);
}
//rewrite BackgroundService.ExecuteAsync Method, encapsulation windows Service or linux Processing logic in Daemons
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//If the service is stopped, the following IsCancellationRequested Will return true,We should end the cycle
while (!stoppingToken.IsCancellationRequested)
{
//Simulate the processing logic in the service. Here, we only output one log and wait for 1 second
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
//rewrite BackgroundService.StopAsync Method to execute some processing logic at the end of the service. Here we only output a log
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker stopping at: {time}", DateTimeOffset.Now);
await base.StopAsync(cancellationToken);
}
}
}
Since the StartAsync(), ExecuteAsync(), StopAsync() methods of the BackgroundService class all return Task types, as shown in the above code, we can use async and await keywords to rewrite them as asynchronous functions to improve the usability of the program. Now we run the above code directly in Visual Studio. The results are as follows. The running time is printed every 1 second:
Among them, I use three red boxes to identify the output results of overriding StartAsync(), ExecuteAsync(), StopAsync() methods in the Worker class. When running the Worker Service project in Visual Studio, You can use the shortcut "Ctrl+C" in the startup console to stop the operation of Worker Service (equivalent to stopping windows service or linux daemon), so we can see from the above results that StartAsync, ExecuteAsync and StopAsync are executed, and logs are output.
In fact, we can see that the Worker Service project is essentially a console project, but when it is deployed as a windows service or linux daemon, the console window will not be displayed.
So in fact, when debugging in Visual Studio, you can use console Console methods such as writeline () replace the log output method of ILogger interface:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DemoWorkerService
{
public class Worker : BackgroundService
{
public Worker()
{
}
//rewrite BackgroundService.StartAsync Method, execute some processing logic when starting the service. Here we only output a log
public override async Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Worker starting at: {0}", DateTimeOffset.Now);
await base.StartAsync(cancellationToken);
}
//rewrite BackgroundService.ExecuteAsync Method, encapsulation windows Service or linux Processing logic in Daemons
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//If the service is stopped, the following IsCancellationRequested Will return true,We should end the cycle
while (!stoppingToken.IsCancellationRequested)
{
//Simulate the processing logic in the service. Here we only output one log and wait for 1 second
Console.WriteLine("Worker running at: {0}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
//rewrite BackgroundService.StopAsync Method to execute some processing logic at the end of the service. Here we only output a log
public override async Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Worker stopping at: {0}", DateTimeOffset.Now);
await base.StopAsync(cancellationToken);
}
}
}
Its effect is similar to the log output method of ILogger interface:
However, because the log output method of ILogger interface can also output information to the console, I prefer to use ILogger interface to output debugging information. After all, it is more suitable for logging.
4. Do not let the thread block the overridden StartAsync, ExecuteAsync and StopAsync methods in the worker class
Note: don't let your code block the overridden StartAsync(), ExecuteAsync(), StopAsync() methods in the Worker class.
Because the StartAsync() method is responsible for starting the Worker Service, if the thread calling the StartAsync method is blocked all the time, the start of the Worker Service cannot be completed all the time.
Similarly, the StopAsync() method is responsible for ending the Worker Service. If the thread calling the StopAsync method is blocked all the time, the end of the Worker Service cannot be completed all the time.
Here we mainly explain why the ExecuteAsync method cannot be blocked. We try to change the ExecuteAsync method in this example to the following code:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
Thread.Sleep(1000);//use Thread.Sleep Wait for synchronization, call ExecuteAsync The thread of the method will always execute the loop here and will be blocked all the time
}
await Task.CompletedTask;
}
We will use the asynchronous wait method task in the ExecuteAsync() method Delay, change to synchronous wait method thread Sleep (about the difference between Thread.Sleep and Task.Delay, Please check here ). Due to thread The sleep method waits for the execution thread by blocking, so now the thread calling the ExecuteAsync method will always execute the loop in the ExecuteAsync method and be blocked. Unless the loop in the ExecuteAsync method ends, the thread calling the ExecuteAsync method will always be stuck in the ExecuteAsync method. Now we run the Worker Service in Visual Studio, and the execution results are as follows:
We can see that when we use the shortcut "Ctrl+C" in the console to try to stop the Worker Service (the log output in the red box above), the loop in the ExecuteAsync method still runs continuously to output the log, which indicates that the IsCancellationRequested property of the canceltoken parameter of the ExecuteAsync method still returns false, So this is the problem. If we directly use the thread calling the ExecuteAsync method to cycle to execute the processing logic of the windows service or linux daemon, the Worker Service will not be stopped normally because the CancellationToken parameter of the ExecuteAsync method has not been updated.
Therefore, the processing logic of windows services or linux daemons that are time-consuming and need cyclic processing should be executed in another thread, not by the thread calling the ExecuteAsync method.
So suppose we have three windows services or linux daemons whose logic needs to be processed. We can put them into three new threads for execution, as shown in the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DemoWorkerService
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
//rewrite BackgroundService.StartAsync Method, execute some processing logic when starting the service. Here we only output a log
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker starting at: {time}", DateTimeOffset.Now);
await base.StartAsync(cancellationToken);
}
//first windows Service or linux The processing logic of the daemon is provided by RunTaskOne Method started internally Task Task threads can also be processed from parameters CancellationToken stoppingToken Medium IsCancellationRequested Properties, knowing Worker Service Has the service been stopped
protected Task RunTaskOne(CancellationToken stoppingToken)
{
return Task.Run(() =>
{
//If the service is stopped, the following IsCancellationRequested Will return true,We should end the cycle
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("RunTaskOne running at: {time}", DateTimeOffset.Now);
Thread.Sleep(1000);
}
}, stoppingToken);
}
//the second windows Service or linux The processing logic of the daemon is provided by RunTaskTwo Method started internally Task Task threads can also be processed from parameters CancellationToken stoppingToken Medium IsCancellationRequested Properties, knowing Worker Service Has the service been stopped
protected Task RunTaskTwo(CancellationToken stoppingToken)
{
return Task.Run(() =>
{
//If the service is stopped, the following IsCancellationRequested Will return true,We should end the cycle
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("RunTaskTwo running at: {time}", DateTimeOffset.Now);
Thread.Sleep(1000);
}
}, stoppingToken);
}
//Third windows Service or linux The processing logic of the daemon is provided by RunTaskThree Method started internally Task Task threads can also be processed from parameters CancellationToken stoppingToken Medium IsCancellationRequested Properties, knowing Worker Service Has the service been stopped
protected Task RunTaskThree(CancellationToken stoppingToken)
{
return Task.Run(() =>
{
//If the service is stopped, the following IsCancellationRequested Will return true,We should end the cycle
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("RunTaskThree running at: {time}", DateTimeOffset.Now);
Thread.Sleep(1000);
}
}, stoppingToken);
}
//rewrite BackgroundService.ExecuteAsync Method, encapsulation windows Service or linux Processing logic in Daemons
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
Task taskOne = RunTaskOne(stoppingToken);
Task taskTwo = RunTaskTwo(stoppingToken);
Task taskThree = RunTaskThree(stoppingToken);
await Task.WhenAll(taskOne, taskTwo, taskThree);//use await Keywords, asynchronous wait RunTaskOne,RunTaskTwo,RunTaskThree Method returns three Task Object is completed, so call ExecuteAsync The thread of the method will return immediately and will not be blocked here
}
catch (Exception ex)
{
//RunTaskOne,RunTaskTwo,RunTaskThree Method, the processing logic after exception capture. Here, we only output a log
_logger.LogError(ex.Message);
}
finally
{
//Worker Service After the service is stopped, if there is any logic that needs to be closed, it can be written here
}
}
//rewrite BackgroundService.StopAsync Method to execute some processing logic at the end of the service. Here we only output a log
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker stopping at: {time}", DateTimeOffset.Now);
await base.StopAsync(cancellationToken);
}
}
}
So now the thread calling ExecuteAsync method will not be blocked. Run Worker Service in Visual Studio, and the execution results are as follows:
You can see that this time, when we use the shortcut "Ctrl+C" in the console to try to stop the Worker Service (the log output in the red box in the above figure), the ExecuteAsync method immediately stops running. Therefore, it is emphasized again that never block the thread calling the ExecuteAsync method!
In addition, in the above code, we put a finally code block in the ExecuteAsync method rewritten by the worker class. This code block can be used to execute some closing code logic (such as closing database connection, releasing resources, etc.) after the Worker Service service is stopped (such as stopping windows service or linux daemon), I prefer to use the finally code block in the ExecuteAsync method to finish the work of the Worker Service, rather than in the StopAsync method overridden by the worker class (from BackgroundService source code , we can see that the StopAsync method of the worker class may be completed before the ExecuteAsync method, so the closing work of the Worker Service should be placed in the finally code block in the ExecuteAsync method, because the finally code block in the ExecuteAsync method must be executed after the three Task objects returned by the RunTaskOne, RunTaskTwo and RunTaskThree methods are executed.
5. Run multiple Worker classes in the Worker Service
In the previous example, we can see that we have defined three methods RunTaskOne, RunTaskTwo and RunTaskThree in a Worker class to execute the logic of three windows services or linux daemons.
In fact, we can also define and execute multiple Worker classes in a Worker Service project, instead of putting all the code logic in one Worker class.
First, we define the first Worker class WorkerOne:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DemoWorkerService
{
public class WorkerOne : BackgroundService
{
private readonly ILogger<Worker> _logger;
public WorkerOne(ILogger<Worker> logger)
{
_logger = logger;
}
//rewrite BackgroundService.StartAsync Method, execute some processing logic when starting the service. Here we only output a log
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("WorkerOne starting at: {time}", DateTimeOffset.Now);
await base.StartAsync(cancellationToken);
}
//rewrite BackgroundService.ExecuteAsync Method, encapsulation windows Service or linux Processing logic in Daemons
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//If the service is stopped, the following IsCancellationRequested Will return true,We should end the cycle
while (!stoppingToken.IsCancellationRequested)
{
//Simulate the processing logic in the service. Here we only output one log and wait for 1 second
_logger.LogInformation("WorkerOne running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
//rewrite BackgroundService.StopAsync Method to execute some processing logic at the end of the service. Here we only output a log
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("WorkerOne stopping at: {time}", DateTimeOffset.Now);
await base.StopAsync(cancellationToken);
}
}
}
Next, we define the second Worker class, WorkerTwo:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DemoWorkerService
{
public class WorkerTwo : BackgroundService
{
private readonly ILogger<WorkerTwo> _logger;
public WorkerTwo(ILogger<WorkerTwo> logger)
{
_logger = logger;
}
//rewrite BackgroundService.StartAsync Method, execute some processing logic when starting the service. Here we only output a log
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("WorkerTwo starting at: {time}", DateTimeOffset.Now);
await base.StartAsync(cancellationToken);
}
//rewrite BackgroundService.ExecuteAsync Method, encapsulation windows Service or linux Processing logic in Daemons
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//If the service is stopped, the following IsCancellationRequested Will return true,We should end the cycle
while (!stoppingToken.IsCancellationRequested)
{
//Simulate the processing logic in the service. Here we only output one log and wait for 1 second
_logger.LogInformation("WorkerTwo running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
//rewrite BackgroundService.StopAsync Method to execute some processing logic at the end of the service. Here we only output a log
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("WorkerTwo stopping at: {time}", DateTimeOffset.Now);
await base.StopAsync(cancellationToken);
}
}
}
Then, in the Program class, we add WorkerOne and WorkerTwo services to DI container:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace DemoWorkerService
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<WorkerOne>();
services.AddHostedService<WorkerTwo>();
});
}
}
Then run the Worker Service in Visual Studio, and the execution results are as follows:
After using the shortcut key "Ctrl+C" in the console to stop the operation of Worker Service, I use three red boxes to identify the output results of overriding StartAsync, ExecuteAsync and StopAsync methods in WorkerOne and WorkerTwo classes. You can see that WorkerOne and WorkerTwo classes have been executed and log information has been output.
6. Deploy to run as a Windows Service
6.1. Add nuget package to the project: Microsoft.Extensions.Hosting.WindowsServices
6.2. Then in the program CS internally, add UseWindowsService() to CreateHostBuilder
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService();
{
services.AddHostedService<Worker>();
});
Note that calling the UseWindowsService method on a non Windows platform will not report an error, and the non Windows platform will ignore this call.
6.3. Execute the following command to release the project
dotnet publish -c Release -o C:\WorkerPub
Execute in CMD:
Of course, you can also use the project's own Publishing Wizard in Visual Studio to publish the Worker Service project to the folder "C:\WorkerPub":
By default, the Worker Service project will be published as an exe file:
6.4. Then use the sc.exe tool to manage the service. Enter the command to create a windows service. Here we will demoworkerservice Exe to create a windows Service named NETCoreDemoWorkerService:
sc.exe create NETCoreDemoWorkerService binPath=C:\WorkerPub\DemoWorkerService.exe
Execute in CMD. Note that you should start CMD in Run as administrator mode:
To view the service status, use the following command:
sc.exe query NETCoreDemoWorkerService
Run as administrator in CMD:
Start command:
sc.exe start NETCoreDemoWorkerService
Run as administrator in CMD:
Viewing from the windows service list, NETCoreDemoWorkerService has been successfully installed:
Deactivate and delete commands:
sc.exe stop NETCoreDemoWorkerService sc.exe delete NETCoreDemoWorkerService
Run as administrator in CMD:
7. Deploy and run as a Linux daemon
It is also convenient to deploy linux daemons. You can perform the following two steps:
Add Microsoft.Extensions.Hosting.Systemd NuGet package into the project and tell your new Worker that its life cycle is managed by systemd!
Add UseSystemd() to the host builder
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSystemd();
{
services.AddHostedService<Worker>();
});
Similarly, calling the UseSystemd method on the Windows platform will not report an error, and the Windows platform will ignore this call.
3, Summary
Personally, I feel that Worker Service is much easier to use than Windows service. It is convenient to install, configure and uninstall. If you have used Windows service, you will know that there are still many processes, and you have to add an installer, etc. Now with this project, it will be much easier to develop background services in the future. Well, that's all. Keep trying.