В первый раз за долгое время в практике понадобилось использовать бэкграундную таску.
Нужно было добавить в процесс логина автоприсваивание пользователям определенных доступов. Операция не обязательная и из-за нее не должен падать логин. Тем более процесс логина из-за нее тоже не хочется растягивать.
Можно было сделать fire and forget опустив слово await:
groupAccessService.AssignPredefinedSpacesAsync(existingUser, CancellationToken.None);
То есть пустить таску на самотек. С одной стороны просто и понятно, но хочется как-то понимать, дошла таска до своего финала или нет и как-то все это залогировать. Короче хочется больше контроля над таким.
Вспомнил, что когда-то с IHostedService слышал про BackgroundTaskQueue
Оказалось, что в доке прям есть кусок кода, который мне нужен. По нему решил реализовать вот такой простенький бэкграунд кью воркер
using System.Threading.Channels;
namespace Dodo.KnowledgeBase.Web.Services.BackgroundTasks;
public class BackgroundTaskQueue(IServiceScopeFactory serviceScopeFactory) : IBackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, ValueTask>> _queue =
Channel.CreateUnbounded<Func<CancellationToken, ValueTask>>();
public async ValueTask QueueAsync(Func<CancellationToken, ValueTask> workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
await _queue.Writer.WriteAsync(workItem);
}
public async ValueTask QueueAsync(Func<IServiceProvider, CancellationToken, ValueTask> workItem)
{
ArgumentNullException.ThrowIfNull(workItem);
await _queue.Writer.WriteAsync(ScopedWorkItem);
return;
async ValueTask ScopedWorkItem(CancellationToken ct)
{
using var scope = serviceScopeFactory.CreateScope();
await workItem(scope.ServiceProvider, ct);
}
}
internal async IAsyncEnumerable<Func<CancellationToken, ValueTask>> DequeueAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
{
await foreach (var item in _queue.Reader.ReadAllAsync(ct))
{
yield return item;
}
}
}
В котором вы по сути кладете функцию в канальчик, а бэкграундный хостед сервис этот канальчик потихоньку разгребает вот так:
namespace Dodo.KnowledgeBase.Web.Services.BackgroundTasks;
public class QueuedHostedService(
IBackgroundTaskQueue taskQueue,
ILogger<QueuedHostedService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
logger.LogInformation("Queued Hosted Service is running");
await foreach (var workItem in ((BackgroundTaskQueue)taskQueue).DequeueAsync(ct))
{
try
{
await workItem(ct);
}
catch (Exception ex)
{
logger.LogError(ex, "Error occurred executing task work item");
}
}
}
}
И вот так это можно использовать:
await backgroundTaskQueue.QueueAsync(async (sp, ct) =>
{
var groupAccessService = sp.GetRequiredService<IGroupAccessService>();
await groupAccessService.AssignPredefinedSpacesAsync(existingUser, ct);
});
Вроде несложно, но если что упадет, то всегда можно увидеть в логах в отличие от fire and forget запуска.
В отличие от варианта в доке сделал через IAsyncEnumerable. Так что вышло ну чуть покрасивше и с моей любимой фичей, которую редко когда получается заюзать.