pattern - Marco de entidad c#: uso correcto de la clase DBContext dentro de su clase de repositorio
patron repository entity framework c# (10)
Solía implementar mis clases de repositorio como puedes ver a continuación
public Class MyRepository
{
private MyDbContext _context;
public MyRepository(MyDbContext context)
{
_context = context;
}
public Entity GetEntity(Guid id)
{
return _context.Entities.Find(id);
}
}
Sin embargo, recientemente leí este artículo que dice que es una mala práctica tener contexto de datos como miembro privado en su repositorio: http://devproconnections.com/development/solving-net-scalability-problem
Ahora, teóricamente, el artículo es correcto: dado que DbContext implementa IDisposable, la implementación más correcta sería la siguiente.
public Class MyRepository
{
public Entity GetEntity(Guid id)
{
using (MyDbContext context = new MyDBContext())
{
return context.Entities.Find(id);
}
}
}
Sin embargo, de acuerdo con este otro artículo, eliminar DbContext no sería esencial: http://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext.html
¿Cuál de los dos artículos es correcto? Estoy bastante confundido Tener DbContext como miembro privado en su clase de repositorio realmente puede causar "problemas de escalabilidad" como sugiere el primer artículo.
Básicamente, la clase DbContext no es más que un contenedor que maneja todas las cosas relacionadas con la base de datos como: 1. Crear conexión 2. Ejecutar consulta. Ahora, si hacemos lo mencionado anteriormente usando ado.net normal, entonces necesitamos cerrar explícitamente la conexión correctamente, ya sea escribiendo el código en la instrucción de uso o llamando al método close () en el objeto de clase de conexión.
Ahora, como la clase de contexto implementa internamente la interfaz IDisposable, es una buena práctica escribir el dbcontext al usar la instrucción para que no tengamos que preocuparnos por cerrar la conexión.
Gracias.
Creo que no deberías seguir el primer artículo, y te diré por qué.
Siguiendo el primer enfoque, está perdiendo casi todas las características que Entity Framework proporciona a través del
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.
Esto se debe a que en el escenario anterior, se crea una nueva instancia de
DbContext
para cada consulta de base de datos y se elimina inmediatamente después, evitando así que la instancia de
DbContext
pueda rastrear el estado de sus objetos de datos en toda la transacción comercial.
Tener un
DbContext
como propiedad privada en su clase de repositorio también tiene sus problemas.
Creo que el mejor enfoque es tener un CustomDbContextScope.
Este enfoque está muy bien explicado por este tipo: Mehdi El Gueddari
Este artículo http://mehdi.me/ambient-dbcontext-in-ef6/ uno de los mejores artículos sobre EntityFramework que he visto. Debería leerlo por completo, y creo que responderá todas sus preguntas.
Creo que el primer enfoque es mejor, nunca tiene que crear un dbcontext para cada repositorio, incluso si lo dispone. Sin embargo, en el primer caso, puede usar una base de datos Factory para crear una instancia de un solo dbcontext:
public class DatabaseFactory : Disposable, IDatabaseFactory {
private XXDbContext dataContext;
public ISefeViewerDbContext Get() {
return dataContext ?? (dataContext = new XXDbContext());
}
protected override void DisposeCore() {
if (dataContext != null) {
dataContext.Dispose();
}
}
}
Y use esta instancia en el repositorio:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
private IXXDbContext dataContext;
private readonly DbSet<TEntity> dbset;
public Repository(IDatabaseFactory databaseFactory) {
if (databaseFactory == null) {
throw new ArgumentNullException("databaseFactory", "argument is null");
}
DatabaseFactory = databaseFactory;
dbset = DataContext.Set<TEntity>();
}
public ISefeViewerDbContext DataContext {
get { return (dataContext ?? (dataContext = DatabaseFactory.Get()));
}
public virtual TEntity GetById(Guid id){
return dbset.Find(id);
}
....
El enfoque a utilizar depende de la responsabilidad del repositorio.
¿Es responsabilidad del repositorio ejecutar transacciones completas? es decir, para hacer cambios y luego guardar los cambios en la base de datos llamando a `SaveChanges? ¿O es solo una parte de una transacción más grande y, por lo tanto, solo hará cambios sin guardarlos?
Caso # 1) El repositorio ejecutará transacciones completas (realizará cambios y los guardará):
En este caso, el segundo enfoque es mejor (el enfoque de su segundo ejemplo de código).
Solo modificaré este enfoque un poco introduciendo una fábrica como esta:
public interface IFactory<T>
{
T Create();
}
public class Repository : IRepository
{
private IFactory<MyContext> m_Factory;
public Repository(IFactory<MyContext> factory)
{
m_Factory = factory;
}
public void AddCustomer(Customer customer)
{
using (var context = m_Factory.Create())
{
context.Customers.Add(customer);
context.SaveChanges();
}
}
}
Estoy haciendo este ligero cambio para habilitar la inyección de dependencia . Esto nos permite cambiar más tarde la forma en que creamos el contexto.
No quiero que el repositorio tenga la responsabilidad de crear el contexto por sí mismo.
La fábrica que implementa
IFactory<MyContext>
tendrá la responsabilidad de crear el contexto.
Observe cómo el repositorio administra la vida útil del contexto, crea el contexto, realiza algunos cambios, guarda los cambios y luego elimina el contexto. El repositorio tiene una vida útil más larga que el contexto en este caso.
Caso # 2) El repositorio es parte de una transacción más grande (hará algunos cambios, otros repositorios harán otros cambios, y luego alguien más va a confirmar la transacción invocando
SaveChanges
):
En este caso, el primer enfoque (que usted describe primero en su pregunta) es mejor.
Imagine que esto sucede para comprender cómo el repositorio puede ser parte de una transacción más grande:
using(MyContext context = new MyContext ())
{
repository1 = new Repository1(context);
repository1.DoSomething(); //Modify something without saving changes
repository2 = new Repository2(context);
repository2.DoSomething(); //Modify something without saving changes
context.SaveChanges();
}
Tenga en cuenta que se utiliza una nueva instancia del repositorio para cada transacción. Esto significa que la vida útil del repositorio es muy corta.
Tenga en cuenta que estoy actualizando el repositorio en mi código (que es una violación de la inyección de dependencia). Solo estoy mostrando esto como un ejemplo. En código real, podemos usar fábricas para resolver esto.
Ahora, una mejora que podemos hacer a este enfoque es ocultar el contexto detrás de una interfaz para que el repositorio ya no tenga acceso a
SaveChanges
(eche un vistazo al
Principio de segregación de interfaz
).
Puedes tener algo como esto:
public interface IDatabaseContext
{
IDbSet<Customer> Customers { get; }
}
public class MyContext : DbContext, IDatabaseContext
{
public IDbSet<Customer> Customers { get; set; }
}
public class Repository : IRepository
{
private IDatabaseContext m_Context;
public Repository(IDatabaseContext context)
{
m_Context = context;
}
public void AddCustomer(Customer customer)
{
m_Context.Customers.Add(customer);
}
}
Puede agregar otros métodos que necesite a la interfaz si lo desea.
Tenga en cuenta también que esta interfaz no hereda de
IDisposable
.
Lo que significa que la clase
Repository
no es responsable de administrar la vida útil del contexto.
El contexto en este caso tiene una vida útil mayor que el repositorio.
Alguien más administrará la vida útil del contexto.
Notas sobre el primer artículo:
El primer artículo sugiere que no debe usar el primer enfoque que describe en su pregunta (inyecte el contexto en el repositorio).
El artículo no es claro sobre cómo se usa el repositorio. ¿Se usa como parte de una sola transacción? ¿O abarca varias transacciones?
Supongo (no estoy seguro) que, en el enfoque que describe el artículo (negativamente), el repositorio se utiliza como un servicio de larga duración que abarcará muchas transacciones. En este caso estoy de acuerdo con el artículo.
Pero lo que sugiero aquí es diferente, sugiero que este enfoque solo se usa en el caso en que se crea una nueva instancia del repositorio cada vez que se necesita una transacción.
Notas sobre el segundo artículo:
Creo que de lo que habla el segundo artículo es irrelevante para el enfoque que debe utilizar.
El segundo artículo está discutiendo si es necesario disponer del contexto en cualquier caso (irrelevante para el diseño del repositorio).
Tenga en cuenta que en los dos enfoques de diseño, estamos eliminando el contexto. La única diferencia es quién es responsable de dicha eliminación.
El artículo dice que el
DbContext
parece limpiar recursos sin la necesidad de eliminar el contexto explícitamente.
El primer artículo que vinculó se olvidó de precisar una cosa importante: cuál es la vida útil de las instancias llamadas
NonScalableUserRepostory
(también se olvidó de hacer que
NonScalableUserRepostory
implemente
IDisposable
también, para disponer correctamente la instancia
DbContext
).
Imagine el siguiente caso:
public string SomeMethod()
{
using (var myRepository = new NonScalableUserRepostory(someConfigInstance))
{
return myRepository.GetMyString();
}
}
Bueno ... todavía habría algún campo privado
DbContext
dentro de la clase
NonScalableUserRepostory
, pero el contexto solo se usará
una vez
.
Entonces, es exactamente lo mismo que el artículo describe como la mejor práctica.
Entonces, la pregunta no es "¿ debería usar un miembro privado frente a una declaración de uso? ", Sino más bien " ¿cuál debería ser la vida útil de mi contexto? ".
La respuesta sería entonces: tratar de acortarlo tanto como sea posible.
Existe la noción de
Unidad de trabajo
, que representa una operación comercial.
Básicamente, debe tener un nuevo
DbContext
para cada unidad de trabajo.
Cómo se define una unidad de trabajo y cómo se implementa dependerá de la naturaleza de su aplicación;
por ejemplo, para una aplicación ASP.Net MVC, la duración del
DbContext
es generalmente la duración de la
HttpRequest
, es decir, se crea un nuevo contexto cada vez que el usuario genera una nueva solicitud web.
EDITAR:
Para responder a tu comentario:
Una solución sería inyectar a través del constructor un método de fábrica. Aquí hay un ejemplo básico:
public class MyService : IService
{
private readonly IRepositoryFactory repositoryFactory;
public MyService(IRepositoryFactory repositoryFactory)
{
this.repositoryFactory = repositoryFactory;
}
public void BusinessMethod()
{
using (var repo = this.repositoryFactory.Create())
{
// ...
}
}
}
public interface IRepositoryFactory
{
IRepository Create();
}
public interface IRepository : IDisposable
{
//methods
}
El primer código no tiene relación con el problema de escalabilidad, la razón por la que es malo es porque crea un nuevo contexto para cada repositorio que es malo, lo cual comenta uno de los comentaristas pero ni siquiera respondió. En la web era 1 solicitud 1 dbContext, si planea usar un patrón de repositorio, entonces se traduce en 1 solicitud> muchos repositorios> 1 dbContext. Esto es fácil de lograr con IoC, pero no es necesario. así es como lo haces sin IoC:
var dbContext = new DBContext();
var repository = new UserRepository(dbContext);
var repository2 = new ProductRepository(dbContext);
// do something with repo
En cuanto a la eliminación o no, generalmente lo elimino, pero si el plomo en sí mismo dijo esto, entonces probablemente no haya razón para hacerlo. Solo me gusta deshacerme si tiene IDisposable.
El segundo enfoque (usar) es mejor, ya que seguramente mantiene la conexión solo durante el tiempo mínimo necesario y es más fácil de usar con seguridad de subprocesos.
La regla raíz es: su vida útil de DbContext debe limitarse a la transacción que está ejecutando .
Aquí, "transacción" puede referirse a una consulta de solo lectura o una consulta de escritura. Y como ya sabrá, una transacción debe ser lo más breve posible.
Dicho esto, diría que debe favorecer la forma de "usar" en la mayoría de los casos y no usar un miembro privado.
El único caso que puedo ver para usar un miembro privado es para un patrón CQRS (CQRS: un examen cruzado de cómo funciona) .
Por cierto, la respuesta de Diego Vega en la http://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext.html también da algunos consejos sabios:
Hay dos razones principales por las que nuestro código de muestra tiende a usar siempre "usar" o eliminar el contexto de alguna otra manera:
El comportamiento de apertura / cierre automático predeterminado es relativamente fácil de anular: puede asumir el control de cuándo se abre y se cierra la conexión abriendo manualmente la conexión. Una vez que comience a hacer esto en alguna parte de su código, entonces olvidarse de dividir el contexto se vuelve dañino, ya que podría estar perdiendo conexiones abiertas.
DbContext implementa IDiposable siguiendo el patrón recomendado, que incluye exponer un método de eliminación virtual protegido que los tipos derivados pueden anular si, por ejemplo, la necesidad de agregar otros recursos no administrados en la vida útil del contexto.
HTH
Supongamos que tiene más de un repositorio y necesita actualizar 2 registros de diferentes repositorios. Y debe hacerlo transaccional (si uno falla, ambas actualizaciones retroceden):
var repositoryA = GetRepository<ClassA>();
var repositoryB = GetRepository<ClassB>();
repository.Update(entityA);
repository.Update(entityB);
Entonces, si tiene DbContext propio para cada repositorio (caso 2), debe usar TransactionScope para lograr esto.
Mejor manera: tenga un DbContext compartido para una operación (para una llamada, para una unidad de trabajo ). Entonces, DbContext puede administrar transacciones. EF es bastante para eso. Puede crear un solo DbContext, hacer todos los cambios en muchos repositorios, llamar a SaveChanges una vez, desecharlo después de que todas las operaciones y el trabajo estén hechos.
Here hay un ejemplo de implementación del patrón UnitOfWork.
Su segunda forma puede ser buena para operaciones de solo lectura.
Utilizo la primera manera (inyectando el dbContext) Por supuesto, debería ser un IMyDbContext y su motor de inyección de dependencias está administrando el ciclo de vida del contexto, por lo que solo está activo mientras sea necesario.
Esto le permite simular el contexto para la prueba, la segunda forma hace que sea imposible examinar las entidades sin una base de datos para el contexto que se utilizará.