Un DbContext por solicitud web ... ¿por qué?

398

He estado leyendo muchos artículos que explican cómo configurar Entity Framework DbContextpara 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 DbContexts por llamada al método de repositorio?

Andrés
fuente
99
Gueddari en mehdi.me/ambient-dbcontext-in-ef6 llama a la instancia de DbContext por método de repositorio llamado antipatrón. Cita: "Al hacer esto, está perdiendo casi todas las características que Entity Framework proporciona a través de DbContext, incluida su caché de primer nivel, su mapa de identidad, su unidad de trabajo y sus capacidades de seguimiento de cambios y carga lenta ". Excelente artículo con excelentes sugerencias para manejar el ciclo de vida de DBContexts. Definitivamente vale la pena leerlo.
Christoph

Respuestas:

565

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 DataContexty NHibernate ISession.

Comencemos repitiendo Ian: Tener un single DbContextpara 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 DbContextes seguro para subprocesos y, como los DbContextdatos 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) DbContexten 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 DbContexttransitorio 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 DbContextimplementa 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 DbContexten absoluto. En cambio, inyecta una DbContextFactoryque 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 DbContextexplí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 DbContextde 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 DbContextse 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.

Steven
fuente
2
Wow, gracias por la respuesta completa. Si pudiera votar dos veces, lo haría. Arriba, usted dice "... sin intención de permitir que un conjunto completo de operaciones opere dentro del mismo contexto, en ese caso el estilo de vida transitorio está bien ...". ¿Qué quieres decir con "transitorio", específicamente?
Andrew
14
@Andrew: 'Transitorio' es un concepto de Inyección de dependencia, lo que significa que si un servicio está configurado para ser transitorio, se crea una nueva instancia del servicio cada vez que se inyecta en un consumidor.
Steven
1
@ user981375: para las operaciones CRUD, puede crear un genérico CreateCommand<TEnity>y otro genérico CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>(y hacer lo mismo para Actualizar, Eliminar y tener una sola GetByIdQuery<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.
Steven
3
+1 ¿Creería que escribí toda esta respuesta antes de leer esto? Por cierto, creo que es importante que hablen sobre la eliminación del DbContext al final (aunque es genial que se
mantengan
1
Pero no pasa el contexto a la clase decorada, ¿cómo podría funcionar la clase decorada con el mismo contexto que pasó a la clase TransactionCommandHandlerDecorator? por ejemplo, si la clase decorada es InsertCommandHandlerclase, ¿cómo podría registrar la operación de inserción en el contexto (DbContext en EF)?
Masoud
35

Hay dos recomendaciones contradictorias de Microsoft y muchas personas usan DbContexts de una manera completamente divergente.

  1. Una recomendación es "Eliminar DbContexts lo antes posible" porque tener un DbContext Alive ocupa recursos valiosos como conexiones db, etc.
  2. El otro afirma que One DbContext por solicitud es muy recomendable

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

Anestis Kivranoglou
fuente
3
Para ser justos, sus solicitudes HTTP deberían estar terminando bastante rápido (unos pocos ms). Si van más tiempo que eso, entonces es posible que desee pensar en hacer un procesamiento en segundo plano con algo como un programador de trabajos externo para que la solicitud pueda regresar de inmediato. Dicho esto, tu arquitectura tampoco debería depender realmente de HTTP. En general, una buena respuesta sin embargo.
aplastar
34

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:

Posibles ganancias de rendimiento.

Cada instancia de DbContext mantiene un caché de primer nivel de todas las entidades que carga desde la base de datos. Cada vez que consulta una entidad por su clave principal, el DbContext primero intentará recuperarla de su caché de primer nivel antes de consultarla desde la base de datos. Dependiendo de su patrón de consulta de datos, la reutilización del mismo DbContext en múltiples transacciones comerciales secuenciales puede dar como resultado que se realicen menos consultas de la base de datos gracias al caché de primer nivel DbContext.

Permite la carga diferida.

Si sus servicios devuelven entidades persistentes (en lugar de devolver modelos de vista u otro tipo de DTO) y desea aprovechar la carga diferida en esas entidades, la vida útil de la instancia de DbContext de la que se recuperaron esas entidades debe extenderse más allá El alcance de la transacción comercial. Si el método de servicio eliminó la instancia de DbContext que usó antes de regresar, cualquier intento de cargar con pereza las propiedades en las entidades devueltas fallaría (si usar o no la carga diferida es una buena idea, es un debate completamente diferente en el que no entraremos aquí). En nuestro ejemplo de aplicación web, la carga diferida normalmente se usaría en métodos de acción del controlador en entidades devueltas por una capa de servicio separada. En ese caso,

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.

usuario4893106
fuente
Buen enlace! La gestión explícita del DBContext parece el enfoque más seguro.
aggsol
22

Estoy bastante seguro de que es porque el DbContext no es seguro para subprocesos. Entonces compartir la cosa nunca es una buena idea.

Ian
fuente
¿Quiere decir que compartirlo a través de solicitudes HTTP nunca es una buena idea?
Andrew
2
Sí Andrew, eso es lo que quiso decir. Compartir el contexto es solo para aplicaciones de escritorio de un solo hilo.
Elisabeth
10
¿Qué hay de compartir el contexto para una solicitud? Entonces, ¿para una solicitud podemos tener acceso a diferentes repositorios y realizar una transacción a través de ellos compartiendo el mismo contexto?
Lyubomir Velchev
16

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.

Rick Strahl
fuente
1
Esta es una respuesta interesante y estoy parcialmente de acuerdo con usted. Para mí, un DbContext no tiene que estar vinculado a una solicitud web, pero siempre se escribe en una sola 'solicitud' como en: 'transacción comercial'. Y cuando vincula el contexto a una transacción comercial, la cancelación de cambios se vuelve realmente extraña. Pero no tenerlo en el límite de solicitud web no significa que los componentes del negocio (BC) deberían estar creando el contexto; Creo que esa no es su responsabilidad. En cambio, puede aplicar el alcance utilizando decoradores alrededor de sus BC. De esta manera, incluso puede cambiar el alcance sin ningún cambio de código.
Steven
1
Bueno, en ese caso, la inyección en el objeto comercial debería ocuparse de la gestión de por vida. En mi opinión, el objeto comercial posee el contexto y, como tal, debe controlar la vida útil.
Rick Strahl el
En resumen, ¿qué quiere decir cuando dice "la capacidad de recrear el contexto si es necesario"? ¿Estás rodando tu propia habilidad de retroceso? puedes elaborar un poco?
tntwyckoff
2
Personalmente, creo que es un poco problemático forzar un DbContext al comienzo allí. No hay garantía de que incluso necesite acceder a la base de datos. Tal vez está llamando a un servicio de terceros que cambia de estado en ese lado. O tal vez en realidad tiene 2 o 3 bases de datos con las que está trabajando al mismo tiempo. No crearía un montón de DbContexts al principio en caso de que termine usándolos. La empresa conoce los datos con los que está trabajando, por lo que pertenece a eso. Simplemente coloque un TransactionScope al inicio si es necesario. No creo que todas las llamadas necesiten una. Se necesita recursos.
Daniel Lorenz
Esa es la cuestión de si permite que el contenedor controle la vida útil del dbcontext que luego controla la vida útil de los controles principales, a veces de manera indebida. Digamos que si quiero que se inyecte un singleton de servicio simple en mis controladores, entonces no podré usar la inyección de constuctor debido a una solicitud semántica.
davidcarr
10

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. Aplicación que comparte DBContext en solicitud HTTP

Aquí hay una imagen de ejemplo real: la aplicación se implementó a las 12 p.m.

Miroslav Holec
fuente
Posiblemente, la idea es compartir el contexto de una solicitud. Si accedemos a diferentes repositorios y - clases DBSet y queremos que las operaciones con ellos sean transaccionales, esa debería ser una buena solución. Eche un vistazo al proyecto de código abierto mvcforum.com . Creo que eso se hace en su implementación del patrón de diseño de Unit Of Work.
Lyubomir Velchev
3

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.

RB.
fuente
3

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.

Dmitry S.
fuente
Esto es correcto, pero debe suponer que el recolector de basura funcionará, haciendo que este problema sea más virtual que real.
tocqueville
3
El recolector de basura no va a recolectar ninguna instancia de objeto en poder de un objeto estático / singleton activo. Terminarán en la generación 2 del montón.
Dmitry S.
1

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:

 public class Foo {
     public string Id {get; set; }
     public string BarId {get; set; }
     // lazy loaded relationship to bar
     public virtual Bar Bar { get; set;}
 }
 var foo = new Foo {
     Id = "foo id"
     BarId = "some existing bar id"
 };
 dbContext.Set<Foo>().Add(foo);
 dbContext.SaveChanges();

 // some other code, using the same context
 var foo = dbContext.Set<Foo>().Find("foo id");
 var barProp = foo.Bar.SomeBarProp; // fails with null reference even though we have BarId set.
Ted Elliott
fuente