C# 让后台服务变得更加容易。

作者:微信公众号:【架构师老卢】
3-12 15:48
22

概述:几个月前,我写了一篇关于ASP.net 后台服务的文章。几天前,我把它展示给一位同事,他发现我的解决方案存在一些问题,这让我感到惊讶:我从控制器而不是业务层将工作发送到后台服务。我的解决方案不容易进行单元测试。我的解决方案不是通用的。所以我回到了绘图板,想出了更好的东西。更通用的解决方案:第一步是找到一种方法来拥有可用于调用多个服务的设置对象。public class BackgroundTaskSettings {     public BackgroundTaskSettings(string name, JObject settings)     {         Name =

几个月前,我写了一篇关于ASP.net 后台服务的文章。几天前,我把它展示给一位同事,他发现我的解决方案存在一些问题,这让我感到惊讶:

  1. 我从控制器而不是业务层将工作发送到后台服务。
  2. 我的解决方案不容易进行单元测试。
  3. 我的解决方案不是通用的。

所以我回到了绘图板,想出了更好的东西。

更通用的解决方案:

第一步是找到一种方法来拥有可用于调用多个服务的设置对象。

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;  
}

现在,后台服务是通用的,我已将其导出到可以使用的 Nuget 中。

相关留言评论
昵称:
邮箱:
阅读排行