asp.net entity - Un DbContext por solicitud web...¿por qué?




framework core (9)

He estado leyendo muchos artículos que explican cómo configurar el DbContext Entity Framework para que solo se cree y se use uno por solicitud web HTTP utilizando varios marcos DI.

¿Por qué es una buena idea en primer lugar? ¿Qué ventajas obtienes al utilizar este enfoque? ¿Hay ciertas situaciones donde esta sería una buena idea? ¿Hay cosas que puede hacer usando esta técnica que no puede hacer cuando se DbContext instancia de DbContext por el método del repositorio?


Answers

Lo que me gusta de esto es que alinea la unidad de trabajo (como lo ve el usuario, es decir, un envío de página) con la unidad de trabajo en el sentido de ORM.

Por lo tanto, puede hacer que todo el envío de la página sea transaccional, lo que no podría hacer si estuviera exponiendo los métodos CRUD al crear un nuevo contexto.


Ni una sola respuesta aquí responde la pregunta. El OP no preguntó por un diseño de DbContext de singleton / per-application, preguntó por un diseño de solicitud por (web) y qué beneficios potenciales podrían existir.

Me referiré a 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, DbContext primero intentará recuperarla de su caché de primer nivel antes de establecer la consulta de forma predeterminada en la base de datos. Dependiendo de su patrón de consulta de datos, reutilizar el mismo DbContext en múltiples transacciones comerciales secuenciales puede hacer que se realicen menos consultas en la base de datos gracias al caché de primer nivel de DbContext.

Permite la carga perezosa.

Si sus servicios devuelven entidades persistentes (en lugar de devolver modelos de vista u otros tipos de DTO) y le gustaría aprovechar la carga lenta en esas entidades, la vida útil de la instancia 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 perezosa las propiedades en las entidades devueltas fallaría (si usar una carga perezosa o no es una buena idea es un debate completamente diferente en el que no vamos a entrar) aquí). En nuestro ejemplo de aplicación web, la carga diferida normalmente se usaría en los métodos de acción del controlador en entidades devueltas por una capa de servicio independiente. En ese caso, la instancia 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 el método de acción se haya completado).

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

Solo publico esto en caso de que alguien más se tropiece con esta pregunta y no se absorba en respuestas que en realidad no responden a la pregunta.


NOTA: Esta respuesta habla sobre el DbContext del Entity Framework, pero es aplicable a cualquier tipo de implementación de Unidad de Trabajo, como LINQ al DataContext de SQL, y ISession de NHibernate.

Comencemos haciendo eco de Ian: Tener una sola 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 hilo y una base de datos que solo utiliza esa instancia de la aplicación. DbContext no es seguro para subprocesos y, dado que DbContext almacena datos en caché, se vuelve obsoleto muy pronto. Esto le causará todo tipo de problemas cuando múltiples 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 quiero 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 único DbContext o incluso en el contexto por subproceso, es malo, lea esta respuesta ).

Permítanme comenzar diciendo que el registro de un DbContext como transitorio podría funcionar, pero normalmente desea tener una instancia única 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 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 tener un conjunto de operaciones operativas dentro del mismo contexto, en ese caso el estilo de vida transitorio está bien, pero hay algunas cosas que debe observar:

  • Dado que cada objeto tiene su propia instancia, cada clase que cambia el estado del sistema debe llamar a _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 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, necesita volver a cargarlas por ID, lo que también podría causar problemas de rendimiento.
  • Dado que DbContext implementa IDisposable , es probable que aún desee desechar todas las instancias creadas. Si quieres hacer esto, básicamente tienes dos opciones. Debe disponer de ellos en el mismo método justo después de llamar a 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 aún necesita algún tipo de alcance para que el contenedor sepa cuándo deben desecharse esas instancias.

Otra opción es no inyectar un DbContext en absoluto. En su lugar, inyecta un DbContextFactory que puede crear una nueva instancia (solía usar este enfoque en el pasado). De esta manera la lógica de negocio 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 administra la vida de DbContext explícitamente y es fácil de configurar. También le permite usar un solo contexto en un cierto alcance, lo que tiene claras ventajas, como ejecutar el código en una sola transacción comercial, y poder pasar entidades, ya que se originan en el mismo DbContext .

El inconveniente es que tendrá que pasar el DbContext del método al método (que se denomina Inyección del método). Tenga en cuenta que, en cierto sentido, esta solución es la misma que la del enfoque de "ámbito", pero ahora el alcance se controla en el propio código de la aplicación (y posiblemente se repita muchas veces). Es la aplicación la que se encarga de crear y disponer la unidad de trabajo. Dado que el 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 usted debe aplazar la Inyección del Método cuando necesita pasar el contexto de una clase a la otra.

La inyección de métodos no es tan mala, pero cuando la lógica de negocios se vuelve más compleja y más clases se involucran, 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). Para una aplicación simple, este enfoque funcionará bien sin embargo.

Debido a las desventajas que tiene este enfoque de fábrica para sistemas más grandes, otro enfoque puede ser útil y es aquel en el que permite que el contenedor o el código de infraestructura / Raíz de composición gestione la unidad de trabajo. Este es el estilo del que trata 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 desechar 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, ¿cometió y dispuso la instancia?

La disposició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 momento de la aplicación, simplemente no puede determinar con seguridad si la unidad de trabajo debería comprometerse. por ejemplo, si el código de la capa empresarial arrojó una excepción que se detectó más arriba en la pila de llamadas, definitivamente no querrá Comprometerse.

La solución real es, de nuevo, 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<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 asegura que solo necesita 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> implementaciones de ICommandHandler<T> manera coherente.


Otro problema a tener en cuenta específicamente con Entity Framework es cuando se utiliza una combinación de creación de nuevas entidades, carga diferida y, a continuación, se utilizan esas nuevas entidades (desde el mismo contexto). Si no usa IDbSet.Create (vs solo 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.

Estoy de acuerdo con las opiniones anteriores. Es bueno decir que si va a compartir DbContext en una 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 aproximadamente 30 usuarios por hora.

Aquí está la imagen de ejemplo real: la aplicación se ha implementado en 12PM


Otra razón subestimada para no usar un DbContext de singleton, incluso en una aplicación de un solo usuario de un solo hilo, es debido al patrón de mapa de identidad que utiliza. Esto significa que cada vez que recupere datos utilizando la consulta o por id, mantendrá las instancias de la entidad recuperada en caché. La próxima vez que recupere la misma entidad, le dará la instancia almacenada 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 varias instancias de entidad diferentes de los mismos registros de base de datos; de lo contrario, el contexto tendría que combinar de alguna manera los datos de todas esas instancias de entidad.

La razón por la que es un problema es que un DbContext de singleton puede convertirse en una bomba de tiempo que eventualmente podría almacenar en caché toda la base de datos + la sobrecarga de los objetos .NET en la memoria.

Hay formas de evitar este comportamiento usando solo consultas Linq con el método de extensión .NoTracking() . También en estos días las PC tienen mucha memoria RAM. Pero por lo general ese no es el comportamiento deseado.


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

  1. Una recomendación es "Desechar DbContexts lo antes posible" porque tener un DbContext Alive ocupa recursos valiosos como conexiones de db, etc.
  2. Los otros estados que One DbContext por solicitud es altamente recomendado

Esos se contradicen entre sí porque si su Solicitud está haciendo un montón de cosas no relacionadas con la Db, entonces su DbContext se mantiene sin ninguna razón. Por lo tanto, es un desperdicio mantener su DbContext vivo mientras su solicitud solo está esperando que se realicen cosas al azar ...

Así que 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 de modo X * DbContext por solicitud

Solo obtienen sus datos y disponen el contexto lo antes posible. Esto es considerado por MUCHAS personas una práctica aceptable. Si bien esto tiene los beneficios de ocupar sus recursos de db por el tiempo mínimo, sacrifica claramente todos los dulces de UnitOfWork y Caching que EF tiene para ofrecer.

Mantener vivo 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 mantener.

Por lo tanto, la recomendación del equipo de EF sobre el uso de 1 Db Contexto 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 tiene un hilo. Entonces, un DbContext por solicitud es como el beneficio ideal de UnitOfWork y Caching.

Pero en muchos casos esto no es cierto. Considero que registrar un UnitOfWork por separado, por lo tanto tener un nuevo DbContext para el registro posterior a la solicitud en subprocesos asíncronos es completamente aceptable

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


Una cosa que realmente no se aborda 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 usa un contexto por solicitud no tiene suerte si necesita deshacerse de los cambios por cualquier motivo.

Personalmente, creo instancias de DbContext cuando sea necesario, generalmente adjunto a componentes de negocios 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 utiliza. 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, suelo adoptar este último enfoque para evitar la creación de instancias de DbContext cuando en realidad no son necesarias.

Depende de qué ángulo lo mires también. Para mí, la instancia por solicitud nunca ha tenido sentido. ¿Pertenece realmente el DbContext a la solicitud de HTTP? En términos de comportamiento es el lugar equivocado. Los componentes de su negocio deben 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 vida útil del contexto.


Si está utilizando ghostscript desde un objeto de shell (es decir, ejecutando una línea de comando para procesar el archivo), no será atrapado por problemas de subprocesamiento porque cada instancia que se ejecute lo hará en un proceso diferente en el servidor. Donde debe tener cuidado es cuando tiene un dll que está usando desde C # para procesar el PDF, ese código debería estar sincronizado para evitar que dos subprocesos ejecuten el mismo código al mismo tiempo.





asp.net entity-framework dependency-injection inversion-of-control dbcontext