NOTA: Esta respuesta habla sobre el Entity Framework DbContext
, pero es aplicable a cualquier tipo de implementación de Unidad de Trabajo, como LINQ to SQL DataContext
y NHibernate ISession
.
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. No DbContext
es seguro para subprocesos y, como los DbContext
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 solo DbContext
-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:
- Como cada objeto tiene su propia instancia, cada clase que cambia el estado del sistema debe llamar
_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 .
- Debe asegurarse de que las entidades [cargadas y guardadas por un
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.
- Dado que se
DbContext
implementa IDisposable
, 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 llamar context.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 una DbContextFactory
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í:
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 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 misma DbContext
.
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. Como DbContext
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:
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 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.