How can I implement a transaction for my repositories with Entity Framework?
Asked Answered
T

4

18

I am trying to utilize the repository design pattern in my application for 2 reasons

  1. I like to de-couple my application from Entity in case I decide to not use Entity Framework at some point

  2. I want to be able reuse the logic that interacts with the model

I successfully setup and used the repository pattern. However, I have one complexity to deal with which is a transaction.

I want to be able to use transaction so that I can make multiple calls to the repository and then commit or rollback.

Here is my repository interface

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Support.Repositories.Contracts
{
    public interface IRepository<TModel> where TModel : class
    {
        // Get records by it's primary key
        TModel Get(int id);

        // Get all records
        IEnumerable<TModel> GetAll();

        // Get all records matching a lambda expression
        IEnumerable<TModel> Find(Expression<Func<TModel, bool>> predicate);

        // Get the a single matching record or null
        TModel SingleOrDefault(Expression<Func<TModel, bool>> predicate);

        // Add single record
        void Add(TModel entity);

        // Add multiple records
        void AddRange(IEnumerable<TModel> entities);

        // Remove records
        void Remove(TModel entity);

        // remove multiple records
        void RemoveRange(IEnumerable<TModel> entities);
    }
}

Then I create an implementation for Entity Framework like so

using Support.Repositories.Contracts;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;

namespace Support.Repositories
{
    public class EntityRepository<TEntity> : IRepository<TEntity>
        where TEntity : class
    {
        protected readonly DbContext Context;
        protected readonly DbSet<TEntity> DbSet;

        public EntityRepository(DbContext context)
        {
            Context = context;
            DbSet = context.Set<TEntity>();
        }

        public TEntity Get(int id)
        {
            return DbSet.Find(id);
        }

        public IEnumerable<TEntity> GetAll()
        {
            return DbSet.ToList();
        }

        public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
        {
            return DbSet.Where(predicate);
        }

        public TEntity SingleOrDefault(Expression<Func<TEntity, bool>> predicate)
        {
            return DbSet.SingleOrDefault(predicate);
        }

        public void Add(TEntity entity)
        {
            DbSet.Add(entity);
        }

        public void AddRange(IEnumerable<TEntity> entities)
        {
            DbSet.AddRange(entities);
        }

        public void Remove(TEntity entity)
        {
            DbSet.Remove(entity);
        }

        public void RemoveRange(IEnumerable<TEntity> entities)
        {
            DbSet.RemoveRange(entities);
        }

    }
}

Now, I create a IUnitOfWork to interact with repository like so

using System;

namespace App.Repositories.Contracts
{
    public interface IUnitOfWork : IDisposable
    {
        IUserRepository Users { get; }
        IAddressRepository Addresses { get;  }
    }
}

Then I implemented this interface for Entity Framework like this:

using App.Contexts;
using App.Repositories.Contracts;
using App.Repositories.Entity;

namespace App.Repositories
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly AppContext _context;
        public IUserRepository  Users { get; private set; }
        public IAddressRepository Addresses { get; private set; }

        public UnitOfWork(AppContext context)
        {
            _context = context;

            Users = new UserRepository(_context);
            Addresses = new AddressRepository(_context);
        }

        public UnitOfWork() : this(new AppContext())
        {
        }

        public int Save()
        {
            return _context.SaveChanges();
        }

        public void Dispose()
        {
            _context.Dispose();
        }
    }
}

I am able to use the repository like this

using(var repository = new UnitOfWork())
{
     repository.Users.Add(new User(... User One ...))
     repository.Save();

     repository.Addresses(new Address(... Address For User One ...))
     repository.Save();

     repository.Users.Add(new User(... User Two...))
     repository.Save();

     repository.Addresses(new Address(... Address For User Two...))
     repository.Save();
}

Now, I want to be able to use database transaction so only when everything is good then commit otherwise rollback.

My first take is to add a new method called BeginTransaction() to my UnitOfWork class. But will couple my code to Entity Framework only.

Now, I am thinking to create a new interface that provides BeginTransaction(), Commit() and Rollback() method which will allow me to write an implementation for any ORM.

i.e.

namespace Support.Contracts
{
    public IRepositoryDatabase
    {
        SomethingToReturn BeginTransaction();

        void Commit();
        void Rollback();
    }
}

The question is how would I tie IRepositoryDatabase back to my UnitOfWork so I can implement correctly? And what would BeginTransaction() needs to return?

Tribe answered 6/10, 2016 at 22:18 Comment(9)
EF will store by default all changed entities or none on Save() call.Thruway
but how would I begin transaction?Tribe
Change 10 entities and after that call Save(). It will be written in a single transaction by EF (or nothing will be written, if any error occurs).Thruway
but in some cases I will need to call Save() multiple times in order to get the last inserted id. So I can't wait until the end to call Save which is why I meed transactionTribe
Use the entity references and not the id values and you are fineThruway
How would I do that?Tribe
var user = new User(); var address = new Address { User = user };Thruway
What if I want to change to a different framework and not entity. May be the other framework does not handle everything like Entity. Wouln't I need some kind of a wrapper to ensure this will work the same regardless of the implementation?Tribe
@Tribe Out of curiosity, did you finally switch to another ORM? was it really worth creating so many wrappers?Competence
T
32

I think I figured out the way to do it. (I hope I did it the right way)

Here is what I have done, I hope this helps someone looking to do the same thing.

I created a new Interface like so

using System;

    namespace Support.Repositories.Contracts
    {
        public interface IDatabaseTransaction : IDisposable
        {
            void Commit();
    
            void Rollback();
        }
    }

Then I implemented IDatabaseTransaction for Entity framework like so

using Support.Repositories.Contracts;
using System.Data.Entity;

namespace Support.Entity.Repositories
{
    public class EntityDatabaseTransaction : IDatabaseTransaction
    {
        private DbContextTransaction _transaction;

        public EntityDatabaseTransaction(DbContext context)
        {
            _transaction = context.Database.BeginTransaction();
        }

        public void Commit()
        {
            _transaction.Commit();
        }

        public void Rollback()
        {
            _transaction.Rollback();
        }

        public void Dispose()
        {
            _transaction.Dispose();
        }
    }
}

Then, I added a new method called BeginTransaction() to my IUnitOfWork contract like so

using System;

namespace App.Repositories.Contracts
{
    public interface IUnitOfWork : IDisposable
    {
        IDatabaseTransaction BeginTransaction();
        IUserRepository Users { get; }
        IAddressRepository Addresses { get;  }
    }
}

Finally, following is my UnitOfwork implementation for Entity

using App.Contexts;
using App.Repositories.Contracts;
using App.Repositories.Entity;
using Support.Repositories;


namespace App.Repositories
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly AppContext _context;
        public IUserRepository  Users { get; private set; }
        public IAddressRepository Addresses { get; private set; }

        public UnitOfWork(AppContext context)
        {
            _context = context;

            Users = new UserRepository(_context);
            Addresses = new AddressRepository(_context);
        }

        public UnitOfWork() : this(new AppContext())
        {
        }

        public int Save()
        {
            return _context.SaveChanges();
        }

        public void Dispose()
        {
            _context.Dispose();
        }

        public IDatabaseTransaction BeginTransaction()
        {
            return new EntityDatabaseTransaction(_context);
        }
    }
}

And here is how I consume the UnitOfWork implementation from my controller

using(var unitOfWork = new UnitOfWork())
using(var transaction = new unitOfWork.BeginTransaction())
{
     try
     {
         unitOfWork.Users.Add(new User(... User One ...))
         unitOfWork.Save();

         unitOfWork.Addresses(new Address(... Address For User One ...))
         unitOfWork.Save();

         unitOfWork.Users.Add(new User(... User Two...))
         unitOfWork.Save();

         unitOfWork.Addresses(new Address(... Address For User Two...))
         unitOfWork.Save();
         transaction.Commit();
     } 
     catch(Exception)
     {
          transaction.Rollback();
     }

}
Tribe answered 7/10, 2016 at 16:3 Comment(1)
Shouldn't be repository changed with unitOfWorkvariable name in working example at the bottom?Flavin
C
14

In EF Core, Although UnitOfWork pattern is implemented internally, you can simply use IDbContextTransaction interface as follow (supposing that you use Dependency Injection):

public interface IUnitOfWork 
{
    int SaveChanges();
    Task<int> SaveChangesAsync();
    IDbContextTransaction BeginTransaction();
    Task<IDbContextTransaction> BeginTransactionAsync();
    
    IUserRepository Users { get; }
    IAddressRepository Addresses { get;  }
}

and the implementation:

public class UnitOfWork : IUnitOfWork, IDisposable
{
    private bool _disposed;
    private readonly AppDbContext _context;

    public UnitOfWork(AppDbContext context, 
                 IUserRepository userRepositpry, IAddressRepository addressRepository)
    {
        _context = context;
        Users = userRepositpry;
        Addresses = addressRepository;
    }

    public IUserRepository Users { get; }
    public IAddressRepository Addresses { get; }

    public int SaveChanges()
    {
        return _context.SaveChanges();
    }

    public async Task<int> SaveChangesAsync()
    {
        return await _context.SaveChangesAsync();
    }

    public IDbContextTransaction BeginTransaction()
    {
        return _context.Database.BeginTransaction();
    }

    public async Task<IDbContextTransaction> BeginTransactionAsync()
    {
        return await _context.Database.BeginTransactionAsync();
    }

    protected void Dispose(bool disposing)
    {
        if (!this._disposed)
        {
            if (disposing)
            {
                _context.Dispose();
            }
        }
        this._disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Usage:

 public class FooService
{
    private readonly IUnitOfWork _unitOfWork;
    public FooService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public void Bar()
    {
        using (var transaction = _unitOfWork.BeginTransaction())
        {
            try
            {
                _unitOfWork.Users.Add(new UserModel("dummy username"));
                _unitOfWork.SaveChanges();
                _unitOfWork.Addresses.Add(new AddressModel("dummy address"));
                _unitOfWork.SaveChanges();

                transaction.Commit();
            }
            catch (Exception)
            {
                transaction.Rollback();
            }
        }
    }
}
Clarisclarisa answered 28/6, 2020 at 12:11 Comment(0)
M
1

While comments by Sir Rufo are correct, you did said that wanted an EF independent solution and although usually abstracting away from the ORM is an overkill, if you are still set on handling the transaction yourself you can use TransactionScope (which was apparently the way to achieve control over the transaction before having BeginTransaction in the context.Database).

Please see the following article for details: https://msdn.microsoft.com/en-us/data/dn456843.aspx

Relevant bits are that you can enclose all the calls in a TransactionScope (this will actually work out of the box in other ORMs as well):

using System.Collections.Generic; 
using System.Data.Entity; 
using System.Data.SqlClient; 
using System.Linq; 
using System.Transactions; 

namespace TransactionsExamples 
{ 
    class TransactionsExample 
    { 
        static void UsingTransactionScope() 
        { 
            using (var scope = new TransactionScope(TransactionScopeOption.Required)) 
            { 
                using (var conn = new SqlConnection("...")) 
                { 
                    conn.Open(); 

                    var sqlCommand = new SqlCommand(); 
                    sqlCommand.Connection = conn; 
                    sqlCommand.CommandText = 
                        @"UPDATE Blogs SET Rating = 5" + 
                            " WHERE Name LIKE '%Entity Framework%'"; 
                    sqlCommand.ExecuteNonQuery(); 

                    using (var context = 
                        new BloggingContext(conn, contextOwnsConnection: false)) 
                    { 
                        var query = context.Posts.Where(p => p.Blog.Rating > 5); 
                        foreach (var post in query) 
                        { 
                            post.Title += "[Cool Blog]"; 
                        } 
                        context.SaveChanges(); 
                    } 
                } 

                scope.Complete(); 
            } 
        } 
    } 
}

But do need to mind the following caveats:

There are still some limitations to the TransactionScope approach:

  • Requires .NET 4.5.1 or greater to work with asynchronous methods
  • It cannot be used in cloud scenarios unless you are sure you have one and only one connection (cloud scenarios do not support distributed transactions)
  • It cannot be combined with the Database.UseTransaction() approach of the previous sections
  • It will throw exceptions if you issue any DDL (e.g. because of a Database Initializer) and have not enabled distributed transactions through the MSDTC Service
Multiplicity answered 6/10, 2016 at 22:45 Comment(2)
Can I wrap my commands into a method that accepts action? something like this? public void ExecuteWithInTransaction(Action actions) { var trans = _context.Database.BeginTransaction(); try { actions(); trans.Commit(); } catch (Exception) { trans.Rollback(); } }Tribe
I suppose you can, but it seems very unorthodox and it still ties your ExecuteWithInTransaction to EF I think unless you make it an interface and provide the implementation somehow.Multiplicity
S
0

Aside all the solutions above I did it the easy way I know most of the sollutions are handy but I'm sharing it maybe it can help somebody. First in IUnitOfWork I set a rule as:

DatabaseFacade DbTransaction();

And in the implementation I just used:

public DatabaseFacade DbTransaction()
{
return _myContext.Database;
}

Now anywhere in my code I can use it easily and I can call it and simplicity enable me to keep track of things and remember what I have coded. Calling process:

var transaction = _unitOfWork.DbTransaction().BeginTransaction();
Try{
//Any operations you like
transaction.Commit();
}catch(Exception E){
//handling Exception
}
Sihun answered 19/9, 2022 at 23:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.