You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Core Web API中异步调用长时运行任务的最佳实现方案及任务状态查询方法咨询

Hey there! Let's break down what's wrong with your current code and walk through better, more reliable solutions step by step.

First: Fix the Broken Async Logic in TestFunctionAsync

Your current TestFunctionAsync has a critical issue: none of your Task.Delay calls are awaited. That means the code will skip all the delays and jump straight to setting ProcessStatus = 9 immediately. Let's fix that first:

public async Task TestFunctionAsync()
{
    ProcessStatus = 0;
    await Task.Delay(TimeSpan.FromSeconds(30)); // Add await here
    ProcessStatus = 1;//写入数据库
    await Task.Delay(TimeSpan.FromSeconds(10)); // Add await here
    ProcessStatus = 2;//从Excel读取数据
    await Task.Delay(TimeSpan.FromSeconds(20)); // Add await here
    ProcessStatus = 3;//处理数据
    await Task.Delay(TimeSpan.FromSeconds(50)); // Add await here
    ProcessStatus = 9;//完成
}

Second: Ditch Task.Run for Reliable Background Tasks

Using Task.Run to start long-running tasks in an ASP.NET Core Web API is risky. The framework can terminate these tasks unexpectedly if the app recycles (e.g., due to resource limits, deployment, or app pool restarts). Here are two robust alternatives:

Option 1: Use BackgroundService + Task Queue (Built-in, No External Libraries)

This is the official ASP.NET Core way to handle on-demand background tasks. We'll create a queue to hold task requests and a background service that processes them.

  1. Create a task queue:
public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
    Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, Task>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, Task>>(options);
    }

    public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
    {
        if (workItem == null) throw new ArgumentNullException(nameof(workItem));
        _queue.Writer.TryWrite(workItem);
    }

    public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
    {
        return await _queue.Reader.ReadAsync(cancellationToken);
    }
}
  1. Build the background service:
public class ReportProcessingService : BackgroundService
{
    private readonly ILogger<ReportProcessingService> _logger;
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly IReportService _reportService; // Your existing report service

    public ReportProcessingService(ILogger<ReportProcessingService> logger, 
                                   IBackgroundTaskQueue taskQueue, 
                                   IReportService reportService)
    {
        _logger = logger;
        _taskQueue = taskQueue;
        _reportService = reportService;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Report processing background service started.");

        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = await _taskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to execute background report task.");
            }
        }

        _logger.LogInformation("Report processing background service stopped.");
    }
}
  1. Register services in Program.cs:
builder.Services.AddSingleton<IBackgroundTaskQueue>(_ => new BackgroundTaskQueue(10));
builder.Services.AddHostedService<ReportProcessingService>();
builder.Services.AddScoped<IReportService, ReportService>(); // Register your report service
  1. Update your API endpoint:
private readonly IBackgroundTaskQueue _taskQueue;
private readonly IReportService _reportService;

public YourApiController(IBackgroundTaskQueue taskQueue, IReportService reportService)
{
    _taskQueue = taskQueue;
    _reportService = reportService;
}

[HttpGet]
public IActionResult TriggerReportTask()
{
    _taskQueue.QueueBackgroundWorkItem(async token =>
    {
        await _reportService.TestFunctionAsync();
    });

    return Accepted(); // 202 tells the client the task was accepted for processing
}

Option 2: Use Hangfire (For Advanced Task Management)

If you need features like task retries, monitoring, or scheduled tasks, Hangfire is a great choice. It persists tasks to a storage (like SQL Server or Redis) so they survive app restarts.

  1. Install NuGet packages:
Install-Package Hangfire.AspNetCore
Install-Package Hangfire.SqlServer # Or your preferred storage
  1. Configure Hangfire in Program.cs:
builder.Services.AddHangfire(config =>
{
    config.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection"));
});
builder.Services.AddHangfireServer();
  1. Update your API endpoint:
[HttpGet]
public IActionResult TriggerReportTask()
{
    BackgroundJob.Enqueue(() => _report.TestFunctionAsync());
    return Accepted();
}

You can access the Hangfire dashboard at /hangfire to monitor task statuses.

Third: Getting ProcessStatus Safely

To track task progress, you need to handle thread safety (for single-instance apps) or shared storage (for multi-instance/load-balanced apps):

Single-Instance App: Thread-Safe Status Storage

Modify your report service to use thread-safe operations for ProcessStatus:

public class ReportService : IReportService
{
    private int _processStatus;
    public int ProcessStatus
    {
        get => Interlocked.CompareExchange(ref _processStatus, 0, 0);
        private set => Interlocked.Exchange(ref _processStatus, value);
    }

    public async Task TestFunctionAsync()
    {
        ProcessStatus = 0;
        await Task.Delay(TimeSpan.FromSeconds(30));
        ProcessStatus = 1;//写入数据库
        await Task.Delay(TimeSpan.FromSeconds(10));
        ProcessStatus = 2;//从Excel读取数据
        await Task.Delay(TimeSpan.FromSeconds(20));
        ProcessStatus = 3;//处理数据
        await Task.Delay(TimeSpan.FromSeconds(50));
        ProcessStatus = 9;//完成
    }
}

Add an endpoint to fetch the status:

[HttpGet("status")]
public IActionResult GetTaskStatus()
{
    return Ok(new { CurrentStatus = _reportService.ProcessStatus });
}

Multi-Instance/Load-Balanced App: Shared Storage

For apps running on multiple servers, store ProcessStatus in a shared database or Redis. Here's a quick example with a database:

  1. Create a task status table:
CREATE TABLE TaskStatuses (
    TaskId UNIQUEIDENTIFIER PRIMARY KEY,
    Status INT NOT NULL,
    CreatedAt DATETIME2 DEFAULT GETUTCDATE()
);
  1. Update TestFunctionAsync to update the database:
public async Task TestFunctionAsync(Guid taskId)
{
    await _dbContext.TaskStatuses
        .Where(t => t.TaskId == taskId)
        .ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, 0));
    
    await Task.Delay(TimeSpan.FromSeconds(30));
    await _dbContext.TaskStatuses
        .Where(t => t.TaskId == taskId)
        .ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, 1));
    
    // Repeat for other statuses...
}
  1. Modify the API to return a task ID:
[HttpGet]
public async Task<IActionResult> TriggerReportTask()
{
    var taskId = Guid.NewGuid();
    _dbContext.TaskStatuses.Add(new TaskStatus { TaskId = taskId, Status = 0 });
    await _dbContext.SaveChangesAsync();

    BackgroundJob.Enqueue(() => _report.TestFunctionAsync(taskId)); // Or use the queue approach
    return Accepted(new { TaskId = taskId });
}
  1. Add a status endpoint that queries the database:
[HttpGet("status/{taskId:guid}")]
public async Task<IActionResult> GetTaskStatus(Guid taskId)
{
    var status = await _dbContext.TaskStatuses.FindAsync(taskId);
    if (status == null) return NotFound();
    return Ok(new { TaskId = taskId, CurrentStatus = status.Status });
}

内容的提问来源于stack exchange,提问作者Sandeep Thomas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 21:32:50