Un DbContext por solicitud web... ¿por qué?
He estado leyendo muchos artículos que explican cómo configurar Entity Framework DbContext
para que solo se cree y utilice uno por solicitud web HTTP utilizando varios marcos DI.
¿Por qué es esta una buena idea en primer lugar? ¿Qué ventajas obtiene al utilizar este enfoque? ¿Existen determinadas situaciones en las que esto sería una buena idea? ¿Hay cosas que puedes hacer usando esta técnica que no puedes hacer al crear una instancia DbContext
de s por llamada al método del repositorio?
NOTA: Esta respuesta habla sobre Entity Framework
DbContext
, pero es aplicable a cualquier tipo de implementación de Unidad de trabajo, como LINQ to SQLDataContext
y NHibernateISession
.
Comencemos haciéndonos eco de Ian: tener una aplicación única 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 un solo subproceso y una base de datos que utiliza únicamente esa única instancia de aplicación. No DbContext
es seguro para subprocesos y, dado que los DbContext
datos se almacenan en caché, se vuelve obsoleto muy pronto. Esto le causará 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 quieras saber por qué no inyectar una nueva instancia (es decir, con un estilo de vida transitorio) a DbContext
cualquiera que la necesite. (Para obtener más información sobre por qué un solo contexto DbContext
, o incluso un contexto por hilo, es malo, lea esta respuesta ).
Permítanme comenzar diciendo que registrar un elemento DbContext
como transitorio podría funcionar, pero normalmente desea tener una única instancia de dicha unidad de trabajo dentro de un determinado alcance. En una aplicación web, puede resultar 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 permitir que un conjunto completo de objetos opere dentro del mismo contexto. Es decir, operan dentro de la misma transacción comercial.
Si no tiene el objetivo de que un conjunto de operaciones operen dentro del mismo contexto, en ese caso el estilo de vida transitorio está bien, pero hay algunas cosas a tener en cuenta:
- Dado que cada objeto tiene su propia instancia, cada clase que cambia el estado del sistema necesita llamar
_context.SaveChanges()
(de lo contrario, los cambios se perderían). Esto puede complicar su código y agregar una segunda responsabilidad al código (la responsabilidad de controlar el contexto) y es una violación del Principio de Responsabilidad Única . - Debe asegurarse de que las entidades [cargadas y guardadas por
DbContext
] nunca abandonen el alcance de dicha clase, porque no se pueden usar en la instancia de contexto de otra clase. Esto puede complicar enormemente tu código, porque cuando necesitas esas entidades, necesitas cargarlas nuevamente por id, lo que también podría causar problemas de rendimiento. - Dado que
DbContext
se implementaIDisposable
, probablemente aún desee deshacerse de todas las instancias creadas. Si quieres hacer esto, básicamente tienes dos opciones. Debe eliminarlos con el mismo método inmediatamente después de llamarloscontext.SaveChanges()
, pero en ese caso la lógica empresarial toma posesión de un objeto que se transmite desde el exterior. La segunda opción es Eliminar todas las instancias creadas en el límite de la Solicitud Http, pero en ese caso aún necesita algún tipo de alcance para que el contenedor sepa cuándo es necesario eliminar esas instancias.
Otra opción es no inyectar DbContext
nada. En su lugar, inyecta un DbContextFactory
que sea capaz de crear una nueva instancia (solía usar este enfoque en el pasado). De esta forma, la lógica empresarial controla el contexto explícitamente. Si podría verse así:
public void SomeOperation()
{
using (var context = this.contextFactory.CreateNew())
{
var entities = this.otherDependency.Operate(
context, "some value");
context.Entities.InsertOnSubmit(entities);
context.SaveChanges();
}
}
El lado positivo de esto es que usted administra la vida de forma DbContext
explícita y es fácil de configurar. También permite utilizar un contexto único en un determinado ámbito, lo que tiene claras ventajas, como ejecutar código en una sola transacción comercial y poder pasar entidades, ya que se originan en la misma DbContext
.
La desventaja es que tendrá que pasar de un DbContext
método a otro (lo que se denomina inyección de método). Tenga en cuenta que, en cierto sentido, esta solución es la misma que el enfoque de 'alcance', pero ahora el alcance se controla en el código de la aplicación (y posiblemente se repita muchas veces). Es la aplicación la que se encarga de crear y disponer de la unidad de trabajo. Dado que se DbContext
crea después de construir el gráfico de dependencia, la inyección de constructor está fuera de escena y debe diferir la inyección de 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ás que pasarla de método a método y de clase a clase, lo que puede complicar mucho el código (he visto esto en el pasado). Sin embargo, para una aplicación sencilla, este enfoque funcionará bien.
Debido a las desventajas que este enfoque de fábrica tiene para sistemas más grandes, otro enfoque puede ser útil y es aquel en el que se permite que el contenedor o el código de infraestructura/ Composition Root administre la unidad de trabajo. Este es el estilo al que se refiere tu pregunta.
Al permitir que el contenedor y/o la infraestructura se encarguen de 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 empresarial simple y limpia (solo una responsabilidad única). Hay algunas dificultades con este enfoque. Por ejemplo, ¿dónde se confirma y se elimina la instancia?
La eliminación de una unidad de trabajo se puede realizar al finalizar la solicitud web. Sin embargo, muchas personas asumen 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 se puede determinar con seguridad si la unidad de trabajo realmente debe comprometerse. por ejemplo, si el código de la capa empresarial arrojó una excepción que se detectó en una parte superior de la pila de llamadas, definitivamente no querrás confirmar.
La verdadera solución es nuevamente administrar explícitamente algún tipo de alcance, pero esta vez hacerlo dentro de la raíz de composición. Al abstraer 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:
class TransactionalCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand>
{
readonly DbContext context;
readonly ICommandHandler<TCommand> decorated;
public TransactionCommandHandlerDecorator(
DbContext context,
ICommandHandler<TCommand> decorated)
{
this.context = context;
this.decorated = decorated;
}
public void Handle(TCommand command)
{
this.decorated.Handle(command);
context.SaveChanges();
}
}
Esto garantiza que solo necesitará escribir este código de infraestructura una vez. Cualquier contenedor DI sólido le permite configurar dicho decorador para que se ajuste a todas ICommandHandler<T>
las implementaciones de manera consistente.
Hay dos recomendaciones contradictorias de Microsoft y muchas personas usan DbContexts de manera completamente divergente.
- Una recomendación es "Deshacerse de DbContexts lo antes posible" porque tener un DbContext Alive ocupa recursos valiosos como conexiones de base de datos, etc.
- El otro afirma que se recomienda encarecidamente un DbContext por solicitud.
Estos se contradicen entre sí porque si su Solicitud está haciendo muchas cosas no relacionadas con Db, entonces su DbContext se conserva sin ningún motivo. Por lo tanto, es un desperdicio mantener vivo su DbContext mientras su solicitud solo espera que se hagan cosas aleatorias...
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. MUCHAS personas consideran que esto es una práctica aceptable. Si bien esto tiene los beneficios de ocupar los recursos de su base de datos durante el mínimo de tiempo, claramente sacrifica todos los beneficios de UnitOfWork y Caching que EF tiene para ofrecer.
Mantener activa 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 lo más largo que puede conservarse.
Entonces, la recomendación del equipo de EF sobre el uso de 1 Db de contexto por solicitud se basa claramente en el hecho de que en una aplicación web una unidad de trabajo probablemente estará dentro de una solicitud y esa solicitud tiene un hilo. Entonces, un DbContext por solicitud es el beneficio ideal de UnitOfWork y Caching.
Pero en muchos casos esto no es cierto. Considero que registrar una UnitOfWork separada, por lo tanto, tener un nuevo DbContext para el registro posterior a la solicitud en subprocesos asíncronos es completamente aceptable
Finalmente, resulta que la vida útil de un DbContext está restringida a estos dos parámetros. Unidad de trabajo y hilo