.NET Entity Framework y transacciones

Siendo nuevo en Entity Framework, estoy bastante atascado en cómo proceder con este conjunto de problemas. En el proyecto en el que estoy trabajando actualmente, todo el sitio está muy integrado con el modelo EF. Al principio, el acceso al contexto EF se controlaba con un bootstrapper de Dependency Injection. Por razones operacionales, no pudimos usar una biblioteca DI. Eliminé esto y usé un modelo de instancias individuales del objeto de contexto donde fue requerido. Empecé a recibir la siguiente excepción:

El tipo ‘XXX’ se ha mapeado más de una vez.

Llegamos a la conclusión de que las diferentes instancias del contexto estaban causando este problema. Luego resumí el objeto de contexto en una única instancia estática a la que accedía cada hilo / página. Ahora recibo una de varias excepciones sobre transacciones:

Nueva transacción no está permitida porque hay otros hilos en ejecución en la sesión.

La operación de transacción no se puede realizar porque hay solicitudes pendientes trabajando en esta transacción.

ExecuteReader requiere que el comando tenga una transacción cuando la conexión asignada al comando se encuentra en una transacción local pendiente. La propiedad de Transacción del comando no se ha inicializado.

La última de estas excepciones ocurrió en una operación de carga. No estaba tratando de guardar el estado del contexto en el Db en el hilo que falló. Sin embargo, había otro hilo que realizaba una operación de este tipo.

Estas excepciones son intermitentes en el mejor de los casos, pero he logrado que el sitio entre en un estado donde se rechazaron nuevas conexiones debido a un locking de transacción. Desafortunadamente no puedo encontrar los detalles de la excepción.

Supongo que mi primera pregunta es, ¿debería usarse el modelo EF desde una única instancia estática? Además, ¿es posible eliminar la necesidad de transacciones en EF? Intenté usar un objeto TransactionScope sin éxito …

Para ser sincero, estoy muy atrapado aquí, y no puedo entender por qué (lo que debería ser) operaciones bastante simples están causando tal problema …

Crear un ObjectContext global en una aplicación web es muy malo. La clase ObjectContext no es segura para subprocesos. Se basa en el concepto de la unidad de trabajo y esto significa que se utiliza para operar un solo caso de uso: por lo tanto, para una transacción comercial. Está destinado a manejar una única solicitud.

La excepción que obtienes sucede porque para cada solicitud creas una nueva transacción, pero intenta usar ese mismo ObjectContext . Tienes suerte de que ObjectContext detecte esto y arroje una excepción, porque ahora descubriste que esto no funcionará.

Por favor piensa en esto porque esto no puede funcionar. ObjectContext contiene un caché local de entidades en su base de datos. Le permite hacer un montón de cambios y finalmente enviar esos cambios a la base de datos. Cuando se usa un único ObjectContext estático, con múltiples usuarios que llaman a SaveChanges en ese objeto, ¿cómo se supone que se debe saber exactamente qué se debe comprometer y qué no? Debido a que no lo sabe, guardará todos los cambios, pero en ese momento otro usuario podría estar haciendo cambios. Cuando tengas suerte, EF o tu base de datos fallarán, porque las entidades están en un estado inválido. Si tienes objetos desafortunados que están en un estado inválido, se guardan con éxito en la base de datos y es posible que descubras semanas después que tu base de datos está llena de basura. La solución a su problema es crear al menos un ObjectContext por solicitud . Aunque en teoría podría almacenar en caché un contexto de objeto en la sesión del usuario, esta también es una mala idea, ya que el ObjectContext generalmente vivirá demasiado tiempo y contendrá datos obsoletos (porque su caché interna no se actualizará automáticamente).

ACTUALIZAR :

También tenga en cuenta que tener un ObjectContext por hilo es tan malo como tener una sola instancia para la aplicación web completa. ASP.NET utiliza un grupo de subprocesos, lo que significa que se creará una cantidad limitada de subprocesos durante la vida de una aplicación web. Esto básicamente significa que esas instancias de ObjectContext en ese caso todavía vivirán durante el tiempo de vida de la aplicación, causando los mismos problemas con la caducidad de los datos.

Podría pensar que tener un DbContext por subproceso es realmente seguro para subprocesos, pero esto no suele ser el caso, ya que ASP.NET tiene un modelo asincrónico que permite finalizar solicitudes en un subproceso diferente de donde se inició (y las últimas versiones de MVC y Web API incluso permiten que un número arbitrario de subprocesos maneje una sola solicitud en orden secuencial). Esto significa que el hilo que inició una solicitud y creó el ObjectContext puede estar disponible para procesar otra solicitud mucho antes de que finalice la solicitud inicial. Sin embargo, los objetos utilizados en esa solicitud (como una página web, un controlador o cualquier clase de negocios) podrían hacer referencia a ese ObjectContext . Dado que la nueva solicitud web se ejecuta en el mismo subproceso, obtendrá la misma instancia de ObjectContext que la que usa la solicitud anterior. Esto nuevamente causa condiciones de carrera en su aplicación y causa los mismos problemas de seguridad de subprocesos que lo que causa una instancia global de ObjectContext .

Al referirse al “sitio” en su pregunta, supongo que esta es una aplicación web. Los miembros estáticos existen solo una vez para toda la aplicación, si está utilizando un patrón de tipo singleton con una sola instancia de contexto en toda la aplicación, todo tipo de solicitudes van a estar en todo tipo de estados en toda la aplicación.

Una única instancia de contexto estático no funcionará, pero las instancias de contexto múltiples por subproceso serán problemáticas y no se podrán mezclar y combinar contextos. Lo que necesitas es un único contexto por hilo. Hemos hecho esto en nuestra aplicación usando un patrón de tipo de dependency injection. Nuestras clases BLL y DAL toman un contexto como parámetro en los métodos, de esa manera puedes hacer algo como a continuación:

 using (TransactionScope ts = new TransactionScope()) { using (ObjectContext oContext = new ObjectContext("MyConnection")) { oBLLClass.Update(oEntity, oContext); } } 

Si necesita llamar a otros métodos BLL / DAL dentro de su actualización (o el método que elija) simplemente pase el mismo contexto. De esa forma las actualizaciones / inserciones / eliminaciones son atómicas, todo dentro de un único método está usando la misma instancia de contexto, pero esa instancia no está siendo utilizada por otros hilos.