1

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?

5
  • 2
    Services should not separated out like this, they are more like repositories. A service should deal with an aspect of business logic that implements everything associated with that aspect and has access to the database context. This way it is able to perform all the inserts, updates and deletes that are necessary to implement the required business logic. Commented Feb 23, 2021 at 14:49
  • A Controller is used in Client for parsing a response from the Server. You want to Post data to the server. Commented Feb 23, 2021 at 14:52
  • The problem of using repositories with ef is that it's an anti-pattern. You've already have repositories in the form of DbSet<> and a unit of work in the DbContext. You haven't created a unit of work here, which would ensure your transaction boundaries. but why do that. it's already in ef and you are putting a duplicate layer on top of it Commented Feb 23, 2021 at 15:38
  • Everything buried in useless layers that each take you further away from what you actually want to do. With EF you can very simple save an object graph in one transactional operation without even being aware of transactions. Commented Feb 23, 2021 at 16:20
  • @GertArnold Yes. but if someone inadvertently call SaveChanges() in the middle, it immediately commits and breaks the transaction. Commented Feb 23, 2021 at 16:36

1 Answer 1

1

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();
}
Sign up to request clarification or add additional context in comments.

12 Comments

What exactly is junk here? Can you provide some tips or code example what would you remove since I am not sure what you mean by junk?
Litterally every class you posted other than the controller is utterly unnecessary.
I would respectfully disagree that only controller is necessary here. How would that code look like if application grows? I am trying to apply clean coding practices, separation of concerns and N-Tier architecture. It is possible that I am not doing it the best, but I think saying everything is utterly unnecessary sounds rough to me.
KISS and YAGNI. Your whole question boils down to solving a problem that you created for yourself by introducing a bunch of abstractions between your application code and your real repository -- your DbContext subtype.
So you are saying the code designed by clean architecture would look like this: 1. Controler definition 2. dbContext.BeginTransaction... 3. dbContext.Add.Users(user) 4. dbContext.Add.Roles(role) 5. dbContext.Add.Somethings(Something) 6. dbContext.CommitTransaction 7. return response and status code Plus, in between each of those 7 bullet points you would have some logic like if statements, foreachs etc.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.