托管服务中数据库上下文的并发问题

Concurrency issue with database context in hosted service

我有一个通过 API 端点处理对象 PUT/POST 的托管服务,也就是说,一旦给出新实体或编辑现有实体,(a) 托管服务开始处理它(一个漫长的 运行ning 过程),并且 (b) received/modified 对象(作为 JSON 对象)返回给 API 调用者。

当 PUT/POST 一个实体时,我在这里和那里看到 运行 时间错误(例如,在对象 JSON 序列化程序处)抱怨不同的问题,例如:

ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.

或:

InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext.

最初我使用的是数据库上下文池,但是 according to this,池似乎与托管服务存在已知问题。 因此,我切换到常规 AddDbContext;但是,这都没有解决问题。

这是我定义数据库上下文和托管服务的方式:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCustomDbContext(Configuration);

        // This is the hosted service:
        services.AddHostedService<MyHostedService>();
    }
}

public static class CustomExtensionMethods
{
    public static IServiceCollection AddCustomDbContext(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddDbContext<MyContext>(
            options =>
            {
                options
                .UseLazyLoadingProxies(true)
                .UseSqlServer(
                    configuration.GetConnectionString("DefaultConnection"),
                    sqlServerOptionsAction: sqlOptions => { sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); });
            });

        return services;
    }
}

我按如下方式访问托管服务中的数据库上下文 ():

using(var scope = Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<MyContext>();
}

编辑 1

如前所述,错误发生在代码周围;但是,由于我提到了序列化程序上发生的错误,因此我在下面共享序列化程序代码:

public class MyJsonConverter : JsonConverter
{
    private readonly Dictionary<string, string> _propertyMappings;

    public MyJsonConverter()
    {
        _propertyMappings = new Dictionary<string, string>
        {
            {"id", nameof(MyType.ID)},
            {"name", nameof(MyType.Name)}
        };
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JObject obj = new JObject();
        Type type = value.GetType();

        foreach (PropertyInfo prop in type.GetProperties())
        {
            if (prop.CanRead)
            {
                // The above linked errors happen here. 
                object propVal = prop.GetValue(value, null);
                if (propVal != null)
                    obj.Add(prop.Name, JToken.FromObject(propVal, serializer));
            }
        }

        obj.WriteTo(writer);
    }
}

更新 2

示例API端点如下:

[Route("api/v1/[controller]")]
[ApiController]
public class MyTypeController : ControllerBase
{
    private readonly MyContext _context;
    private MyHostedService _service;

    public MyTypeController (
        MyContext context,
        MyHostedService service)
    {
        _context = context;
        _service = service
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<IEnumerable<MyType>>> GetMyType(int id)
    {
        return await _context.MyTypes.FindAsync(id);
    }

    [HttpPost]
    public async Task<ActionResult<MyType>> PostMyType(MyType myType)
    {
        myType.Status = State.Queued;
        _context.MyTypes.Add(myType);
        _context.MyTypes.SaveChangesAsync().ConfigureAwait(false);

        // the object is queued in the hosted service for execution.
        _service.Enqueue(myType);

        return CreatedAtAction("GetMyType", new { id = myType.ID }, myType);
    }
}

以下行最有可能导致 ObjectDisposedException 错误:

return await _context.MyTypes.FindAsync(id);

return CreatedAtAction("GetMyType", new { id = myType.ID }, myType);

这是因为你依赖这个变量:

private readonly MyContext _context;

因为对象 myType 已附加到该上下文。

正如我之前提到的,发送上下文实体进行序列化并不是一个好主意,因为当序列化器有机会触发时,上下文可能已经被释放了。使用模型(意思是 Models 文件夹中的 class)并将所有相关属性从您的真实实体映射到它。例如,您可以创建一个名为 MyTypeViewModel 的 class,它只包含您需要 return:

的属性
public class MyTypeViewModel
{
    public MyTypeViewModel(MyType obj)
    {
        Map(obj);
    }

    public int ID { get; set; }

    private void Map(MyType obj)
    {
        this.ID = obj.ID;
    }
}

然后不使用 return 创建实体,而是使用视图模型:

var model = new MyTypeViewModel(myType);
return CreatedAtAction("GetMyType", new { id = myType.ID }, model);

InvalidOperationException 而言,我有根据的猜测是,由于您没有等待 SaveChangesAsync 方法,序列化程序在原始操作仍在进行时触发,导致双重命中到上下文,导致错误。

SaveChangesAsync 方法上使用 await 应该可以解决这个问题,但是您仍然需要停止发送延迟加载的实体进行序列化。

经过进一步审查,服务本身也可能会导致问题,因为您将其传递给对象 myType:

_service.Enqueue(myType);

如果服务正在对导致调用现在处置的上下文的对象执行某些操作,或者同时其他异步部分(例如序列化)尝试延迟加载内容,则可能会出现相同的两个问题。