¿Cómo almacenar en caché las instancias de DataContext en una aplicación de tipo consumidor?


8

Tenemos una aplicación que utiliza SDK proporcionada por nuestro proveedor para integrarse fácilmente con ellos. Este SDK se conecta al punto final AMQP y simplemente distribuye, almacena en caché y transforma mensajes a nuestros consumidores. Anteriormente, esta integración era a través de HTTP con XML como fuente de datos y la integración anterior tenía dos formas de almacenar en caché DataContext: por solicitud web y por ID de subproceso administrado. (1)

Ahora, sin embargo, no nos integramos a través de HTTP, sino más bien AMQP, que es transparente para nosotros, ya que el SDK está haciendo toda la lógica de conexión y solo nos queda definir a nuestros consumidores, por lo que no hay opción de almacenar en caché DataContext "por solicitud web". solo queda por ID de hilo administrado. Implementé un patrón de cadena de responsabilidad, por lo que cuando recibimos una actualización, se coloca en una tubería de controladores que usa DataContext para actualizar la base de datos de acuerdo con las nuevas actualizaciones. Así es como se ve el método de invocación de la canalización:

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

El problema es (al menos lo que creo que es) que esta cadena de responsabilidad es una cadena de métodos que devuelven / inician nuevas tareas, por lo que cuando llega una actualización para la entidad A, se maneja mediante el hilo administrado id = 1, digamos, y solo en algún momento después de nuevo, la misma entidad A llega solo para ser manejada por el hilo administrado id = 2, por ejemplo . Esto lleva a:

System.InvalidOperationException: "Un objeto de entidad no puede ser referenciado por múltiples instancias de IEntityChangeTracker".

porque DataContext del hilo administrado id = 1 ya rastrea la entidad A. (al menos eso es lo que creo que es)

Mi pregunta es ¿cómo puedo almacenar en caché DataContext en mi caso? ¿Ustedes tuvieron el mismo problema? Leí esto y estas respuestas y, por lo que entendí, usar un DataContext estático tampoco es una opción. (2)

  1. Descargo de responsabilidad: debería haber dicho que heredamos la aplicación y no puedo responder por qué se implementó así.
  2. Descargo de responsabilidad 2: Tengo poca o ninguna experiencia con EF.

La comunidad hizo preguntas:

  1. ¿Qué versión de EF estamos usando? 5.0
  2. ¿Por qué las entidades viven más tiempo que el contexto? - No lo hacen, pero tal vez se pregunte por qué las entidades necesitan vivir más tiempo que el contexto. Utilizo repositorios que usan DataContext en caché para obtener entidades de la base de datos para almacenarlas en una colección en memoria que uso como caché.

Así es como se "extraen" las entidades, dónde DatabaseDataContextestá el DataContext en caché del que estoy hablando (BLOB con conjuntos de bases de datos enteras 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;
}

Luego, cada vez que mi solicitud de consumidor recibe un mensaje AMQP, mi patrón de cadena de responsabilidad comienza a verificar si este mensaje y sus datos ya los he procesado. Entonces tengo un método que se ve así:

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
}

Pensé que obtener entidades de la base de datos con el AsNoTracking()método o separar entidades cada vez que "actualizaba" o "insertaba" la entidad resolvería esto, pero no fue así.


No es que tenga una respuesta todavía, ¿puede decirme qué versión de EF está utilizando
Simon Price

también, eche un vistazo a esto y vea si esto le ayuda en todo stackoverflow.com/questions/41346635/…
Simon Price

@SimonPrice, 5.0
kuskmen

Puede destrabar la entidad A después de actualizarla. Pero esto no manejará su problema de concurrencia, solo minimiza la ocurrencia
ilkerkaran

@ilkerkaran, pero si me destrabo después de actualizar / insertar, ¿eso no significa que no podré guardarlo en db más tarde? Básicamente, estoy llamando a actualizar o insertar según los criterios y luego inmediatamente seguido SaveChanges.
kuskmen

Respuestas:


2

Si bien hay una cierta sobrecarga para actualizar un DbContext, y el uso de DI para compartir una sola instancia de un DbContext dentro de una solicitud web puede ahorrar algo de esta sobrecarga, las operaciones CRUD simples pueden renovar un nuevo DbContext para cada acción.

Mirando el código que ha publicado hasta ahora, probablemente tendría una instancia privada del DbContext renovada en el constructor del Repositorio, y luego un nuevo Repositorio para cada método.

Entonces su método se vería así:

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

}

También es posible que desee considerar el uso de entidades Stub como una forma de compartir un DbContext con otras clases de repositorio.


Sí, desafortunadamente, el proyecto de capa de datos es utilizado tanto por nuestro nuevo servicio como por la antigua aplicación del sitio web y no está sujeto a cambios. : / Terminé usando un singleton dbcontext para todo mi flujo y supongo que pensaré en cambiar esto más adelante cuando hagamos nuestra tubería multiproceso
kuskmen

0

Como se trata de alguna aplicación comercial existente, me centraré en ideas que pueden ayudar a resolver el problema en lugar de dar una conferencia sobre las mejores prácticas o proponer cambios arquitectónicos.

Sé que esto es algo obvio, pero a veces volver a redactar mensajes de error nos ayuda a comprender mejor lo que está sucediendo, así que tengan paciencia conmigo.

El mensaje de error indica que las entidades están siendo utilizadas por múltiples contextos de datos, lo que indica que hay varias instancias de dbcontext y que las entidades están referenciadas por más de una de esas instancias.

Luego, la pregunta establece que hay un contexto de datos por subproceso que solía ser por solicitud http y que las entidades se almacenan en caché.

Por lo tanto, parece seguro suponer que las entidades leen de un contexto db en un error de caché y regresan del caché en un golpe. Intentar actualizar las entidades cargadas desde una instancia de contexto db utilizando una segunda instancia de contexto db causa el error. Podemos concluir que en este caso se usó exactamente la misma instancia de entidad en ambas operaciones y que no hay serialización / deserialización para acceder al caché.

Las instancias de DbContext son en sí mismas cachés de entidades a través de su mecanismo interno de seguimiento de cambios y este error es una protección que protege su integridad. Dado que la idea es tener un proceso de larga duración que maneje solicitudes simultáneas a través de múltiples contextos db (uno por hilo) más una caché de entidad compartida, sería muy beneficioso en cuanto al rendimiento y la memoria (el seguimiento de cambios probablemente aumentaría el consumo de memoria en el tiempo ) para intentar cambiar el ciclo de vida de los contextos db para que sea por mensaje o vaciar su rastreador de cambios después de procesar cada mensaje.

Por supuesto, para procesar las actualizaciones de la entidad, deben adjuntarse al contexto de base de datos actual justo después de recuperarlo de la memoria caché y antes de que se les aplique ningún cambio.


Gracias por las ideas, estoy de acuerdo con usted, sin embargo, el problema aquí persiste. Mi solución actual será omitir el "repositorio" y trabajar directamente con el contexto de datos ...
kuskmen
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.