Проблема параллелизма с контекстом базы данных в размещенном сервисе - PullRequest
0 голосов
/ 28 января 2020

У меня есть размещенный сервис, который обрабатывает объекты PUT / POST через конечную точку API, то есть, как только дается новый объект или редактируется существующий, (a) размещенный сервис начинает его обрабатывать (долго работает обработать) и (b) полученный / измененный объект, возвращенный (как JSON объект) вызывающей стороне API.

Когда PUT / POST сущность, тут и там я вижу ошибки времени выполнения (например, в JSON serializer), жалующиеся на различные проблемы, такие как:

ObjectDisposedException : Невозможно получить доступ к удаленному объекту. Распространенной причиной этой ошибки является удаление контекста, который был разрешен путем внедрения зависимостей, а затем попытка использовать тот же экземпляр контекста в другом месте вашего приложения. Это может произойти, если вы вызываете Dispose () для контекста или заключаете контекст в оператор using. Если вы используете внедрение зависимости, вы должны позволить контейнеру введения зависимости позаботиться об удалении экземпляров контекста.

или:

InvalidOperationException: вторая операция запущена в этом контексте до завершения предыдущей операции. Обычно это вызвано тем, что разные потоки используют один и тот же экземпляр DbContext.

Изначально я использовал пул контекста базы данных, но в соответствии с этим кажется, что в пуле есть известные проблемы с размещенными службами. Поэтому я переключился обычному 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>();
}

Edit 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);
    }
}

1 Ответ

1 голос
/ 29 января 2020

Следующие строки, скорее всего, вызывают ошибку ObjectDisposedException:

return await _context.MyTypes.FindAsync(id);

и

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

Это потому, что вы полагаетесь на эту переменную:

private readonly MyContext _context;

Поскольку объект myType был присоединен к этому контексту.

Как я упоминал ранее, отправлять объекты контекста для сериализации не очень хорошая идея, потому что к тому времени, когда сериализатор имеет шанс запустить контекст мог быть удален. Вместо этого используйте модель (имеется в виду класс в папке Models) и сопоставьте с ней все соответствующие свойства вашей реальной сущности. например, вы можете создать класс с именем MyTypeViewModel, содержащий только те свойства, которые вам нужно вернуть:

public class MyTypeViewModel
{
    public MyTypeViewModel(MyType obj)
    {
        Map(obj);
    }

    public int ID { get; set; }

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

Затем вместо возврата сущности используйте модель представления:

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

Что касается InvalidOperationException, я считаю, что, поскольку вы не ожидаете метода SaveChangesAsync, сериализатор запускается, пока исходная операция все еще выполняется, вызывая двойной удар по контексту, что приводит к ошибка.

Использование await в методе SaveChangesAsync должно исправить это, но вам все равно нужно прекратить отправку загруженных объектов для сериализации.

При дальнейшем рассмотрении , сама служба также может вызывать проблемы, так как вы передаете ей ссылку на объект myType:

_service.Enqueue(myType);

Те же две проблемы могут возникнуть, если служба что-то делает с объектом, вызывающим вызов в текущем расположении контекста или одновременно с другими асинхронными частями (например, сериализация) пытается лениво загрузить материал.

...