小伙伴们大家好,今儿我将和大家分享一下如何基于.net core 3.0编写windows service,以及介绍一些例如读取配置,记录日志文件,依赖注入等技术细节,在文章的最后还会介绍windows service的安装过程。具体可参照示例源码
windows服务的过去与现在
相信很多小伙伴都使用.net写过windows服务,在.NET framework时代,创建一个windows服务并不是什么难事儿,但调试起来的痛苦却是记忆犹新。后来有了Topshelf框架,很多问题都得到了好转。
后来进入.net core 2.x时代,编写windows服务又变的繁琐了起来,幸运的是,Topshelf后来也支持Core了,使得我们可以借助Console Application轻松构建服务,一切又美好了起来。
在.net core 3.0时代,我们迎来了今天的主角,worker service
通过它我们可以轻松的构建跨平台的服务,无论是windows平台的windows服务,还是Linux平台的Systemd进程。
废话不多说,我们开始吧
创建服务
- 打开vs2019
- 创建新项目
- 选择Woker Service,点击下一步
- 起一个项目名,然后点击创建
创建的完的目录结构如下
为了让worker service能够作为windows 服务运行,我们还需要安装一下NuGet包:Microsoft.Extensions.Hosting.WindowsServices,接下来我们修改一下Program.cs文件,将UseWindowsService()扩展方法添加到CreateHostBuilder中。
UseWindowsService主要做了三件事:
- 设置lifetime为WindowsServiceLifetime
- 设置content root为BaseDirectory
- 开启event log
添加Serilog
我们安装下面几个NuGet包:
- Serilog.Extensions.Hosting
- Serilog.Sinks.Console
- Serilog.Sinks.File
我们再修改一下Program.cs
public
在上面的代码段中,我们开启了两个logging sinks,console,file。在代码段的结尾,我们向HostBuilder中添加了UseSerilog,以示意host使用serilog作为logging provider。
值得注意的是,记录文件的路径我设置为BaseDirectory,实际上UseWindowsService()这个方法内部也做了同样的事,以便于程序能够在正确的位置读取到配置文件。很多小伙伴错误的使用了Directory.GetCurrentDirectory()这个函数来指明路径,但却得不到正确的结果,原因是Directory.GetCurrentDirectory()这个方法在http://Asp.net Core中是没有问题的,但在寄宿在windows service后,当前工作目录是C:WINDOWSsystem32而不是当前程序目录,这一点需要格外关注,以免产生意想不到的错误。
编写服务
其实在worker service创建好的同时,服务也已经有个预置版本,就是项目目录中的Worker.cs。并且在ConfigureServices中通过services.AddHostedService进行了注册。项目模板很贴心的给了我们指引,不至于茫然失措。
出于演示目的,我们仅简单修改Worker文件,让它实现每隔5s时间记录一次日志即可。
可以看出Worker这个类继承于BackgroundService,而BackgroundService又实现了IHostedService。这两个并非.net core3.0的产物,从2.x时代就已经存在了。BackgroundService有下面几个方法:
ExecuteAsync,必须实现,可在此编写后台服务逻辑
StartAsync,可重写,服务启动时会触发,在ExecuteAsync前执行
StopAsync,可重写,服务正常关闭时会触发,但如果异常终止,可能不会执行这个方法
Dispose,可重写,执行一些“清理”工作
其实StartAsync,StopAsync这两个方法都源自于IHostedService,它们会在应用程序启动和停止期间分别调用。感兴趣的小伙伴可以深入研究,这并非本文重点,这里不再赘述。
添加配置文件
我在appsettings.json中添加了一个简单的配置项,用于演示如何读取配置文件
{
同时创建了一个AppSettings.cs文件,用于和配置文件做映射,代码如下
public class AppSettings
{
public string WorkerName { get; set; }
}
在ConfigureServices进行绑定
下面在改造一下worker,让它通过配置文件读取到worker的名字
我们可以通过appsettings.{environment}.json来根据环境来分别配置
这里额外说两句,写过http://asp.net core web app的小伙伴都知道区别系统环境的是ASPNETCORE_ENVIRONMENT,但Generic Host(不知如何翻译比较恰当,姑且就叫这个)里,系统环境标识为DOTNET_ENVIRONMEN,希望小伙伴不要搞混,尤其是部署的时候,要多留意一下。
依赖注入
为了演示这块内容,我创建了IService和ServiceA。
public interface IService
{
void Run();
}
public class ServiceA:IService
{
private readonly ILogger<ServiceA> _logger;
public ServiceA(ILogger<ServiceA>logger)
{
_logger = logger;
}
public void Run()
{
_logger.LogInformation("Service A is running");
}
}
接着我们注册一下
这里需要注意的是,我注册的是单例,至于为什么,后面会提到。
然后改造一下Worker
我们运行一下看看
完美!可是,如果这么简单,就不用单独来讲了,我们换个lifetime注册试试,比如注册为Scoped
嘿嘿,异常如期而知,我们看一下
最关键的一句是,cannot consume scoped service from singleton。为什么会这样?原因是Worker生命周期是singleton,也就是单例,我们注册ServiceA的lifetime是Scoped,这就会导致回收机制在Worker之前就会将其回收,引发了DI容器的错误。
但现实场景下我们的确需要比较短周期的service(short-lived services),比如数据库连接,http client等等,这种情况怎么办呢?答案是使用IServiceProvider。下面我再改造一下Worker。
一切又正确运行了
安装windows服务
一切就绪,我们先发布一下。
下面我们通过windows提供的sc进行安装,注意需要以管理员身份运行
sc create DemoService DisplayName="_Demo Service" binPath="D:MyProjectWorkerServiceDemoWorkerServiceDemobinReleasenetcoreapp3.1publishWorkerServiceDemo.exe"
启动一下试试,一切正常。
如果想删除服务,我们也可以借助sc的delete来实现
至此,.net core 3.0创建windows服务已经完成,写了一下午,终于写完了 。小伙伴们我们下篇文章见~