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

Blazor WASM API控制器中Hangfire后台作业触发JsonSerializationException自引用循环异常求助

解决Hangfire序列化时的自引用循环异常

这个问题的核心在于Hangfire需要序列化你要执行的方法及其参数,但你直接传递了ApplicationDbContext实例——DbContext内部关联的实体(比如Role和User)存在双向导航属性,导致Json序列化时触发自引用循环;而且更重要的是,DbContext是请求生命周期内的对象,请求结束后可能被释放,直接传给后台任务会带来生命周期问题。

下面是具体的解决方案,分两种场景推荐:


方案1:创建独立的静态任务方法(快速解决)

把原来的DbContext扩展方法改成独立的静态方法,在任务内部创建新的DbContext实例,避免传递DbContext:

第一步:修改API端点代码

不再传递DbContext给Hangfire,只传任务必要的参数:

[HttpPost("CreateTask")]
public async Task<IActionResult> CreateTask(TaskCreateViewModel taskCreate) {
    if (!ModelState.IsValid) return BadRequest(ModelState);
    
    var task = new Shared.Models.ApplicationTask {
        UserId = taskCreate.UserId,
        ApplicationId = taskCreate.ApplicationId,
        Status = Status.CREATED.ToString(),
        Parameters = taskCreate.Parameters,
        Remarks = JsonSerializer.Serialize(taskCreate.Remarks),
        GUID = Guid.NewGuid().ToString(),
        FinishedFiles = ""
    };
    
    DbContext.Add(task);
    await DbContext.SaveChangesAsync(); // 改用异步方法更贴合ASP.NET Core实践
    
    // 只传递任务所需的核心参数,移除DbContext
    BackgroundJob.Enqueue(() => TaskRunner.RunTask(task.GUID, taskCreate.Remarks, taskCreate.Parameters));
    
    return Ok(ModelState);
}

第二步:创建独立的任务运行类

public static class TaskRunner
{
    public static async Task RunTask(string guid, Dictionary<FileType, string> remarks, string parameter)
    {
        // 在任务内部创建新的DbContext实例,配置和Program.cs中一致
        var dbOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseSqlServer("你的数据库连接字符串") // 替换成实际连接字符串
            .Options;

        using (var dbContext = new ApplicationDbContext(dbOptions))
        {
            var task = await dbContext.Tasks.FirstAsync(t => t.GUID == guid);
            task.Status = Status.ANALYSING.ToString();
            task.Start = DateTime.Now;
            dbContext.Update(task);
            await dbContext.SaveChangesAsync();

            var appId = await dbContext.Applications
                .Where(a => a.FacultyId == task.ApplicationId)
                .Select(x => x.Id)
                .FirstAsync();

            var result = await Runner.Runner
                .Apps[appId]
                .Run(resources: remarks, parameters: parameter);

            task.Status = result.Status.ToString();
            task.End = result.End;
            task.FinishedFiles = result.ResultsSimulation.ToList().Serialize();
            dbContext.Update(task);
            await dbContext.SaveChangesAsync();
        }
    }
}

方案2:使用依赖注入(推荐,更符合ASP.NET Core最佳实践)

如果你的项目已经使用依赖注入,让Hangfire从DI容器中获取DbContext,避免手动创建实例:

第一步:配置Hangfire支持DI

在Program.cs中确保Hangfire已配置并添加DI支持:

// 注册Hangfire服务
builder.Services.AddHangfire(config =>
{
    config.UseSqlServerStorage("你的数据库连接字符串"); // 替换成实际存储配置
});
builder.Services.AddHangfireServer();

第二步:创建任务服务类(带DI)

// 定义接口,方便依赖注入和测试
public interface ITaskRunner
{
    Task RunTask(string guid, Dictionary<FileType, string> remarks, string parameter);
}

public class TaskRunner : ITaskRunner
{
    private readonly ApplicationDbContext _dbContext;

    // 通过构造函数注入DbContext
    public TaskRunner(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task RunTask(string guid, Dictionary<FileType, string> remarks, string parameter)
    {
        var task = await _dbContext.Tasks.FirstAsync(t => t.GUID == guid);
        task.Status = Status.ANALYSING.ToString();
        task.Start = DateTime.Now;
        _dbContext.Update(task);
        await _dbContext.SaveChangesAsync();

        var appId = await _dbContext.Applications
            .Where(a => a.FacultyId == task.ApplicationId)
            .Select(x => x.Id)
            .FirstAsync();

        var result = await Runner.Runner
            .Apps[appId]
            .Run(resources: remarks, parameters: parameter);

        task.Status = result.Status.ToString();
        task.End = result.End;
        task.FinishedFiles = result.ResultsSimulation.ToList().Serialize();
        _dbContext.Update(task);
        await _dbContext.SaveChangesAsync();
    }
}

第三步:注册任务服务到DI容器

在Program.cs中添加:

builder.Services.AddScoped<ITaskRunner, TaskRunner>();

第四步:修改API端点,使用DI调用任务

private readonly ApplicationDbContext _dbContext;
private readonly ITaskRunner _taskRunner;

// 注入ITaskRunner
public YourApiController(ApplicationDbContext dbContext, ITaskRunner taskRunner)
{
    _dbContext = dbContext;
    _taskRunner = taskRunner;
}

[HttpPost("CreateTask")]
public async Task<IActionResult> CreateTask(TaskCreateViewModel taskCreate) {
    if (!ModelState.IsValid) return BadRequest(ModelState);
    
    var task = new Shared.Models.ApplicationTask {
        UserId = taskCreate.UserId,
        ApplicationId = taskCreate.ApplicationId,
        Status = Status.CREATED.ToString(),
        Parameters = taskCreate.Parameters,
        Remarks = JsonSerializer.Serialize(taskCreate.Remarks),
        GUID = Guid.NewGuid().ToString(),
        FinishedFiles = ""
    };
    
    _dbContext.Add(task);
    await _dbContext.SaveChangesAsync();
    
    // 通过Hangfire的泛型Enqueue方法,从DI容器中获取ITaskRunner实例
    BackgroundJob.Enqueue<ITaskRunner>(runner => runner.RunTask(task.GUID, taskCreate.Remarks, taskCreate.Parameters));
    
    return Ok(ModelState);
}

额外提示:避免实体自引用的辅助配置

如果你的实体双向导航属性确实需要保留,可以通过以下方式减少序列化问题:

  1. 在DbContext中关闭延迟加载,避免不必要的导航属性被加载:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseLazyLoadingProxies(false);
}
  1. 配置Json序列化忽略循环引用(仅作为辅助,核心还是不要传递DbContext):
var jsonOptions = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.IgnoreCycles
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 17:27:28