0

I want to use Shared Service that include some common methods like create, update, delete in my business layer. I have already implemented repository and unit of work classes but I encountered some problems while trying to create a shared service. Lets assume that we have a non-shared create method like this:

public class ProductService : IProductService
{
    private readonly IUnitOfWork _unitOfWork;
    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    public async Task<Product> CreateProduct(Product newProduct)
    {
        await _unitOfWork.Products.AddAsync(newProduct);
        await _unitOfWork.CommitAsync();
        return newProduct;
    }
}

The part that confuses me in the above code is, I call my UnitOfWork with _unitOfWork.Product command, how do we convert it to unitOfWork.TEntity to make generic? Is it possible? In this case, I tried doing generic but I guess there is no such thing as _unitOfWork.TEntity. I just need to edit the Service class, I add other related classes to give extra information.

Service:

    public class Service<TEntity>: IService<TEntity> where TEntity : class
    {
        private readonly IUnitOfWork _unitOfWork;
        public Service(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }
        public async Task<TEntity> AddAsync(TEntity entity)
        {
            await _unitOfWork.TEntity.AddAsync(entity);
            await _unitOfWork.CommitAsync();
            return entity;
        }
    }

IService:

    public interface IService<TEntity> where TEntity : class
    {
        Task<TEntity> AddAsync(TEntity entity);
    }

Repository:

    public abstract class Repository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        private readonly DbContext _context;
        private readonly DbSet<TEntity> _dbSet;

        public Repository(ECommerceDbContext context)
        {
            _context = context;
            _dbSet = context.Set<TEntity>();
        }

        public async Task<TEntity> AddAsync(TEntity entity)
        {
            entity.CreateDate = DateTime.Now;
            await _dbSet.AddAsync(entity);
            await SaveAsync();
            return entity;
        }

        public async Task AddAsync(IEnumerable<TEntity> entities)
        {
            foreach (var item in entities)
            {
                await AddAsync(item);
            }
        }

        public async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> expression)
        {
            return await _dbSet.AnyAsync(expression);
        }

        public async Task<bool> AnyAsync()
        {
            return await AnyAsync(x => true);
        }

        public async Task<long> CountAsync()
        {
            return await CountAsync(x => true);
        }

        public async Task<long> CountAsync(Expression<Func<TEntity, bool>> expression)
        {
            return await _dbSet.LongCountAsync(expression);
        }

        public void Delete(TEntity model)
        {
            _dbSet.Remove(model);
        }

        public async Task DeleteAsync(int id)
        {
            var entity = await GetAsync(id);
            Delete(entity);
        }

        public async Task<TEntity> FirstOrDefault(Expression<Func<TEntity, bool>> expression)
        {
            return await _dbSet.FirstOrDefaultAsync(expression);
        }

        public async Task<TEntity> GetAsync(int id)
        {
            return await FirstOrDefault(x => x.Id == id);
        }

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

        public async Task SaveAsync()
        {
           await _context.SaveChangesAsync();
        }

        public async Task<TEntity> Update(TEntity entity)
        {
            var temp = GetAsync(entity.Id);
            entity.UpdateDate = DateTime.Now;
            _context.Entry(temp).CurrentValues.SetValues(entity);
            await SaveAsync();
            return await temp;
        }
    }

IRepository:

    public interface IRepository<TEntity> where TEntity :class
    {
        Task<TEntity> AddAsync(TEntity entity);
        Task AddAsync(IEnumerable<TEntity> entities);
        Task<bool> AnyAsync(Expression<Func<TEntity, bool>> expression);
        Task<bool> AnyAsync();
        Task<long> CountAsync();
        Task<long> CountAsync(Expression<Func<TEntity, bool>> expression);
        void Delete(TEntity model);
        Task DeleteAsync(int id);
        Task<TEntity> FirstOrDefault(Expression<Func<TEntity, bool>> expression);
        Task<TEntity> GetAsync(int id);
        Task<IEnumerable<TEntity>> GetAll();
        Task SaveAsync();
        Task<TEntity> Update(TEntity entity);
    }

UnitOfWork:

    public class UnitOfWork:IUnitOfWork
    {
        private readonly ECommerceDbContext _context;
        private ProductRepository _productRepository;

        public UnitOfWork(ECommerceDbContext context)
        {
            _context = context;
        }

        public IProductRepository Products => _productRepository = _productRepository ?? new ProductRepository(_context);

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

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

    public interface IUnitOfWork : IDisposable
    {
        IProductRepository Products { get; }
        Task<int> CommitAsync();
    }
Amit Joshi
  • 15,448
  • 21
  • 77
  • 141
alihangir
  • 25
  • 1
  • 7
  • If there is no generic implementation that meets your needs, you can always use an action/func callback pattern to apply an update to the scoped instance. – David L Dec 23 '20 at 22:38
  • Can't I implement a generic AddAsync method that can be useful for all entities? How can I use action/func callback pattern? Can you explain more? – alihangir Dec 24 '20 at 01:07
  • I added an answer with an example that explains the approach. – David L Dec 24 '20 at 02:16
  • Be aware that the generic repository is considered an [anti-pattern](https://stackoverflow.com/a/51781877/5779732). – Amit Joshi Jan 22 '21 at 06:59

2 Answers2

0

Service TEntity : class .. I think here class is TEntity : Entity.

It's helpful if you share your 'IProductRepository' and ProductRepository objects.

public interface IService<TEntity> where TEntity : Entity
{
    Task<TEntity> AddAsync(TEntity entity);
}

public class Service<TEntity> : IService<TEntity> where TEntity : Entity
{
    private readonly IUnitOfWork _unitOfWork;
    public Service(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    public async Task<TEntity> AddAsync(TEntity entity)
    {
        //await _unitOfWork.TEntity.AddAsync(entity);
        await _unitOfWork.Products.AddAsync(entity);
        await _unitOfWork.CommitAsync();
        return entity;
    }
}

public class ProductService : Service<Product>,IProductService
{
    public ProductService(IUnitOfWork unitOfWork):base(unitOfWork)
    {

    }
}

public class UnitOfWork : IUnitOfWork
{
    private readonly ECommerceDbContext _context;
    private IProductRepository _productRepository;

    public UnitOfWork(ECommerceDbContext context)
    {
        _context = context;
    }

    public IProductRepository Products => _productRepository = _productRepository ?? new ProductRepository(_context);

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

    public void Dispose()
    {
        _context.Dispose();
    }
}
  • Bro this is not generic, imagine that they are many entities like Product. Other entites will also use this method. So, you can't add here _unitOfWork.Products. ProductRepository inherited from Repository they are sharing same methods right now. – alihangir Dec 24 '20 at 00:36
  • Oh! I missed that. Need generic unit of work. Try at https://stackoverflow.com/questions/39343750/generic-unit-of-work – Prasad Ramireddy Dec 24 '20 at 01:31
  • It seems Generic UnitOfWork will solve the problem, but I can't implement Generic UnitOfWork. Any help would be most welcome. – alihangir Dec 24 '20 at 02:18
0

Because each entity type is a strongly typed object, you cannot genericize your implementation without reflection. A common workaround is to provide a generic callback while allowing the invoker to provide the specific mapping per entity type.

public class Service<TEntity> : IService<TEntity> where TEntity : class
{
    private readonly IUnitOfWork _unitOfWork;
    public Service(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public async Task<TEntity> AddAsync(TEntity entity, 
        Func<TEntity, IUnitOfWork, Task> addEntityCallback)
    {
        await addEntityCallback(entity, _unitOfWork);
        await _unitOfWork.CommitAsync();
        return entity;
    }
}

public interface IService<TEntity> where TEntity : class
{
    Task<TEntity> AddAsync(TEntity entity, Func<TEntity, IUnitOfWork, Task> addEntityCallback);
}

You can then call IService.AddAsync with a specific mapping:

public class ProductService : IProductService
{
    private readonly IService<Product> _service;
    public ProductService(IService<Product> service)
    {
        _service = service;
    }

    public async Task<Product> CreateProduct(Product newProduct)
    {
        await _service.AddAsync(newProduct, 
            (entity, unitOfWork) => unitOfWork.Products.AddAsync(entity));
        return newProduct;
    }
}

Update: in the case where you want to always inherit Service<TEntity> (per your comment), you could use an abstract method that functions in a similar manner to a callback parameter. This allows you to still encapsulate the logic in the ProductService but now no longer requires you to provide a create method.

public class ProductService : Service<Product>, IProductService
{
    public ProductService(IUnitOfWork unitOfWork) : base(unitOfWork)
    {
    }

    protected override async Task<Product> AddEntityCallback(Product entity, 
        IUnitOfWork unitOfWork)
    {
        await unitOfWork.Products.AddAsync(entity);
        return entity;
    }
}

public abstract class Service<TEntity> : IService<TEntity> where TEntity : class
{
    private readonly IUnitOfWork _unitOfWork;
    public Service(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    protected abstract Task<TEntity> AddEntityCallback(TEntity entity, 
        IUnitOfWork unitOfWork);
    
    public async Task<TEntity> AddAsync(TEntity entity)
    {
        await AddEntityCallback(entity, _unitOfWork);
        await _unitOfWork.CommitAsync();
        return entity;
    }
}
David L
  • 32,885
  • 8
  • 62
  • 93
  • There is a little misunderstanding here. Product Service should inherit from the base Service class, it's my fault I forgot to specify it. Thus, we will not need to define the insert, update, delete methods over and over again in each service. In other words, there should not be CreateProduct in ProductService, I was planning to call it from Service class in Controller. – alihangir Dec 24 '20 at 02:54
  • @alihangir ahh, that wasn't clear from your original question. I've updated my answer. You can still handle this by using an abstract method. You have to encapsulate the logic **somewhere** and doing it in a concrete, knowledgeable class will make it easier to maintain down the road. – David L Dec 24 '20 at 03:29
  • I have ProductController and async Task NewProduct(SaveProductDto saveProductDto) action in contoller. I just need to call AddAsync in this Action from ProductService by intherit from base Service. For example, _productService.AddAsync(do something) – alihangir Dec 24 '20 at 03:31
  • Right, and the updated approach will enable that. – David L Dec 24 '20 at 03:35
  • Works very very well, thanks. I should mention that ProductService also wanted to implement this: async Task IProductService.AddEntityCallback(Product entity, IUnitOfWork unitOfWork). I don't now why it forced me to do this. We have both 1)protected async override Task AddEntityCallback(Product entity, IUnitOfWork unitOfWork) and 2)async Task IProductService.AddEntityCallback(Product entity, IUnitOfWork unitOfWork) at the same time. – alihangir Dec 24 '20 at 05:02