c# - Arquitectura de cebolla, unidad de trabajo y un patrón de repositorio genérico.
.net repository-pattern (3)
Esta es la primera vez que estoy implementando un enfoque de diseño más dominado por el dominio. He decidido probar la arquitectura Onion, ya que se enfoca en el dominio en lugar de en infraestructura / plataformas / etc.
Para abstraerme de Entity Framework, he creado un repositorio genérico con una implementación de Unidad de Trabajo .
Las IRepository<T>
e IUnitOfWork
:
public interface IRepository<T>
{
void Add(T item);
void Remove(T item);
IQueryable<T> Query();
}
public interface IUnitOfWork : IDisposable
{
void SaveChanges();
}
Implementaciones de Entity Framework de IRepository<T>
e IUnitOfWork
:
public class EntityFrameworkRepository<T> : IRepository<T> where T : class
{
private readonly DbSet<T> dbSet;
public EntityFrameworkRepository(IUnitOfWork unitOfWork)
{
var entityFrameworkUnitOfWork = unitOfWork as EntityFrameworkUnitOfWork;
if (entityFrameworkUnitOfWork == null)
{
throw new ArgumentOutOfRangeException("Must be of type EntityFrameworkUnitOfWork");
}
dbSet = entityFrameworkUnitOfWork.GetDbSet<T>();
}
public void Add(T item)
{
dbSet.Add(item);
}
public void Remove(T item)
{
dbSet.Remove(item);
}
public IQueryable<T> Query()
{
return dbSet;
}
}
public class EntityFrameworkUnitOfWork : IUnitOfWork
{
private readonly DbContext context;
public EntityFrameworkUnitOfWork()
{
this.context = new CustomerContext();;
}
internal DbSet<T> GetDbSet<T>()
where T : class
{
return context.Set<T>();
}
public void SaveChanges()
{
context.SaveChanges();
}
public void Dispose()
{
context.Dispose();
}
}
El repositorio de clientes :
public interface ICustomerRepository : IRepository<Customer>
{
}
public class CustomerRepository : EntityFrameworkRepository<Customer>, ICustomerRepository
{
public CustomerRepository(IUnitOfWork unitOfWork): base(unitOfWork)
{
}
}
Controlador MVC de ASP.NET utilizando el repositorio:
public class CustomerController : Controller
{
UnityContainer container = new UnityContainer();
public ActionResult List()
{
var unitOfWork = container.Resolve<IUnitOfWork>();
var customerRepository = container.Resolve<ICustomerRepository>();
return View(customerRepository.Query());
}
[HttpPost]
public ActionResult Create(Customer customer)
{
var unitOfWork = container.Resolve<IUnitOfWork>();
var customerRepository = container.Resolve<ICustomerRepository>();;
customerRepository.Add(customer);
unitOfWork.SaveChanges();
return RedirectToAction("List");
}
}
Inyección de dependencia con la unidad:
container.RegisterType<IUnitOfWork, EntityFrameworkUnitOfWork>();
container.RegisterType<ICustomerRepository, CustomerRepository>();
Solución:
¿PROBLEMAS?
La implementación del repositorio (código EF) es muy genérica. Todo se encuentra en el lado de la
EntityFrameworkRepository<T>
. Los repositorios de modelos concretos no contienen nada de esta lógica. ¿Esto me ahorra escribir toneladas de código redundante, pero posiblemente sacrifica la flexibilidad?Las clases
ICustomerRepository
yCustomerRepository
están básicamente vacías. Ellos están puramente allí para proporcionar abstracción. Según tengo entendido, esto encaja con la visión de la arquitectura Onion, donde la infraestructura y el código que depende de la plataforma se encuentra en el exterior de su sistema, pero tener clases vacías e interfaces vacías ¿se siente mal?Para usar una implementación de persistencia diferente (por ejemplo, Azure Table Storage), entonces se debería crear una nueva clase
CustomerRepository
y se heredaría unAzureTableStorageRepository<T>
. ¿Pero esto podría llevar a código redundante (múltiples depósitos de clientes)? ¿Cómo se burlaría este efecto?Otra implementación (por ejemplo, Azure Table Storage) tiene limitaciones en el soporte transnacional, por lo que la clase AzureTableStorageUnitOfWork no funcionaría en este contexto.
¿Hay otros problemas con la forma en que he hecho esto?
(He tomado la mayor parte de mi inspiración de este post )
La única desventaja que veo es que depende mucho de su herramienta IOC, así que asegúrese de que su implementación sea sólida. Sin embargo, esto no es exclusivo de los diseños de Cebolla. He usado Onion en varios proyectos y no me he encontrado con ningún "error" real.
Puedo decir que este código es lo suficientemente bueno para el primer intento, pero tiene algunos lugares para mejorar.
Vamos a ver algunos de ellos.
1. Inyección de dependencia (DI) y uso de IoC.
Utiliza la versión más sencilla del patrón de Localizador de servicios : la propia instancia de container
.
Te sugiero que uses ''inyección de constructor''. Puede encontrar más información aquí (ASP.NET MVC 4 Dependency Injection) .
public class CustomerController : Controller
{
private readonly IUnitOfWork unitOfWork;
private readonly ICustomerRepository customerRepository;
public CustomerController(
IUnitOfWork unitOfWork,
ICustomerRepository customerRepository)
{
this.unitOfWork = unitOfWork;
this.customerRepository = customerRepository;
}
public ActionResult List()
{
return View(customerRepository.Query());
}
[HttpPost]
public ActionResult Create(Customer customer)
{
customerRepository.Add(customer);
unitOfWork.SaveChanges();
return RedirectToAction("List");
}
}
2. Ámbito de la Unidad de Trabajo (UoW).
No puedo encontrar el estilo de vida de IUnitOfWork
y ICustomerRepository
. No estoy familiarizado con Unity pero msdn dice que TransientLifetimeManager se usa de forma predeterminada . Esto significa que obtendrá una nueva instancia cada vez que resuelva el tipo.
Entonces, la siguiente prueba falla:
[Test]
public void MyTest()
{
var target = new UnityContainer();
target.RegisterType<IUnitOfWork, EntityFrameworkUnitOfWork>();
target.RegisterType<ICustomerRepository, CustomerRepository>();
//act
var unitOfWork1 = target.Resolve<IUnitOfWork>();
var unitOfWork2 = target.Resolve<IUnitOfWork>();
// assert
// This Assert fails!
unitOfWork1.Should().Be(unitOfWork2);
}
Y espero que la instancia de UnitOfWork
en su controlador difiera de la instancia de UnitOfWork
en su repositorio. A veces puede resultar en errores. Pero no se destaca en ASP.NET MVC 4 Dependency Injection como un problema para Unity.
En Castle Windsor, el PerWebRequest vida PerWebRequest se usa para compartir la misma instancia de tipo dentro de una única solicitud http.
Es un enfoque común cuando UnitOfWork
es un componente PerWebRequest . ActionFilter
puede usar ActionFilter
personalizado para invocar Commit()
durante la invocación del método OnActionExecuted()
.
También cambiaría el nombre del método SaveChanges()
y lo llamaría simplemente Commit
como se llama en el ejemplo y en el PoEAA .
public interface IUnitOfWork : IDisposable
{
void Commit();
}
3.1. Dependencias de los repositorios.
Si sus repositorios van a estar ''vacíos'' no es necesario crear interfaces específicas para ellos. Es posible resolver IRepository<Customer>
y tener el siguiente código en su controlador
public CustomerController(
IUnitOfWork unitOfWork,
IRepository<Customer> customerRepository)
{
this.unitOfWork = unitOfWork;
this.customerRepository = customerRepository;
}
Hay una prueba que lo prueba.
[Test]
public void MyTest()
{
var target = new UnityContainer();
target.RegisterType<IRepository<Customer>, CustomerRepository>();
//act
var repository = target.Resolve<IRepository<Customer>>();
// assert
repository.Should().NotBeNull();
repository.Should().BeOfType<CustomerRepository>();
}
Pero si desea tener repositorios que sean ''capa de abstracción sobre la capa de mapeo donde se concentra el código de construcción de la consulta''. ( PoEAA, Repositorio )
Un Repositorio media entre el dominio y las capas de mapeo de datos, actuando como una colección de objetos de dominio en memoria. Los objetos del cliente construyen especificaciones de consulta de forma declarativa y las envían al Repositorio para su satisfacción.
3.2. Herencia en EntityFrameworkRepository.
En este caso me gustaría crear un simple IRepository
public interface IRepository
{
void Add(object item);
void Remove(object item);
IQueryable<T> Query<T>() where T : class;
}
y su implementación que sabe cómo trabajar con la infraestructura de EntityFramework y puede reemplazarse fácilmente por otra (por ejemplo, AzureTableStorageRepository
).
public class EntityFrameworkRepository : IRepository
{
public readonly EntityFrameworkUnitOfWork unitOfWork;
public EntityFrameworkRepository(IUnitOfWork unitOfWork)
{
var entityFrameworkUnitOfWork = unitOfWork as EntityFrameworkUnitOfWork;
if (entityFrameworkUnitOfWork == null)
{
throw new ArgumentOutOfRangeException("Must be of type EntityFrameworkUnitOfWork");
}
this.unitOfWork = entityFrameworkUnitOfWork;
}
public void Add(object item)
{
unitOfWork.GetDbSet(item.GetType()).Add(item);
}
public void Remove(object item)
{
unitOfWork.GetDbSet(item.GetType()).Remove(item);
}
public IQueryable<T> Query<T>() where T : class
{
return unitOfWork.GetDbSet<T>();
}
}
public interface IUnitOfWork : IDisposable
{
void Commit();
}
public class EntityFrameworkUnitOfWork : IUnitOfWork
{
private readonly DbContext context;
public EntityFrameworkUnitOfWork()
{
this.context = new CustomerContext();
}
internal DbSet<T> GetDbSet<T>()
where T : class
{
return context.Set<T>();
}
internal DbSet GetDbSet(Type type)
{
return context.Set(type);
}
public void Commit()
{
context.SaveChanges();
}
public void Dispose()
{
context.Dispose();
}
}
Y ahora CustomerRepository
puede ser un proxy y referirse a él.
public interface IRepository<T> where T : class
{
void Add(T item);
void Remove(T item);
}
public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
protected readonly IRepository Repository;
protected RepositoryBase(IRepository repository)
{
Repository = repository;
}
public void Add(T item)
{
Repository.Add(item);
}
public void Remove(T item)
{
Repository.Remove(item);
}
}
public interface ICustomerRepository : IRepository<Customer>
{
IList<Customer> All();
IList<Customer> FindByCriteria(Func<Customer, bool> criteria);
}
public class CustomerRepository : RepositoryBase<Customer>, ICustomerRepository
{
public CustomerRepository(IRepository repository)
: base(repository)
{ }
public IList<Customer> All()
{
return Repository.Query<Customer>().ToList();
}
public IList<Customer> FindByCriteria(Func<Customer, bool> criteria)
{
return Repository.Query<Customer>().Where(criteria).ToList();
}
}
Veo un par de problemas serios en el código.
El primer problema es la relación entre repositorios y UoW.
var unitOfWork = container.Resolve<IUnitOfWork>();
var customerRepository = container.Resolve<ICustomerRepository>();
Aquí está la dependencia implícita. ¡El repositorio no funcionará solo sin UoW! No todos los repositorios necesitan estar conectados con UoW. Por ejemplo, ¿qué pasa con los procedimientos almacenados? Tienes el procedimiento almacenado y lo escondes detrás del repositorio. ¡La invocación de procedimientos almacenados usa una transacción separada! Al menos no en todos los casos. Entonces, si resuelvo el único repositorio y agrego un elemento, entonces no funcionará. Además, este código no funcionará si configuro Transient life license porque el repositorio tendrá otra instancia de UoW. Así que tenemos un acoplamiento implícito apretado.
El segundo problema es crear un acoplamiento apretado entre el motor del contenedor DI y utilizarlo como localizador de servicio. El localizador de servicios no es un buen enfoque para implementar IoC y agregación. En algún caso es anti patrón. Se debe utilizar el contenedor DI