几个月前,我写了一篇关于 ASP.net 后台服务的文章。几天前,我向一位同事展示了它,他发现我的解决方案存在一些问题,这让我感到惊讶:
所以我回到了绘图板,想出了更好的东西。
更通用的解决方案:
第一步是找到一种方法,让设置对象可用于调用多个服务。
public class BackgroundTaskSettings
{
public BackgroundTaskSettings(string name, JObject settings)
{
Name = name;
Settings = settings;
}
public string Name { get; set; }
public JObject Settings { get; set; }
}
我选择使用 JObject 传递设置,而不是使用通用 T,因为在 DI 中注册事物更容易,而不会增加很多复杂性。
还有一个容器,它将包含我们需要执行的任务:
public class TasksToRun
{
private readonly ConcurrentQueue<BackgroundTaskSettings> _tasks = new();
public TasksToRun() => _tasks =
new ConcurrentQueue<BackgroundTaskSettings>();
public void Enqueue(BackgroundTaskSettings settings) =>
_tasks.Enqueue(settings);
public void Enqueue(List<BackgroundTaskSettings> settingsList)
{
foreach (var settings in settingsList)
{
_tasks.Enqueue(settings);
}
}
public BackgroundTaskSettings? Dequeue()
{
var hasTasks = _tasks.TryDequeue(out var settings);
return hasTasks ? settings : null;
}
public bool IsEmpty => _tasks.IsEmpty;
}
下一步是为所有将执行所有工作的具体服务创建一个接口,以便后台服务可以使用它们
public interface IBackgroundServiceable
{
string Name();
Task Execute(BackgroundTaskSettings settings);
}
最后是后台服务本身:
public class GenericBackgroundTaskService : Microsoft.Extensions.Hosting.BackgroundService
{
private readonly List<IBackgroundServiceable> _backgroundServiceableList;
private readonly TasksToRun _tasks;
//TODO - get number of seconds from config
private readonly TimeSpan _timeSpan = TimeSpan.FromSeconds(1);
public GenericBackgroundTaskService(TasksToRun tasks,
IEnumerable<IBackgroundServiceable> backgroundServiceableList)
{
_tasks = tasks;
_backgroundServiceableList = backgroundServiceableList.ToList();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using PeriodicTimer timer = new(_timeSpan);
while (!stoppingToken.IsCancellationRequested &&
await timer.WaitForNextTickAsync(stoppingToken))
{
await ExecuteTasksFromQueue();
}
}
//for unitTests, as we cannot run the protected ExecuteAsync method directly
public async Task ExecuteTasksFromQueue()
{
while (!_tasks.IsEmpty)
{
var taskToRun = _tasks.Dequeue();
if (taskToRun == null) return;
//call the relevant service
var serviceToRun = _backgroundServiceableList
.FirstOrDefault(b => b.Name() == taskToRun.Name);
if (serviceToRun != null)
{
await serviceToRun.Execute(taskToRun);
}
}
}
}
GenericBackgroundTaskService 类似于我在上一篇文章中的内容,但这里它获取了 IBackgroundServiceable 的实现列表(大多数 DI 可以做到这一点,它可能非常有用)。
每隔几秒钟,我们就会检查队列中的所有任务,而不是像在上一个解决方案中那样调用具体的服务,而是通过查看服务名称来找到需要调用的特定服务。
DI 注册
最后,我创建了一些扩展方法来注册 DI 中的所有内容:
public static class ServicesCollectionExtensions
{
public static void RegisterBackgroundService(
this IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<TasksToRun, TasksToRun>();
serviceCollection.AddHostedService<GenericBackgroundTaskService>();
}
public static void RegisterBackgroundServiceable(
this IServiceCollection serviceCollection)
{
//find all implementations of this interface
var (attributeValidatorType, concreteTypes) = FindImplementations();
foreach (var type in concreteTypes)
{
serviceCollection.AddScoped(attributeValidatorType, type);
}
}
public static (Type attributeValidatorType, IEnumerable<Type> concreteTypes)
FindImplementations()
{
var attributeValidatorType = typeof(IBackgroundServiceable);
var concreteTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p =>
attributeValidatorType.IsAssignableFrom(p) && !p.IsInterface);
return (attributeValidatorType, concreteTypes);
}
}
我创建了 2 种单独的方法:
一种是注册 TasksToRun 容器和 GenericBackgroundTaskService 后台服务。还有一种单独的方法来注册 IBackgroundServiceable 的所有实现,因此我们不需要手动注册它们。我已将其拆分为单独的方法,以防有人需要使用某些客户逻辑注册 IBackgroundServiceable 的实现。
我还有一种服务方法,只需找到所有实现,以帮助任何需要注册它们的人。
这一切是如何结合在一起的:
假设我们需要在后台执行 2 个操作,监控和导入。我们可以创建一个常量文件来保留服务名称,以避免任何拼写问题
public static class ServiceNames
{
public const string MonitorService = "monitoring";
public const string ImportService = "importing";
}
监视和导入服务可能如下所示:
public class MonitorService : IBackgroundServiceable
{
public string Name() => ServiceNames.MonitorService;
public async Task Execute(BackgroundTaskSettings settings)
{
var monitorSettings = settings.Settings.ToObject<MonitorSettings>();
//monitor something
....
}
}
public class ImportService : IBackgroundServiceable
{
public string Name() => ServiceNames.ImportService;
public async Task Execute(BackgroundTaskSettings settings)
{
var importSettings = settings.Settings.ToObject<ImportService>();
//import something
....
}
}
请注意,我们必须添加一行来将设置 JObject 解析为类。
让我们来看看我们的主要业务服务
public class BusinessService : IBusinessService
{
private readonly TasksToRun _tasksToRun;
public BusinessService(TasksToRun tasksToRun)
{
_tasksToRun = tasksToRun;
}
public async Task DoSomething(string val)
{
_tasksToRun.Enqueue(new BackgroundTaskSettings(
ServiceNames.MonitorService,
JObject.FromObject(new MonitorSettings
{
ApiKey = "AA-BB-CC11",
ExternalValue = val
})));
await Task.Delay(1000);
}
}
在这里,我们从 DI 获取 TasksToRun 容器,只需将任务排队即可运行。(我还使用 ServiceNames.MonitorService 来确保在一个地方定义服务名称。)
TasksToRun.Enqueue 也有一个重载,该重载采用 BackgroundTaskSettings 列表,以防我们需要做几件事_。_
单元测试:
有了这个新的和改进的解决方案,为我们的主要业务服务连接单元测试就变得非常容易了
[TestClass]
public class BusinessServiceTests
{
[TestMethod]
public async Task DoSomethingTests()
{
var tasks = new TasksToRun();
var service = new BusinessService(tasks);
var expectedVal = "testing11";
await service.DoSomething(expectedVal);
var task = tasks.Dequeue();
Assert.IsNotNull(task);
Assert.IsTrue(
HasSettingsValue(task.Settings, "ExternalValue", expectedVal));
}
}
public static bool HasSettingsValue(JObject obj, string key, string val)
{
var objVal = obj[key];
return objVal?.ToString() == val;
}