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)
- Descargo de responsabilidad: debería haber dicho que heredamos la aplicación y no puedo responder por qué se implementó así.
- Descargo de responsabilidad 2: Tengo poca o ninguna experiencia con EF.
La comunidad hizo preguntas:
- ¿Qué versión de EF estamos usando? 5.0
- ¿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 DatabaseDataContext
está 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í.
fuente
SaveChanges
.Respuestas:
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í:
También es posible que desee considerar el uso de entidades Stub como una forma de compartir un DbContext con otras clases de repositorio.
fuente
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.
fuente