Saltar al contenido

Un DbContext por solicitud web … ¿por qué?

Este equipo de trabajo ha estado mucho tiempo buscando soluciones a tus preguntas, te ofrecemos la resolución y nuestro objetivo es servirte de mucha apoyo.

Solución:

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.

Empecemos repitiendo a Ian: Tener un solo 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 es utilizada únicamente por esa única instancia de aplicación. los DbContext no es seguro para subprocesos y, dado que el DbContext almacena datos en caché, se vuelve obsoleto muy pronto. Esto le traerá 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) del DbContext en cualquiera que lo necesite. (para obtener más información sobre por qué un solo DbContext -o incluso en el contexto por hilo- es malo, lea esta respuesta).

Permítanme comenzar diciendo que registrar un DbContext como transitorio podría funcionar, pero normalmente desea tener una sola 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 todo un conjunto 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 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 obtiene 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 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 [loaded and saved by a DbContext] nunca abandone el alcance de dicha clase, porque no se pueden usar 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.
  • Ya que DbContext implementos IDisposable, probablemente aún desee Eliminar todas las instancias creadas. Si desea hacer esto, básicamente tiene dos opciones. Debe deshacerse de ellos en el mismo método inmediatamente después de llamar context.SaveChanges(), pero en ese caso, la lógica empresarial se apropia 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 esas instancias deben eliminarse.

Otra opcion es no inyectar un DbContext en absoluto. En su lugar, inyecta un DbContextFactory que es capaz de crear una nueva instancia (solía usar este enfoque en el pasado). De esta forma, la lógica empresarial controla el contexto de forma explícita. 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 administras la vida del DbContext explícitamente y es fácil de configurar. También permite utilizar un único contexto en un determinado alcance, lo que tiene claras ventajas, como ejecutar código en una única transacción comercial, y poder pasar entidades, ya que se originan en la misma. DbContext.

La desventaja es que tendrás que sortear el DbContext de método a método (que se denomina inyección de método). Tenga en cuenta que, en cierto sentido, esta solución es la misma que el enfoque ‘con alcance’, pero ahora el alcance se controla en el código de la aplicación en sí (y posiblemente se repita muchas veces). Es la aplicación que se encarga de crear y disponer de la unidad de trabajo. Desde el DbContext se crea después de que se construye el gráfico de dependencia, Constructor Injection está fuera de la imagen y debe diferir a Method Injection cuando necesite pasar el contexto de una clase a otra.

La inyección de métodos no es tan mala, pero cuando la lógica empresarial 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 ese es el que permite que el contenedor o el código de infraestructura / Raíz de composición administre la unidad de trabajo. Este es el estilo sobre el que trata tu 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 empresarial simple y limpia (solo una responsabilidad única). Hay algunas dificultades con este enfoque. Por ejemplo, ¿confirmó y eliminó la instancia?

La eliminación de una unidad de trabajo se puede realizar al final de la solicitud web. Sin embargo, mucha gente incorrectamente Asuma 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 realmente debería estar comprometida. Por ejemplo, si el código de la capa empresarial arrojó una excepción que se capturó más arriba en la pila de llamadas, definitivamente no quiere comprometerse.

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. Al abstraer toda la lógica empresarial 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
    : ICommandHandler

    readonly DbContext context;
    readonly ICommandHandler decorated;

    public TransactionCommandHandlerDecorator(
        DbContext context,
        ICommandHandler decorated)
    
        this.context = context;
        this.decorated = decorated;
    

    public void Handle(TCommand command)
    
        this.decorated.Handle(command);

        context.SaveChanges();
     

Esto asegura que solo necesite escribir este código de infraestructura una vez. Cualquier contenedor DI sólido le permite configurar tal decorador para que se envuelva alrededor de todos ICommandHandler implementaciones de manera consistente.

Hay dos contradecir recomendaciones de microsoft y mucha gente usa DbContexts de una manera completamente divergente.

  1. Una recomendación es “Elimine DbContexts lo antes posible”
    porque tener un DbContext Alive ocupa recursos valiosos como conexiones de base de datos, etc.
  2. El otro dice que Se recomienda encarecidamente un DbContext por solicitud

Esos se contradicen entre sí porque si su Solicitud no está relacionada 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 está esperando que se hagan cosas al azar …

Tanta gente que sigue regla 1 tienen sus DbContexts dentro de sus “Patrón de repositorio” y crear una nueva instancia por consulta de base de datos asi que X * DbContext por solicitud

Simplemente obtienen sus datos y eliminan el contexto lo antes posible. Esto es considerado por MUCHOS personas una práctica aceptable. Si bien esto tiene los beneficios de ocupar los recursos de su base de datos durante el tiempo mínimo, claramente sacrifica todos los UnitOfWork y Almacenamiento en caché dulces que EF tiene para ofrecer.

Manteniendo vivo un soltero de múltiples fines instancia de DbContext maximiza los beneficios de Almacenamiento en caché pero como DbContext es no seguro para subprocesos y cada solicitud web se ejecuta en su propio hilo, un DbContext por solicitud es el mas largo puedes quedártelo.

Entonces, la recomendación del equipo de EF sobre el uso de 1 contexto de Db por solicitud se basa claramente en el hecho de que en una aplicación web, es muy probable que UnitOfWork esté dentro de una solicitud y esa solicitud tenga un hilo. Entonces, un DbContext por solicitud es como el beneficio ideal de UnitOfWork y Caching.

Pero en muchos casos esto no es true. Yo considero Inicio sesión un UnitOfWork separado que, por lo tanto, tiene un nuevo DbContext para el inicio de sesión posterior a la solicitud subprocesos asíncronos es completamente aceptable

Entonces, finalmente, rechaza que la vida útil de un DbContext está restringida a estos dos parámetros. UnitOfWork y Hilo

Ni una sola respuesta aquí responde realmente a la pregunta. El OP no preguntó sobre un diseño de DbContext 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 una caché de primer nivel de todas las entidades que carga desde la base de datos. Siempre que consulte una entidad por su principal key, DbContext primero intentará recuperarlo de su caché de primer nivel antes de consultarlo desde la base de datos de forma predeterminada. Dependiendo de su patrón de consulta de datos, reutilizar el mismo DbContext en múltiples transacciones comerciales secuenciales puede resultar en que se realicen menos consultas a la base de datos gracias a la caché de primer nivel de DbContext.

Permite la carga diferida.

Si sus servicios devuelven entidades persistentes (en lugar de devolver modelos de vista u otros tipos 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 de forma diferida las propiedades de las entidades devueltas fallaría (si usar la carga diferida es una buena idea o no es un debate completamente diferente en el que no entraremos aquí). En nuestro ejemplo de aplicación web, la carga diferida se usaría normalmente en los métodos de acción del controlador en entidades devueltas por una capa de servicio separada. En ese caso, la instancia de DbContext que fue utilizada por el método de servicio para cargar estas entidades debería permanecer activa durante la duración de la solicitud web (o al menos hasta que se complete el método de acción).

Tenga en cuenta que también hay contras. Ese enlace contiene muchos otros recursos para leer sobre el tema.

Solo publique esto en caso de que alguien más se tope con esta pregunta y no se absorba en respuestas que en realidad no abordan la pregunta.

Te mostramos las comentarios y valoraciones de los lectores

Eres capaz de añadir valor a nuestra información participando con tu experiencia en las anotaciones.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *