在 .NET Core 中使用 Quartz.NET 示例理解依赖注入






4.72/5 (16投票s)
本文介绍了在使用 Quartz.NET 库时,如何利用标准的 .NET Core DI 容器库执行依赖注入。此外,我们还将重点介绍其他几个有用的 .NET Core 技术。
引言
文章中引用的整个项目都包含在以下 Github 仓库中。为了更好地理解文章中的代码,您可能需要查看一下。
项目概述
让我们先看看初始的解决方案结构。
项目 QuartzDI.Demo.External.DemoService
代表我们无法控制的某个外部依赖。为了简单起见,它只做了一些不起眼的工作。
项目 QuartzDI.Demo
是我们的工作项目,其中包含一个简单的 Quartz.NET 作业。
public class DemoJob : IJob
{
private const string Url = "https://i.ua";
public static IDemoService DemoService { get; set; }
public Task Execute(IJobExecutionContext context)
{
DemoService.DoTask(Url);
return Task.CompletedTask;
}
}
这是以一种直接的方式进行设置的
var props = new NameValueCollection
{
{ "quartz.serializer.type", "binary" }
};
var factory = new StdSchedulerFactory(props);
var sched = await factory.GetScheduler();
await sched.Start();
var job = JobBuilder.Create<DemoJob>()
.WithIdentity("myJob", "group1")
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity("myTrigger", "group1")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(5)
.RepeatForever())
.Build();
await sched.ScheduleJob(job, trigger);
我们通过作业的 static
属性来提供我们的外部服务
DemoJob.DemoService = new DemoService();
由于项目是一个控制台应用程序,在文章的整个过程中,我们将不得不手动安装所有必需的基础设施,并且能够更深入地理解 .NET Core 到底为我们提供了什么。
此时,我们的项目已经运行起来了。最重要的是它非常简单,这很棒。但我们为了这种简单性付出了应用程序灵活性不足的代价,如果我们想把它作为一个小型工具使用,这也没关系。但对于生产系统来说,情况通常并非如此。所以让我们稍微调整一下,让它更灵活。
创建配置文件
其中一个不灵活的地方是我们硬编码了在 DemoJob
中调用的 URL。理想情况下,我们希望更改它,并且根据我们的环境来更改它。 .NET Core 为此提供了一个 appsettings.json 机制。
为了开始使用 .NET Core 配置机制,我们需要安装几个 Nuget 包
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.FileExtensions
Microsoft.Extensions.Configuration.Json
让我们创建一个具有该名称的文件,并将 URL 提取到其中
{
"connection": {
"Url": "http://i.ua"
}
}
现在我们可以按如下方式从配置文件中提取我们的值
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true);
var configuration = builder.Build();
var connectionSection = configuration.GetSection("connection");
DemoJob.Url = connectionSection["Url"];
请注意,为了实现这一点,我们不得不将 Url
从常量更改为属性。
public static string Url { get; set; }
使用构造函数注入
通过 static
属性注入服务对于简单的项目来说是可以的,但对于较大的项目来说,它可能存在几个缺点:例如,作业可能在未提供服务的情况下被调用而失败,或者在对象运行时更改依赖项,这使得对象更难理解。为了解决这些问题,我们应该采用构造函数注入。
虽然纯依赖注入本身没有问题,而且有些人认为 你应该努力实现它,但在这篇文章中,我们将使用内置的 .NET Core DI 容器,它带有一个 Nuget 包 Microsoft.Extensions.DependencyInjection
。
现在我们在构造函数参数中指定我们依赖的服务
private readonly IDemoService _demoService;
public DemoJob(IDemoService demoService)
{
_demoService = demoService;
}
为了调用作业的参数化构造函数,Quartz.NET 提供了 IJobFactory
接口。这是我们的实现
public class DemoJobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public DemoJobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _serviceProvider.GetService<DemoJob>();
}
public void ReturnJob(IJob job)
{
var disposable = job as IDisposable;
disposable?.Dispose();
}
}
让我们注册我们的依赖项
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<DemoJob>();
serviceCollection.AddScoped<IDemoService, DemoService>();
var serviceProvider = serviceCollection.BuildServiceProvider();
最后一步是让 Quartz.NET 使用我们的工厂。IScheduler
正好为此提供了一个 JobFactory
属性。
sched.JobFactory = new DemoJobFactory(serviceProvider);
理解服务生命周期
在上一节中,我们已将服务注册为作用域生命周期。然而,这里并没有实际的思考来解释为什么我们选择它而不是其他选项,如瞬时或单例生命周期。
让我们来研究一下其他选项。为了实现这一点,我们将向我们的类构造函数添加一些跟踪语句。
public DemoService()
{
Console.WriteLine("DemoService started");
}
以及作业构造函数
public DemoJob(IDemoService demoService, IOptions<DemoJobOptions> options)
{
_demoService = demoService;
_options = options.Value;
Console.WriteLine("Job started");
}
服务注册如下
serviceCollection.AddTransient<DemoJob>();
serviceCollection.AddTransient<IDemoService, DemoService>();
运行程序后,我们将观察到以下输出
DemoService started Job started calling http://i.ua DemoService started Job started calling http://i.ua DemoService started Job started calling http://i.ua
输出本身就说明了问题:每次调用服务时,我们都会创建一个新实例。将两个注册更改为 AddScoped
或 AddSingleton
会产生相同的结果
DemoService started Job started calling http://i.ua calling http://i.ua calling http://i.ua
这两个实例在应用程序启动时只构造一次。让我们查阅一下 文档,看看这些生命周期之间的区别,以及为什么它们对于给定的示例会产生相同的结果。
作用域生命周期服务是每个客户端请求(连接)创建一次。
这是单例的作用
单例生命周期服务在第一次请求时创建。
在我们的例子中,因为我们使用的是控制台应用程序,所以只有一个请求。这就是为什么两种服务生命周期表现相同的原因。
大多数与 DI 相关的文章都不会涵盖的最后一个主题是具有不同生命周期的服务的组合。尽管如此,有些内容值得提及。下面是注册的示例。
serviceCollection.AddSingleton<DemoJob>();
serviceCollection.AddTransient<IDemoService, DemoService>();
这意味着我们将瞬时依赖注入到单例服务中。有人可能会期望,既然我们将 IDemoService
声明为瞬时服务,那么它每次都会被构造。然而,输出却大相径庭
DemoService started Job started calling http://i.ua calling http://i.ua calling http://i.ua
所以,同样,这两个服务都在应用程序启动时构造。这里我们看到瞬时服务的生命周期被使用它的服务提升了。这导致了一个重要的应用。我们注册为瞬时的服务可能不是设计为单例使用的,因为它不是以线程安全的方式编写的,或者出于其他原因。然而,在这种情况下,它变成了单例,这可能会导致一些微妙的错误。这使我们得出结论:除非我们有充分的理由,否则我们不应该将服务注册为单例,例如管理全局状态的服务。最好将服务注册为瞬时服务。
反之,则不会有意外。
serviceCollection.AddTransient<DemoJob>();
serviceCollection.AddSingleton<IDemoService, DemoService>();
产生
DemoService started Job started calling http://i.ua Job started calling http://i.ua Job started calling http://i.ua
在这里,每次创建新的作业实例都会重用同一个单例 DemoService
。
使用 Options 模式
现在我们可以用配置选项来做同样的事情。同样,我们的常规操作从一个 Nuget 包开始。这次是 Microsoft.Extensions.Options
。
让我们为配置选项创建一个强类型定义
public class DemoJobOptions
{
public string Url { get; set; }
}
现在我们按如下方式填充它们
serviceCollection.AddOptions();
serviceCollection.Configure<DemoJobOptions>(options =>
{
options.Url = connectionSection["Url"];
});
并将它们注入构造函数。请注意,我们注入的是 IOptions<T>
,而不是直接注入 options
实例。
public DemoJob(IDemoService demoService, IOptions<DemoJobOptions> options)
{
_demoService = demoService;
_options = options.Value;
}
结论
在本文中,我们看到了如何利用 .NET Core 的功能来使我们对 Quartz.NET 的使用更加灵活。
历史
2019 年 2 月 22 日 - 初始版本
2020 年 9 月 14 日 - 添加了“理解服务生命周期”部分。