Como armazenar em cache instâncias do DataContext em um aplicativo do tipo consumidor?

8

Temos um aplicativo usando o SDK fornecido pelo nosso provedor para integrar-se facilmente a eles. Esse SDK se conecta ao terminal AMQP e simplesmente distribui, armazena em cache e transforma mensagens para nossos consumidores. Anteriormente, essa integração era via HTTP com XML como fonte de dados e a integração antiga tinha duas maneiras de armazenar em cache o DataContext - por solicitação da Web e por ID de encadeamento gerenciado. (1)

Agora, no entanto, não integramos o HTTP, mas sim o AMQP, que é transparente para nós, pois o SDK está fazendo toda a lógica de conexão e resta apenas definir nossos consumidores para que não haja opção de armazenar em cache o DataContext "por solicitação da web", portanto apenas por ID de encadeamento gerenciado é deixado. Eu implementei o padrão de cadeia de responsabilidade; portanto, quando uma atualização chega, ela é colocada em um pipeline de manipuladores que usa o DataContext para atualizar o banco de dados de acordo com as novas atualizações. É assim que o método de chamada do pipeline se parece:

public Task Invoke(TInput entity)
{
    object currentInputArgument = entity;

    for (var i = 0; i < _pipeline.Count; ++i)
    {
        var action = _pipeline[i];
        if (action.Method.ReturnType.IsSubclassOf(typeof(Task)))
        {
            if (action.Method.ReturnType.IsConstructedGenericType)
            {
                dynamic tmp = action.DynamicInvoke(currentInputArgument);
                currentInputArgument = tmp.GetAwaiter().GetResult();
            }
            else
            {
                (action.DynamicInvoke(currentInputArgument) as Task).GetAwaiter().GetResult();
            }
        }
        else
        {
            currentInputArgument = action.DynamicInvoke(currentInputArgument);
        }
    }

    return Task.CompletedTask;
}

O problema é (pelo menos o que eu acho que é) que essa cadeia de responsabilidade é uma cadeia de métodos retornando / iniciando novas tarefas; portanto, quando uma atualização para a entidade A chega, ela é tratada pelo ID do thread gerenciado = 1, digamos, e apenas um tempo depois novamente a mesma entidade A chega apenas para ser tratada pelo ID do encadeamento gerenciado = 2, por exemplo . Isto leva a:

System.InvalidOperationException: 'Um objeto de entidade não pode ser referenciado por várias instâncias do IEntityChangeTracker.'

porque o DataContext do ID do segmento gerenciado = 1 já rastreia a entidade A. (pelo menos é o que eu acho que é)

Minha pergunta é como posso armazenar em cache o DataContext no meu caso? Vocês tiveram o mesmo problema? Eu li estas e estas respostas e pelo que entendi usando um DataContext estático também não é uma opção. (2)

  1. Isenção de responsabilidade: eu deveria ter dito que herdamos o aplicativo e não posso responder por que ele foi implementado dessa maneira.
  2. Isenção de responsabilidade 2: Tenho pouca ou nenhuma experiência com a EF.

Perguntas frequentes da comunidade:

  1. Qual versão do EF estamos usando? 5.0
  2. Por que as entidades vivem mais que o contexto? - Eles não sabem, mas talvez você esteja se perguntando por que as entidades precisam viver mais do que o contexto. Uso repositórios que usam DataContext em cache para obter entidades do banco de dados para armazená-las em uma coleção na memória que eu uso como cache.

É assim que as entidades são "extraídas", onde DatabaseDataContextestá o DataContext em cache de que estou falando (BLOB com conjuntos de bancos de dados inteiros dentro)

protected IQueryable<T> Get<TProperty>(params Expression<Func<T, TProperty>>[] includes)
{
    var query = DatabaseDataContext.Set<T>().AsQueryable();

    if (includes != null && includes.Length > 0)
    {
        foreach (var item in includes)
        {
            query = query.Include(item);
        }
    }

    return query;
}

Então, sempre que meu aplicativo consumidor recebe a mensagem AMQP, meu padrão de cadeia de responsabilidade começa a verificar se essa mensagem e seus dados já foram processados. Então, eu tenho um método que se parece com isso:

public async Task<TEntity> Handle<TEntity>(TEntity sportEvent)
            where TEntity : ISportEvent
{
    ... some unimportant business logic

    //save the sport
    if (sport.SportID > 0) // <-- this here basically checks if so called 
                           // sport is found in cache or not
                           // if its found then we update the entity in the db
                           // and update the cache after that
    {
        _sportRepository.Update(sport); /* 
                                         * because message update for the same sport can come
                                         * and since DataContext is cached by threadId like I said
                                         * and Update can be executed from different threads
                                         * this is where aforementioned exception is thrown
                                        */

    }
    else                   // if not simply insert the entity in the db and the caches
    {
        _sportRepository.Insert(sport);
    }

    _sportRepository.SaveDbChanges();

    ... updating caches logic
}

Eu pensei que obter entidades do banco de dados com AsNoTracking()método ou desanexar entidades toda vez que "atualizo" ou "insiro" a entidade resolverá isso, mas isso não aconteceu.

kuskmen
fonte
Não que eu tenha uma resposta ainda, você pode me dizer qual versão do EF você está usando, por favor #
Simon Price
Além disso, dê uma olhada nisso e veja se isso ajuda você em todos os stackoverflow.com/questions/41346635/…
Simon Price
@SimonPrice, 5.0
kuskmen
Você pode rastrear a entidade A depois de atualizá-la. Mas isso não vai lidar com o seu problema de concorrência, apenas minimiza a ocorrência
ilkerkaran
@ilkerkaran, mas se eu rastrear após a atualização / inserção, isso não significa que não poderei salvá-lo no db mais tarde? Estou basicamente chamando update ou insert com base em critérios e, em seguida, imediatamente seguido por SaveChanges.
kuskmen

Respostas:

2

Embora exista uma certa sobrecarga na atualização de um DbContext, e o uso do DI para compartilhar uma única instância de um DbContext em uma solicitação da Web possa salvar parte dessa sobrecarga, operações simples de CRUD podem apenas renovar um novo DbContext para cada ação.

Olhando para o código que você postou até agora, eu provavelmente teria uma instância privada do DbContext atualizada no construtor Repository e, em seguida, um Repository para cada método.

Então seu método seria algo parecido com isto:

public async Task<TEntity> Handle<TEntity>(TEntity sportEvent)
        where TEntity : ISportEvent
{
        var sportsRepository = new SportsRepository()

        ... some unimportant business logic

        //save the sport
        if (sport.SportID > 0) 
        {
            _sportRepository.Update(sport);
        }
        else
        {
            _sportRepository.Insert(sport);
        }

        _sportRepository.SaveDbChanges();

}

public class SportsRepository
{
    private DbContext _dbContext;

    public SportsRepository()
    {
        _dbContext = new DbContext();
    }

}

Você também pode considerar o uso de Entidades de Stub como uma maneira de compartilhar um DbContext com outras classes de repositório.

ste-fu
fonte
Sim, infelizmente, o projeto datalayer é usado pelo nosso novo serviço e pelo aplicativo antigo do site e não é um assunto de mudança. : / Acabei usando um dbcontext de singleton para todo o meu fluxo e acho que vou pensar em mudar isso mais tarde quando fizermos nosso pipeline multithread
kuskmen
0

Como se trata de algum aplicativo de negócios existente, vou me concentrar nas idéias que podem ajudar a resolver o problema, em vez de dar palestras sobre práticas recomendadas ou propor alterações arquiteturais.

Sei que isso é meio óbvio, mas, às vezes, reformular as mensagens de erro nos ajuda a entender melhor o que está acontecendo.

A mensagem de erro indica que as entidades estão sendo usadas por vários contextos de dados, o que indica que existem várias instâncias de dbcontext e que as entidades são referenciadas por mais de uma dessas instâncias.

Em seguida, a pergunta afirma que há um contexto de dados por encadeamento que costumava ser por solicitação http e que as entidades são armazenadas em cache.

Portanto, parece seguro supor que as entidades lidas em um contexto de banco de dados após uma falta de cache e retornadas do cache em uma ocorrência. Tentar atualizar entidades carregadas de uma instância de contexto db usando uma segunda instância de contexto db causa a falha. Podemos concluir que, nesse caso, a mesma instância de entidade exata foi usada em ambas as operações e não há serialização / desserialização para acessar o cache.

As instâncias do DbContext são, por si só, caches de entidade por meio de seu mecanismo interno de controle de alterações e esse erro é uma salvaguarda que protege sua integridade. Como a idéia é ter um processo de longa execução que lide com solicitações simultâneas através de vários contextos de banco de dados (um por encadeamento) mais um cache de entidade compartilhada, seria muito benéfico em termos de desempenho e de memória (o rastreamento de alterações provavelmente aumentaria o consumo de memória com o tempo ) para tentar alterar o ciclo de vida dos contextos db para ser por mensagem ou esvaziar seu rastreador de alterações após o processamento de cada mensagem.

Obviamente, para processar atualizações de entidade, elas precisam ser anexadas ao contexto atual do banco de dados logo após recuperá-lo do cache e antes que quaisquer alterações sejam aplicadas a elas.

Victor Ortuondo
fonte
Obrigado pelas informações, eu concordo com você, mas o problema aqui permanece. Minha solução atual será pular "repositório" e trabalhar diretamente com o contexto de dados ...
kuskmen