He estado leyendo muchos artículos que explican cómo configurar Entity Framework DbContext
para que solo se cree y use uno por solicitud web HTTP usando varios marcos DI.
¿Por qué es una buena idea en primer lugar? ¿Qué ventajas obtienes al usar este enfoque? ¿Hay ciertas situaciones en las que esto sería una buena idea? ¿Hay cosas que puede hacer con esta técnica que no puede hacer al instanciar DbContext
s por llamada al método de repositorio?
Respuestas:
Comencemos repitiendo Ian: Tener un single
DbContext
para toda la aplicación es una mala idea. La única situación en la que esto tiene sentido es cuando tiene una aplicación de subproceso único y una base de datos que solo utiliza esa instancia de aplicación única. NoDbContext
es seguro para subprocesos y, como losDbContext
datos de la memoria caché, se vuelven obsoletos muy pronto. Esto lo meterá en todo tipo de problemas cuando varios usuarios / aplicaciones trabajen en esa base de datos simultáneamente (lo cual es muy común, por supuesto). Pero espero que ya lo sepas y solo quieras saber por qué no inyectar una nueva instancia (es decir, con un estilo de vida transitorio)DbContext
en cualquier persona que lo necesite. (para obtener más información sobre por qué un soloDbContext
-o incluso en contexto por hilo- es malo, lea esta respuesta ).Permítanme comenzar diciendo que registrar un
DbContext
transitorio podría funcionar, pero generalmente desea tener una sola instancia de dicha unidad de trabajo dentro de un cierto alcance. En una aplicación web, puede ser práctico definir dicho alcance en los límites de una solicitud web; por lo tanto, un estilo de vida por solicitud web. Esto le permite dejar que un conjunto completo de objetos opere dentro del mismo contexto. En otras palabras, operan dentro de la misma transacción comercial.Si no tiene el objetivo de que un conjunto de operaciones opere dentro del mismo contexto, en ese caso el estilo de vida transitorio está bien, pero hay algunas cosas que debe observar:
_context.SaveChanges()
(de lo contrario, los cambios se perderían). Esto puede complicar su código, y agrega una segunda responsabilidad al código (la responsabilidad de controlar el contexto), y es una violación del Principio de Responsabilidad Única .DbContext
] nunca abandonen el alcance de dicha clase, porque no pueden usarse en la instancia de contexto de otra clase. Esto puede complicar enormemente su código, porque cuando necesita esas entidades, debe cargarlas nuevamente por id, lo que también podría causar problemas de rendimiento.DbContext
implementaIDisposable
, probablemente aún desee deshacerse de todas las instancias creadas. Si quieres hacer esto, básicamente tienes dos opciones. Debe deshacerse de ellos con el mismo método justo después de llamarcontext.SaveChanges()
, pero en ese caso la lógica de negocios toma posesión de un objeto que se transmite desde el exterior. La segunda opción es desechar todas las instancias creadas en el límite de la solicitud Http, pero en ese caso todavía necesita algún tipo de alcance para que el contenedor sepa cuándo deben eliminarse esas instancias.Otra opción es no inyectar a
DbContext
en absoluto. En cambio, inyecta unaDbContextFactory
que puede crear una nueva instancia (solía usar este enfoque en el pasado). De esta manera, la lógica de negocios controla el contexto explícitamente. Si pudiera verse así:El lado positivo de esto es que administras la vida de forma
DbContext
explícita y es fácil configurarlo. También le permite usar un contexto único en un determinado alcance, lo que tiene ventajas claras, como ejecutar código en una sola transacción comercial y poder transferir entidades, ya que se originan a partir de la mismaDbContext
.La desventaja es que tendrá que pasar
DbContext
de un método a otro (que se denomina Método de inyección). Tenga en cuenta que, en cierto sentido, esta solución es la misma que el enfoque 'delimitado', pero ahora el alcance se controla en el código de la aplicación en sí (y posiblemente se repite muchas veces). Es la aplicación la responsable de crear y eliminar la unidad de trabajo. ComoDbContext
se crea después de que se construye el gráfico de dependencia, la Inyección del constructor está fuera de la imagen y debe diferir a la Inyección del método cuando necesita pasar el contexto de una clase a otra.La inyección de métodos no es tan mala, pero cuando la lógica de negocios se vuelve más compleja y se involucran más clases, tendrá que pasarla de un método a otro y de una clase a otra, lo que puede complicar mucho el código (he visto esto en el pasado). Sin embargo, para una aplicación simple, este enfoque funcionará bien.
Debido a las desventajas, este enfoque de fábrica tiene para sistemas más grandes, otro enfoque puede ser útil y es el que permite que el contenedor o el código de infraestructura / raíz de composición administren la unidad de trabajo. Este es el estilo sobre el que trata su pregunta.
Al permitir que el contenedor y / o la infraestructura manejen esto, el código de su aplicación no se contamina al tener que crear, (opcionalmente) confirmar y eliminar una instancia de UoW, lo que mantiene la lógica de negocios simple y limpia (solo una responsabilidad única). Hay algunas dificultades con este enfoque. Por ejemplo, ¿cometió y eliminó la instancia?
La eliminación de una unidad de trabajo se puede hacer al final de la solicitud web. Sin embargo, muchas personas suponen incorrectamente que este es también el lugar para comprometer la unidad de trabajo. Sin embargo, en ese punto de la aplicación, simplemente no puede determinar con certeza si la unidad de trabajo debería estar realmente comprometida. por ejemplo, si el código de la capa empresarial arrojó una excepción que se detectó en la parte superior de la pila de llamadas, definitivamente no desea confirmar.
La solución real es nuevamente administrar explícitamente algún tipo de alcance, pero esta vez hacerlo dentro de la raíz de composición. Resumiendo toda la lógica de negocios detrás del patrón de comando / controlador , podrá escribir un decorador que se pueda envolver alrededor de cada controlador de comando que permita hacer esto. Ejemplo:
Esto garantiza que solo necesite escribir este código de infraestructura una vez. Cualquier contenedor DI sólido le permite configurar dicho decorador para que se ajuste a todas las
ICommandHandler<T>
implementaciones de manera coherente.fuente
CreateCommand<TEnity>
y otro genéricoCreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>
(y hacer lo mismo para Actualizar, Eliminar y tener una solaGetByIdQuery<TEntity>
consulta). Aún así, debe preguntarse si este modelo es una abstracción útil para las operaciones CRUD, o si solo agrega complejidad. Aún así, podría beneficiarse de la posibilidad de agregar fácilmente preocupaciones transversales (a través de decoradores) usando este modelo. Tendrás que sopesar los pros y los contras.TransactionCommandHandlerDecorator
? por ejemplo, si la clase decorada esInsertCommandHandler
clase, ¿cómo podría registrar la operación de inserción en el contexto (DbContext en EF)?Hay dos recomendaciones contradictorias de Microsoft y muchas personas usan DbContexts de una manera completamente divergente.
Esos se contradicen entre sí porque si su Solicitud no tiene mucha relación con las cosas de Db, entonces su DbContext se mantiene sin ningún motivo. Por lo tanto, es un desperdicio mantener vivo su DbContext mientras su solicitud solo está esperando que se hagan cosas al azar ...
Muchas personas que siguen la regla 1 tienen sus DbContexts dentro de su "patrón de repositorio" y crean una nueva instancia por consulta de base de datos, por lo que X * DbContext por solicitud
Simplemente obtienen sus datos y eliminan el contexto lo antes posible. Esto es considerado por MUCHAS personas como una práctica aceptable. Si bien esto tiene los beneficios de ocupar sus recursos de db por el tiempo mínimo, sacrifica claramente todo lo que UnitOfWork and Caching candy EF tiene para ofrecer.
Mantener viva una única instancia multipropósito de DbContext maximiza los beneficios del almacenamiento en caché, pero dado que DbContext no es seguro para subprocesos y cada solicitud web se ejecuta en su propio subproceso, un DbContext por solicitud es el más largo que puede conservarlo.
Por lo tanto, la recomendación del equipo de EF sobre el uso de 1 Db Context por solicitud se basa claramente en el hecho de que en una aplicación web, UnitOfWork probablemente estará dentro de una solicitud y esa solicitud tiene un hilo. Entonces, un DbContext por solicitud es como el beneficio ideal de UnitOfWork and Caching.
Pero en muchos casos esto no es cierto. Considero registrar un UnitOfWork separado, por lo tanto, tener un nuevo DbContext para el registro posterior a la solicitud en subprocesos asíncronos es completamente aceptable
Entonces, finalmente, resulta que la vida útil de un DbContext está restringida a estos dos parámetros. UnitOfWork and Thread
fuente
Ni una sola respuesta aquí realmente responde la pregunta. El OP no preguntó sobre un diseño DbContext de singleton / por aplicación, preguntó sobre un diseño de solicitud por (web) y qué beneficios potenciales podrían existir.
Haré referencia a http://mehdi.me/ambient-dbcontext-in-ef6/ ya que Mehdi es un recurso fantástico:
Tenga en cuenta que también hay inconvenientes. Ese enlace contiene muchos otros recursos para leer sobre el tema.
Simplemente publique esto en caso de que alguien más tropiece con esta pregunta y no se vea absorbido por las respuestas que en realidad no abordan la pregunta.
fuente
Estoy bastante seguro de que es porque el DbContext no es seguro para subprocesos. Entonces compartir la cosa nunca es una buena idea.
fuente
Una cosa que no se aborda realmente en la pregunta o la discusión es el hecho de que DbContext no puede cancelar los cambios. Puede enviar cambios, pero no puede borrar el árbol de cambios, por lo que si utiliza un contexto por solicitud, no tendrá suerte si necesita descartar los cambios por cualquier motivo.
Personalmente, creo instancias de DbContext cuando es necesario, generalmente conectado a componentes empresariales que tienen la capacidad de recrear el contexto si es necesario. De esa manera tengo control sobre el proceso, en lugar de tener una sola instancia forzada sobre mí. Tampoco tengo que crear el DbContext en cada inicio del controlador, independientemente de si realmente se usa. Luego, si todavía quiero tener instancias por solicitud, puedo crearlas en el CTOR (a través de DI o manualmente) o crearlas según sea necesario en cada método de controlador. Personalmente, generalmente adopto el último enfoque para evitar crear instancias de DbContext cuando en realidad no son necesarias.
Depende desde qué ángulo lo mires también. Para mí, la instancia por solicitud nunca ha tenido sentido. ¿El DbContext realmente pertenece a la solicitud Http? En términos de comportamiento, ese es el lugar equivocado. Los componentes de su empresa deberían estar creando su contexto, no la solicitud Http. Luego puede crear o desechar los componentes de su negocio según sea necesario y nunca preocuparse por la duración del contexto.
fuente
Estoy de acuerdo con opiniones anteriores. Es bueno decir que si va a compartir DbContext en la aplicación de un solo hilo, necesitará más memoria. Por ejemplo, mi aplicación web en Azure (una instancia extra pequeña) necesita otros 150 MB de memoria y tengo alrededor de 30 usuarios por hora.
Aquí hay una imagen de ejemplo real: la aplicación se implementó a las 12 p.m.
fuente
Lo que me gusta es que alinea la unidad de trabajo (como lo ve el usuario, es decir, el envío de una página) con la unidad de trabajo en el sentido ORM.
Por lo tanto, puede hacer que el envío completo de la página sea transaccional, lo que no podría hacer si estuviera exponiendo métodos CRUD con cada uno creando un nuevo contexto.
fuente
Otra razón subestimada para no utilizar un DbContext singleton, incluso en una aplicación de usuario único con un único subproceso, se debe al patrón de mapa de identidad que utiliza. Significa que cada vez que recupere datos mediante una consulta o por id, mantendrá las instancias de entidad recuperadas en la memoria caché. La próxima vez que recupere la misma entidad, le dará la instancia en caché de la entidad, si está disponible, con cualquier modificación que haya realizado en la misma sesión. Esto es necesario para que el método SaveChanges no termine con múltiples instancias de entidad diferentes de los mismos registros de la base de datos; de lo contrario, el contexto tendría que fusionar de alguna manera los datos de todas esas instancias de entidad.
La razón por la cual es un problema es que un DbContext único puede convertirse en una bomba de tiempo que eventualmente podría almacenar en caché toda la base de datos + la sobrecarga de objetos .NET en la memoria.
Hay formas de evitar este comportamiento utilizando solo consultas de Linq con el
.NoTracking()
método de extensión. También en estos días las PC tienen mucha RAM. Pero generalmente ese no es el comportamiento deseado.fuente
Otro problema a tener en cuenta con Entity Framework específicamente es cuando se usa una combinación de crear nuevas entidades, carga diferida y luego usar esas nuevas entidades (del mismo contexto). Si no usa IDbSet.Create (frente a un nuevo), la carga diferida en esa entidad no funciona cuando se recupera del contexto en el que se creó. Ejemplo:
fuente