Запуск фоновой задачи из действия контроллера в asp.net core 2

Я разрабатываю веб-приложение с REST Api, используя С# с asp.net core 2.0

То, что я хочу достичь, - это когда клиент отправляет запрос конечной точке, я буду запускать фоновую задачу, отделенную от контекста запроса клиента, которая будет завершена, если задача начнется успешно.

Я знаю, что есть HostedService, но проблема в том, что HostedService запускается при запуске сервера, и насколько я знаю, нет способа запустить HostedService вручную с контроллера.

Вот простой код, который демонстрирует вопрос.

[Authorize(AuthenticationSchemes = "UsersScheme")]
public class UsersController : Controller
{

    [HttpPost]
    public async Task<JsonResult> StartJob([FromForm] string UserId, [FromServices] IBackgroundJobService backgroundService) {

           //check user account
           (bool isStarted, string data) result = backgroundService.Start();

           return JsonResult(result);
    }
}

Ответ 1

Вы все еще можете использовать IHostedService в качестве базы для фоновых задач в сочетании с BlockingCollection.

Создайте оболочку для BlockingCollection, чтобы вы могли добавить ее как синглтон.

public class TasksToRun
{
    private readonly BlockingCollection<TaskSettings> _tasks;

    public TasksToRun() => _tasks = new BlockingCollection<TaskSettings>();

    public void Enqueue(TaskSettings settings) => _tasks.Add(settings);

    public TaskSettings Dequeue(CancellationToken token) => _tasks.Take(token);
}

Затем в реализации IHostedService "слушайте" задачи, а когда задачи "прибывают", выполняйте его.
BlockingCollection остановит выполнение, если коллекция пуста, поэтому ваш цикл while не будет занимать процессорное время.
Метод .Take принимает cancellationToken в качестве аргумента. С помощью токена вы можете отменить "ожидание" следующей задачи, когда приложение остановится.

public class BackgroundService : IHostedService
{
    private readonly TasksToRun _tasks;

    private CancellationTokenSource _tokenSource;

    private Task _currentTask;

    public BackgroundService(TasksToRun tasks) => _tasks = tasks;

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        while (cancellationToken.IsCancellationRequested == false)
        {
            try
            {
                var taskToRun = _tasks.Dequeue(_tokenSource.Token);

                // We need to save executable task, 
                // so we can gratefully wait for it completion in Stop method
                _currentTask = ExecuteTask(taskToRun);               
                await _currentTask;
            }
            catch (OperationCanceledException)
            {
                // execution cancelled
            }
        }
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _tokenSource.Cancel(); // cancel "waiting" for task in blocking collection

        if (_currentTask == null) return;

        // wait when _currentTask is complete
        await Task.WhenAny(_currentTask, Task.Delay(-1, cancellationToken));
    }
}

А в контроллере вы просто добавляете задачу, которую хотите запустить, в нашу коллекцию

public class JobController : Controller
{
    private readonly TasksToRun _tasks;

    public JobController(TasksToRun tasks) => _tasks = tasks;

    public IActionResult PostJob()
    {
        var settings = CreateTaskSettings();

        _tasks.Enqueue(settings);

        return Ok();
    }
}

Оболочка для блокировки коллекции должна быть зарегистрирована для внедрения зависимостей как синглтон

services.AddSingleton<TasksToRun, TasksToRun>();

Зарегистрировать фоновую службу

services.AddHostedService<BackgroundService>();