参考资料
https://docs.microsoft.com/zh-cn/dotnet/core/extensions/generic-host
本篇博客会介绍如何把一个控制台应用做成跨平台的服务,并介绍如何在该服务中读取配置文件。
故事的起因
今天早上在关注的曾经满满干货,现在却充满了培训班网课广告和理财割韭菜广告的若干个.NET Core技术公众号中翻出了一篇文章《把 Console 部署成 Windows 服务,四种方式总有一款适合你!》,这篇文章来自一个不错的公众号“一线码农聊技术”。文章中第三种方式“使用微软新内置的 Hosting”让我感觉非常清真,这种方式也没有用第三方库,完全依靠框架解决。
最近公司有一个项目,需要从数据中台中拉取数据到我们的数据库中。我同事负责这个项目。他采用的方式是用一个控制台应用,把拉取数据的方法在Program类的Main方法中调用。我隐隐感觉这种方式不太清真。当时我也不了解什么是Windows服务,什么是Linux服务,所以也没有帮他想出更清真的方式。今天看完上面这篇文章之后,我认为可以把这个控制台做成一个系统服务来实现这个功能。
但由于我就职的公司是一家技术比较朴素的外包公司,公司大概率是不会给我们时间来重构项目。但既然我了解到了实现这个功能更加清真的方式,我就要尝试一下。而且我喜欢参照实际开发中的障碍来学习。比如上面我提到的这个项目,公司给我同事的测试环境是Windows Server,但正式环境是CentOS。虽然不知道是怎样想的,但这意味着如果后面我有机会在公司项目中用上这项技术,我无法完全参考上面那篇文章的内容,而是需要把这个服务做成一个跨平台服务。
Host泛型主机
上面提到的这篇文章给我的提示就是Hosting。因为VS太好用了,我在今年上半年开始学习开发以来,一直依赖VS2019自动生成的项目模板,所以几乎没有了解过Program类中的Main方法和Host这些东西,我完全不清楚它们是什么。看完上面这篇文章,我开始在微软的文档中搜索Host,发现了这篇文章:https://docs.microsoft.com/zh-cn/dotnet/core/extensions/generic-host
可能是因为文章内容升级为了与.NET 5相匹配的内容,所以文章暂时没有汉化,也没有机翻。我们可以强行看英文。
Generic Host在其它的文档中有时被译为泛型主机,有时被译为通用主机。Generic Host可以被用于其他类型的.NET应用,例如控制台应用。一个host是封装了应用程序资源的对象。例如:
- Dependency injection (DI)
- Logging
- Configuration
- IHostedService implementations
看完这一部分,我了解到原来依赖注入,日志,配置,Service这些东西,都是创建一个host的时候来完成的。之前一直听说的“ASP.NET Core Web应用也是一个控制台应用”,今天才仿佛有点理解这句话。
准备host与配置文件
创建一个控制台应用,命名为ServiceDemo。
安装这两个NuGet包:
由于我还打算在这个跨平台服务中使用配置框架来读取配置,所以我在项目中添加两个json文件:
appsettings.json的内容为:
{
"MyConfiguration": {
"Key1": "Value1",
"Key2": "Value2"
}
}
appsettings.Development.json的内容为:
{
"MyConfiguration": {
"Key1": "Dev Value1",
"Key2": "Dev Value2"
}
}
我们准备一个模型来承载我们的配置。新建一个MyConfiguration类:
可以看到我们把两个属性的set访问器都置为private,不允许后续再手动给它们赋值。同时给两个属性都赋了默认值。
然后到Program类中配置我们的host:
可以看到我们在下面定义了一个CreateHostBuilder方法,就跟ASP.NET Web应用自己的模板一样。当然这个方法也可以不叫这个名字,记得在Main方法中调用这个方法即可。
这个方法的返回类型是IHostBuilder。首先它调用Host类的方法CreateDefaultBuilder,创建并且配置了一个HostBuilder对象。这个方法具体做了什么,可以参考:https://docs.microsoft.com/zh-cn/dotnet/core/extensions/generic-host#default-builder-settings
然后我们紧接着调用ConfigureAppConfiguration方法,来为我们这个应用进行配置。这个方法的参数是一个Action委托,委托的两个参数分别是HostBuilderContext,也就是HostBuilder上下文,还有IConfigurationBuilder:
我们在这个方法中先清空了所有的配置文件源(非必须,视情况而定),然后添加了我们的两个配置文件。配置文件均配置为可选,并且支持热重载。
ASP.NET Core 已经帮我们加载appsettings.json和appsettings.{Environment}.json这些配置文件了。但有时候我们可能想自己提供配置文件,比如叫emailsettings.json和emailsettings.{Environment}.json,就可以用上面代码中这种方式。
同时我们看到IHostBuilder还有一个ConfigureService方法,它的作用就跟Startup的ConfigureService是一样的,一会我们会用到它来注册我们的服务。
然后继续看Program类,它的Main方法调用我们下面定义的CreateHostBuilder方法,返回值是一个HostBuilder。紧接着调用IHostBuilder的Build方法,创建了一个IHost实例。然后调用Run或者RunAsync方法,运行起这个host实例。
创建服务
我们创建一个服务类,取名MyService,继承抽象类BackgroundService,必须实现抽象类的抽象方法ExecuteAsync。BackgroundService这个抽象类实现了IHostedService接口,我们顺手实现IHostedService接口的StartAsync和StopAsync方法,当然也可以不实现。
看一下BackgroundService:
目前我们的服务:
我们实现一个构造函数,通过构造函数注入IConfiguration,获取到我们的配置:
我们先实现一下StartAsync和StopAsync:
这样在这个服务运行前和停止后,都会输出这两条语句。
然后实现ExecuteAsync方法:
内容解释已经在注释里了。
现在当然还不能运行,需要注册一下这个服务,注册为HostedService。BackgroundService就是实现的IHostedService接口。
注册服务
如何注册这个服务?如果是Web应用,肯定去Startup中的ConfigureServices方法中注册。现在我们虽然没有Startup,但前面提到过了,可以直接调用ConfigureServices方法。回到Program类中:
现在直接运行项目,就会发现这个服务可以被直接执行了。
运行项目
直接运行的话,会发现输出的Key1和Key2是我们的默认值V1和V2,因为我们编译完之后的文件里并没有我们自己的appsettings.json文件。
需要右键appsettings.json和appsettings.Development.json,选择属性,把“复制到输出目录”这一项选为“始终复制”:
这样再编译时,这两个文件就会被复制到生成目录中。此时再运行:
结果还是不太对,因为在注册配置文件时,也就是AddJsonFile时,后添加的应该覆盖前面添加的:
也就是说我们先添加的appsettings.json,又添加的appsettings.Development.json,但读取到的却是先添加的appsettings.json中的值。
如何解决?右键项目属性,添加一下环境变量:
这时候会有一个launchSettings文件,在Properties目录中:
我们的环境就是Development了,此时再运行:
验证跨平台性
为了验证服务的跨平台性,我们可以使用windwos的linux子系统,又叫WSL。我已经提前安装好了。进入子系统,cd到项目目录下。注意,本机的C盘在子系统中就是/mnt/c:
dotnet run后发现,运行是成功的。说明该服务在Linux内核的系统中也是可以运行的。
总结
在实现这个Demo时,我也遇到了一些阻力。首先是.NET 5准备发布以后,很多文档都针对.NET 5的新特性进行了重写或者修改,基本都是纯英文,还没有汉化。虽然汉化之后读起来也是味同嚼蜡。其次,文档点到即止,对于高手来说是点到了,对于新手来说还完全没有点到。但跟其它语言和框架相比,.NET的文档已经相当完善了。感谢为这些付出的人们,感谢为社区付出的开发者们,感谢微软。
目前我对.NET国内社区的认识是:.NET Core的学习资料,无论免费的还是付费的,从0基础到入门的有很多,从进阶到高手的也有很多,但从入门到进阶的基本没有。我推断从入门到进阶这个过程是要在工作中慢慢渡过的。希望国内的公司也都能认认真真对待技术。