los pollos hermanos
los pollos hermanos

Reputation: 337

How to use database transactions in current approach?

I am writting an API backend application using .NET Core

BaseRepository:

public class BaseRepository<T, TPrimaryKey> : IBaseRepository<T, TPrimaryKey> where T : class where TPrimaryKey : struct
{
    private readonly DatabaseContext _dbContext;

    public BaseRepository(DatabaseContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<IEnumerable<T>> GetAll()
    {
        return await _dbContext.Set<T>().ToListAsync();
    }

    public IQueryable<T> GetQueryable()
    {
        return _dbContext.Set<T>();
    }

    public async Task<T> Find(TPrimaryKey id)
    {
        return await _dbContext.Set<T>().FindAsync(id);
    }

    public async Task<T> Add(T entity, bool saveChanges = true)
    {
        await _dbContext.Set<T>().AddAsync(entity);
        if (saveChanges) 
            await _dbContext.SaveChangesAsync();

        return await Task.FromResult(entity);
    }

    public async Task Edit(T entity, bool saveChanges = true)
    {
        _dbContext.Entry(entity).State = EntityState.Modified;
        if (saveChanges) 
            await _dbContext.SaveChangesAsync();
    }

    public async Task Delete(T entity, bool saveChanges = true)
    {
        if (entity == null)
            throw new NullReferenceException();

        _dbContext.Set<T>().Remove(entity);
        if (saveChanges) 
            await _dbContext.SaveChangesAsync();
    }

    public async Task<IEnumerable<T>> BulkInsert(IEnumerable<T> entities, bool saveChanges = true)
    {
        foreach (T entity in entities)
        {
            await _dbContext.Set<T>().AddAsync(entity);
        }

        if (saveChanges) 
            await _dbContext.SaveChangesAsync();

        return await Task.FromResult(entities);
    }

    public async Task BulkUpdate(IEnumerable<T> entities, bool saveChanges = true)
    {
        foreach (T entity in entities)
        {
            _dbContext.Entry(entity).State = EntityState.Modified;
        }

        if (saveChanges) 
            await _dbContext.SaveChangesAsync();
    }

    public async Task Save()
    {
        await _dbContext.SaveChangesAsync();
    }
}

IBaseRepository:

public interface IBaseRepository<T, E> where T : class where E : struct
{
    Task<IEnumerable<T>> GetAll();
    IQueryable<T> GetQueryable();
    Task<T> Find(E id);
    Task<T> Add(T entity, bool saveChanges = true);
    Task Edit(T entity, bool saveChanges = true);
    Task Delete(T entity, bool saveChanges = true);
    Task<IEnumerable<T>> BulkInsert(IEnumerable<T> entities, bool saveC
    Task BulkUpdate(IEnumerable<T> entities, bool saveChanges = true);
    Task Save();
}

IServiceBase:

public interface IServiceBase<TEntity, TPrimaryKey>
{
    Task<TEntity> GetById(TPrimaryKey id);
    Task<TEntity> GetSingle(Expression<Func<TEntity, bool>> whereCondition);
    Task<IEnumerable<TEntity>> GetAll();
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> whereCondition);
    IQueryable<TEntity> GetAllQueryable();
    IQueryable<TEntity> Query(Expression<Func<TEntity, bool>> whereCondition);
    Task<TEntity> Create(TEntity entity);
    Task Delete(TEntity entity);
    Task Update(TEntity entity);
    Task<long> Count(Expression<Func<TEntity, bool>> whereCondition);
    Task<long> Count();
    Task<IEnumerable<TEntity>> BulkInsert(IEnumerable<TEntity> entities);
    Task BulkUpdate(IEnumerable<TEntity> entities);
}

for example IAddressServices:

public interface IAddressService : IServiceBase<Address, Guid>
{
    Task<Address> VerifyAddress(Address address);
}

ServiceBase:

public abstract class ServiceBase<TEntity, TRepository, TPrimaryKey> : IServiceBase<TEntity, TPrimaryKey>
        where TEntity : class
        where TPrimaryKey : struct
        where TRepository : IBaseRepository<TEntity, TPrimaryKey>
{
    public TRepository Repository;

    public ServiceBase(IBaseRepository<TEntity, TPrimaryKey> rep)
    {
        Repository = (TRepository)rep;
    }

    public virtual async Task<TEntity> GetById(TPrimaryKey id)
    {
        return await Repository.Find(id);
    }

    public async Task<TEntity> GetSingle(Expression<Func<TEntity, bool>> whereCondition)
    {
        return await Repository.GetQueryable().Where(whereCondition).FirstOrDefaultAsync();
    }

    public async Task<IEnumerable<TEntity>> GetAll()
    {
        return await Repository.GetAll();
    }

    public IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> whereCondition)
    {
        return Repository.GetQueryable().Where(whereCondition);
    }

    public IQueryable<TEntity> GetAllQueryable()
    {
        return Repository.GetQueryable();
    }

    public IQueryable<TEntity> Query(Expression<Func<TEntity, bool>> whereCondition)
    {
        return Repository.GetQueryable().Where(whereCondition);
    }

    public virtual async Task<TEntity> Create(TEntity entity)
    {
        return await Repository.Add(entity);
    }

    public virtual async Task Delete(TEntity entity)
    {
        await Repository.Delete(entity);
    }

    public virtual async Task Update(TEntity entity)
    {
        await Repository.Edit(entity);
    }

    public async Task<long> Count(Expression<Func<TEntity, bool>> whereCondition)
    {
        return await Repository.GetQueryable().Where(whereCondition).CountAsync();
    }

    public async Task<long> Count()
    {
        return await Repository.GetQueryable().CountAsync();
    }       

    public async Task<IEnumerable<TEntity>> BulkInsert(IEnumerable<TEntity> entities)
    {
        return await Repository.BulkInsert(entities);
    }

    public async Task BulkUpdate(IEnumerable<TEntity> entities)
    {
        await Repository.BulkUpdate(entities);
    }
}

and concrete implementations of services:

AddressService:

public class AddressService : ServiceBase<Address, IBaseRepository<Address, Guid>, Guid>, IAddressService
{
    public AddressService(IBaseRepository<Address, Guid> rep) : base(rep)
    {
    }

    public async Task<Address> VerifyAddress(Address address)
    {
        //logic
    }
}

ProductController:

 public class ProductController : ControllerBase
    {
        private readonly IProductService _productService;
        private readonly IAddressService _addressService;
        private readonly ILogger _logger;
        private readonly IMapper _mapper;

        public ProductController (IProductService productService,
            IAddressService addressService,
            ILogger<ProductController> logger,
            IMapper mapper)
        {
            _packageService = packageService;
            _addressService = addressService;
            _logger = logger;
            _mapper = mapper;
        }

        [HttpGet]
        public async Task<IActionResult> GetAllProductsWithAddresses()
        {
            try
            {
                var products = await _productService.GetAllQueryable().Include(x => x.Address).ToListAsync();

                return Ok(_mapper.Map<List<ProductResponse>>(products));
            }
            catch (Exception e)
            {
                _logger.LogError($"An unexpected error occured: ${e}");
                return StatusCode(StatusCodes.Status500InternalServerError);
            }
        }
    }

Lets say for example if I had an POST endpoint in ProductController where I need to insert data in 3 different database tables: Address, ProductSize and ProductImage. I would have 3 services and I would call _addressService.Add(address), _productSize.Add(productSize) and _productImageService(image) in my controller. How can I support transactions here if DatabaseContext is located in BaseRepository, what is the best practice?

Upvotes: 1

Views: 713

Answers (1)

David Browne - Microsoft
David Browne - Microsoft

Reputation: 89091

How can I support transactions here if DatabaseContext is located in BaseRepository, what is the best practice?

Best practice is to throw out all that junk and just have your controller talk to the DbContext, or have the controller talk to a business service that talks to the DbContext.

Barring that DI should inject the same DbContext instance in each service or repository, so you can expose Unit-of-work methods on any one of them. Or you could introduce an additional service for managing cross-repo operations. EG

public class UnitOfWork
{
    DbContext db;
    public UnitOfWork(DbContext db)
    {
        this.db = db;
    }

    IDbContextTransaction BeginTransaction() => db.Database.BeginTransaction();
    void CommitTransaction() => db.Database.CommitTransaction();

    int SaveChanges() => db.SaveChanges();
}

Upvotes: 1

Related Questions