c# - repositorio - Entity Framework 6 y Unit of Work… ¿Dónde, cuándo? ¿Es como transacciones en ado.net?
unit of work c# (1)
Primero debo decir que no existe una manera correcta y única de resolver este problema. Solo estoy presentando aquí lo que probablemente haría.
Lo primero es que DbContext
implementa el patrón de unidad de trabajo . Al llamar a SaveChanges
se crea una transacción de base de datos, por lo que todas las consultas ejecutadas contra la base de datos se revertirán si algo sale mal.
Ahora, hay un problema importante en el diseño actual que tiene: su repositorio llama a SaveChanges
en el DbContext
. Esto significa que usted hace responsable a XXXRepository
de cometer todas las modificaciones que realizó en la unidad de trabajo , no solo las modificaciones en las entidades XXX de las que es responsable su repositorio.
Otra cosa es que DbContext
es un repositorio. Entonces, abstraer el uso de DbContext
dentro de otro repositorio simplemente crea otra abstracción en una abstracción existente, eso es simplemente demasiado código IMO.
Además, es posible que necesite acceder a las entidades XXX desde el repositorio YYY y a las entidades YYY desde el repositorio XXX, por lo que, para evitar las dependencias circulares, obtendrá un inútil MyRepository : IRepository<TEntity>
que simplemente duplica todos los métodos DbSet
.
Me gustaría soltar toda la capa del repositorio. Yo usaría DbContext
directamente dentro de la capa de servicio. Por supuesto, puede factorizar todas las consultas complejas que no desea duplicar en la capa de servicio. Algo como:
public MyService()
{
...
public MyEntity Create(some parameters)
{
var entity = new MyEntity(some parameters);
this.context.MyEntities.Add(entity);
// Actually commits the whole thing in a transaction
this.context.SaveChanges();
return entity;
}
...
// Example of a complex query you want to use multiple times in MyService
private IQueryable<MyEntity> GetXXXX_business_name_here(parameters)
{
return this.context.MyEntities
.Where(z => ...)
.....
;
}
}
Con este patrón, cada llamada pública en una clase de servicio se ejecuta dentro de una transacción gracias a que DbContext.SaveChanges
es transaccional.
Ahora, para el ejemplo que tiene con el ID que se requiere después de la primera inserción de la entidad, una solución es no usar el ID, sino la propia entidad. Así que dejas que Entity Framework y su propia implementación del patrón de unidad de trabajo se encarguen de ello.
Entonces, en lugar de:
var entity = new MyEntity();
entity = mydbcontext.Add(entity);
// what should I put here?
var otherEntity = mydbcontext.MyEntities.Single(z => z.ID == 123);
otherEntity.OtherPropertyId = entity.Id;
uow.Commit();
tienes:
var entity = new MyEntity();
entity = mydbcontext.Add(entity);
var otherEntity = mydbcontext.MyEntities.Single(z => z.ID == 123);
otherEntity.OtherProperty = entity; // Assuming you have a navigation property
uow.Commit();
Si no tiene una propiedad de navegación, o si tiene un caso de uso más complejo con el cual tratar, la solución es utilizar la transacción de Good Gold dentro de su método de servicio público:
public MyService()
{
...
public MyEntity Create(some parameters)
{
// Encapuslates multiple SaveChanges calls in a single transaction
// You could use a ITransaction if you don''t want to reference System.Transactions directly, but don''t think it''s really useful
using (var transaction = new TransactionScope())
{
var firstEntity = new MyEntity { some parameters };
this.context.MyEntities.Add(firstEntity);
// Pushes to DB, this''ll create an ID
this.context.SaveChanges();
// Other commands here
...
var newEntity = new MyOtherEntity { xxxxx };
newEntity.MyProperty = firstEntity.ID;
this.context.MyOtherEntities.Add(newEntity);
// Pushes to DB **again**
this.context.SaveChanges();
// Commits the whole thing here
transaction.Commit();
return firstEntity;
}
}
}
Incluso puede llamar al método de servicios múltiples dentro de un ámbito transaccional si es necesario:
public class MyController()
{
...
public ActionResult Foo()
{
...
using (var transaction = new TransactionScope())
{
this.myUserService.CreateUser(...);
this.myCustomerService.CreateOrder(...);
transaction.Commit();
}
}
}
Creando un nuevo proyecto MVC y me gusta la idea de repositorios en la capa de datos, así que los he implementado. También he creado una capa de servicio para manejar toda la lógica empresarial y la validación, esta capa a su vez usa el repositorio apropiado. Algo como esto (estoy usando Simple Injector para inyectar)
DAL LAYER
public class MyRepository {
private DbContext _context;
public MyRepository(DbContext context) {
_context = context;
}
public MyEntity Get(int id)
{
return _context.Set<MyEntity>().Find(id);
}
public TEntity Add(MyEntity t)
{
_context.Set<MyEntity>().Add(t);
_context.SaveChanges();
return t;
}
public TEntity Update(MyEntity updated, int key)
{
if (updated == null)
return null;
MyEntity existing = _context.Set<MyEntity>().Find(key);
if (existing != null)
{
_context.Entry(existing).CurrentValues.SetValues(updated);
_context.SaveChanges();
}
return existing;
}
public void Delete(MyEntity t)
{
_context.Set<MyEntity>().Remove(t);
_context.SaveChanges();
}
}
CAPA DE SERVICIO
public class MyService {
private MyRepository _repository;
public MyService(MyRepository repository) {
_repository = repository;
}
public MyEntity Get(int id)
{
return _repository.Get(id);
}
public MyEntity Add(MyEntity t)
{
_repository.Add(t);
return t;
}
public MyEntity Update(MyEntity updated)
{
return _repository.Update(updated, updated.Id);
}
public void Delete(MyEntity t)
{
_repository.Delete(t);
}
}
Ahora esto es muy simple, así que puedo usar el siguiente código para actualizar un objeto.
MyEntity entity = MyService.Get(123);
MyEntity.Name = "HELLO WORLD";
entity = MyService.Update(entity);
O esto para crear un objeto.
MyEntity entity = new MyEntity();
MyEntity.Name = "HELLO WORLD";
entity = MyService.Add(entity);
// entity.Id is now populated
Ahora, digamos que necesitaba actualizar un elemento basado en la ID de creación de otro, podría usar el código sobre todo bien, pero ¿qué sucede si se produce un error? Necesito algún tipo de transacción / rollback. ¿Es esto lo que el patrón de Unidad de Trabajo se supone que debe resolver?
¿Entonces creo que necesito tener DbContext en mi objeto UnitOfWork, entonces creo un objeto como ese?
public class UnitOfWork : IDisposable {
private DbContext _context;
public UnitOfWork(DbContext context) {
_context = context;
}
public Commit() {
_context.SaveChanges();
}
public Dispose() {
_context.Dispose();
}
}
Ok, de nuevo, eso es bastante simple. UnitOfWork también contiene el contexto (de todos modos uso el mismo contexto en todos los repositorios) y llama al método SaveChanges (). Luego eliminaría la llamada al método SaveChanges () de mi repositorio. Así que para añadir haría lo siguiente:
UnitOfWork uow = new UnitOfWork(new DbContext()); // i would inject this somehow
MyEntity entity = new MyEntity();
MyEntity.Name = "HELLO WORLD";
entity = MyService.Add(entity);
uow.Commit();
Pero, ¿qué sucede si necesito crear un objeto y luego actualizar otros objetos basados en esa identificación? Esto no funcionará, porque la identificación no se creará hasta que llame a Commit en la página. Ejemplo
UnitOfWork uow = new UnitOfWork(new DbContext()); // i would inject this somehow
MyEntity entity = new MyEntity();
MyEntity.Name = "HELLO WORLD";
entity = MyService.Add(entity);
// entity.Id is NOT populated
MyEntity otherEntity = MyService.Get(123);
otherEntity.OtherProperty = entity.Id;
MyService.Update(otherEntity);
uow.Commit(); // otherEntity.OtherProperty is not linked.....?
Así que tengo la sensación de que esta clase de UnitOfWork no está bien ... tal vez no entiendo algo.
Necesito poder agregar una entidad y obtener esa identificación y usarla en otra entidad, pero si ocurre un error, quiero "revertir" como lo haría una transacción de ado.net.
¿Es posible esta funcionalidad usando Entity Framework y Repositories?