Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework 6 and Unit Of Work... Where, When? Is it like transactions in ado.net?

Creating a new MVC project and like the idea of repositories in the data layer, so i have implemented them. I have also created a Service layer to handle all business logic and validation, this layer in turn uses the appropriate repository. Something like this (I am using Simple Injector to inject)

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();     } } 

SERVICE LAYER

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);     } } 

Now this is very simple, so i can use the following code to update an object.

MyEntity entity = MyService.Get(123); MyEntity.Name = "HELLO WORLD"; entity = MyService.Update(entity); 

Or this to create an object

MyEntity entity = new MyEntity(); MyEntity.Name = "HELLO WORLD"; entity = MyService.Add(entity); // entity.Id is now populated 

Now say i needed to update an item based on the creation Id of another, i could use the code above all fine, but what happens if an error occurs? I need some sort of transaction/rollback. Is this what the Unit Of Work pattern is suppose to solve?

So i guess i need to have DbContext in my UnitOfWork object, so i create an object like so?

public class UnitOfWork : IDisposable {      private DbContext _context;      public UnitOfWork(DbContext context) {         _context = context;     }      public Commit() {         _context.SaveChanges();     }      public Dispose() {         _context.Dispose();     }  } 

Ok so again, thats quite simple. UnitOfWork holds the context as well ( i use same context on all repositories anyway) and it calls the SaveChanges() method. I would then remove the SaveChanges() method call from my repository. So to add i would do the following:

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(); 

But what if i need to create an object and then update other objects based on that Id, this will now not work, because the Id will not be created until i call Commit on the uow. Example

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.....? 

So i have a feeling that this UnitOfWork class is not right... maybe i am miss understanding something.

I need to be able to add an entity and get that Id and use it on another entity, but if an error occurs, i want to "rollback" like an ado.net transaction would do.

Is this functionality possible using Entity Framework and Repositories?

like image 891
Gillardo Avatar asked Sep 26 '14 08:09

Gillardo


People also ask

Does Entity Framework support transactions?

Entity Framework internally maintains transactions when the SaveChanges() method is called. It means the Entity Framework maintains a transaction for the multiple entity insert, update and delete in a single SaveChanges() method.

Can we use Entity Framework and ADO.NET together?

EF is built on top of ADO.Net, meaning that you can use both at the same time.

Does Entity Framework use transactions by default?

By default, for instance, on SQL Server this is READ COMMITTED. Entity Framework does not wrap queries in a transaction. This default functionality is suitable for a lot of users and if so there is no need to do anything different in EF6; just write the code as you always did.


1 Answers

I have to say first that there is not a unique right way to solve this issue. I'm just presenting here what I would probably do.


First thing is, DbContext itself implements the Unit of work pattern. Calling SaveChanges does create a DB transaction so every query executed against the DB will be rollbacked is something goes wrong.

Now, there is a major issue in the current design you have: your repository calls SaveChanges on the DbContext. This means that you make XXXRepository responsible to commit all the modification you made on the unit of work, not just the modifications on the XXX entities your repository is responsible for.

Another thing is that DbContext is a repository itself too. So abstracting the DbContext usage inside another repository just creates another abstraction on an existing abstraction, that's just too much code IMO.

Plus the fact you may need to access XXX entities from YYY repository and YYY entities from XXX repository, so to avoid circular dependencies you'll end up with a useless MyRepository : IRepository<TEntity> that just duplicates all the DbSet methods.

I would drop the whole repository layer. I would use the DbContext directly inside the service layer. Of course, you can factor all complex queries you don't want to duplicate in the service layer. Something like:

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 => ...)             .....             ;     } } 

With this pattern, every public call on a service class is executed inside a transaction thanks to DbContext.SaveChanges being transactional.

Now for the example you have with the ID that is required after the first entity insertion, one solution is to not use the ID but the entity itself. So you let Entity Framework and its own implementation of the unit of work pattern deal with it.

So instead of:

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(); 

you have:

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(); 

If you don't have a navigation property, or if you have a more complex use case to deal with, the solution is to use the good gold transaction inside your public service method:

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;         }     } } 

You can even call multiple services method inside a transactional scope if required:

public class MyController() {     ...      public ActionResult Foo()     {         ...         using (var transaction = new TransactionScope())         {             this.myUserService.CreateUser(...);             this.myCustomerService.CreateOrder(...);              transaction.Commit();         }     } } 
like image 101
ken2k Avatar answered Oct 18 '22 07:10

ken2k