asp.net - netcore - services adddbcontext asp net core




Un DbContext por solicitud web... ¿por qué? (6)

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?


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.


Estoy bastante seguro de que es porque DbContext no es seguro para todos los subprocesos. Así que compartir la cosa nunca es una buena idea.


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.


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


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.


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.




dbcontext