4
\$\begingroup\$

I use the Repository/Service design pattern in my projects. I have doubts whether it is worth using common services

BaseEntity:

 public class BaseEntity
    {
        private DateTime _addedDate;
        private DateTime _modifiedDate;
        protected BaseEntity()
        {
            Id = Guid.NewGuid();
            AddedDate = DateTime.UtcNow;
        }

        [Key]
        public Guid Id { get; set; }
        public DateTime AddedDate
        {
            get => DateTime.SpecifyKind(_addedDate, DateTimeKind.Utc);
            private set => _addedDate = value;
        }
        public DateTime ModifiedDate
        {
            get => DateTime.SpecifyKind(_modifiedDate, DateTimeKind.Utc);
            set => _modifiedDate = value;
        }
    }

IGenericRepository:

   public interface IGenericRepository<T> where T : class
    {
        Task<T> FirstAsync(Expression<Func<T, bool>> predicate);
        Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate);

        /// <summary>
        /// Get all queries
        /// </summary>
        /// <returns>IQueryable queries</returns>
        IQueryable<T> GetAll();

        /// <summary>
        /// Find queries by predicate
        /// </summary>
        /// <param name="predicate">search predicate (LINQ)</param>
        /// <returns>IQueryable queries</returns>
        IQueryable<T> FindBy(Expression<Func<T, bool>> predicate);

        /// <summary>
        /// Find entity by keys
        /// </summary>
        /// <param name="keys">search key</param>
        /// <returns>T entity</returns>
        Task<T> FindAsync(params object[] keys);

        /// <summary>
        /// Add new entity
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        Task AddAsync(T entity);

        /// <summary>
        /// Remove entity from database
        /// </summary>
        /// <param name="entity"></param>
        void Delete(T entity);

        /// <summary>
        /// Remove entity from database
        /// </summary>
        /// <param name="keys">entity keys</param>
        void Delete(params object[] keys);

        /// <summary>
        /// Edit entity
        /// </summary>
        /// <param name="entity"></param>
        Task UpdateAsync(T entity);

        /// <summary>
        /// Persists all updates to the data source.
        /// </summary>
        void SaveChanges();
        Task SaveChangesAsync();
    }

GenericRepository:

 public class GenericRepository<T> : IGenericRepository<T> where T : BaseEntity
    {
        private readonly DbContext _context;
        private readonly DbSet<T> _dbSet;

        public GenericRepository(DbContext context)
        {
            _context = context;
            _dbSet = context.Set<T>();
        }
        public virtual async Task<T> FirstAsync(Expression<Func<T, bool>> predicate)
        {
            return await _dbSet.FirstAsync(predicate);
        }

        public virtual async Task <T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate)
        {
            return await _dbSet.FirstOrDefaultAsync(predicate);
        }

        public virtual IQueryable<T> GetAll()
        {
            return _dbSet.AsNoTracking();
        }

        public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
        {
            return _dbSet.Where(predicate);
        }

        public async Task<T> FindAsync(params object[] keys)
        {
            return await _dbSet.FindAsync(keys);
        }

        public virtual async Task AddAsync(T entity)
        {
            await _dbSet.AddAsync(entity);
        }

        public virtual void Delete(T entity)
        {
            _dbSet.Remove(entity);
        }

        public virtual void Delete(params object[] keys)
        {
            var entity = _dbSet.Find(keys);
            _dbSet.Remove(entity);
        }

        public virtual async Task UpdateAsync(T entity)
        {
            var existing = await _dbSet.FindAsync(entity.Id);
            if (existing != null)
            {
                existing.ModifiedDate = DateTime.UtcNow;
                _context.Entry(existing).CurrentValues.SetValues(entity);
                _context.Entry(existing).Property("AddedDate").IsModified = false;
            }
        }

        public virtual void SaveChanges()
        {
            _context.SaveChanges();
        }

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

CommonService:

   public class CommonService<T> : ICommonService<T> where T : BaseEntity
    {
        private readonly IGenericRepository<T> _repository;

        public CommonService(IGenericRepository<T> repository)
        {
            _repository = repository;
        }

        public virtual async Task<T> FirstAsync(Expression<Func<T, bool>> predicate)
        {
            return await _repository.FirstAsync(predicate);
        }

        public virtual async Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate)
        {
            return await _repository.FirstOrDefaultAsync(predicate);
        }

        public virtual IQueryable<T> GetAll()
        {
            return _repository.GetAll();
        }

        public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
        {
            return _repository.FindBy(predicate);
        }

        public async Task<T> FindAsync(params object[] keys)
        {
            return await _repository.FindAsync(keys);
        }

        public virtual async Task AddAsync(T entity)
        {
            await _repository.AddAsync(entity);
            await _repository.SaveChangesAsync();
        }

        public virtual async Task DeleteAsync(T entity)
        {
            _repository.Delete(entity);
            await _repository.SaveChangesAsync();
        }

        public virtual async Task DeleteAsync(params object[] keys)
        {
            var entity = await _repository.FindAsync(keys);
            _repository.Delete(entity);
            await _repository.SaveChangesAsync();
        }

        public virtual async Task UpdateAsync(T entity)
        {
            await _repository.UpdateAsync(entity);
            await _repository.SaveChangesAsync();
        }
    }

ConcreteService:

public class DepartmentService : CommonService<Department>, IDepartmentService
    {
        private readonly IGenericRepository<Department> _repository;

        public DepartmentService(IGenericRepository<Department> repository) : base(repository)
        {
            _repository = repository;
        }
    }

Questions:

  1. Is it good practice that I use the shared service as a base class to avoid duplicate code?

  2. Do I need to create empty services if all the necessary operations are in the base class (CommonService), or in this case, always use common services (inject ICommonService<EntityName> and usage and create specific service classes only when there is a specific 'non-genericable' logic in that?

\$\endgroup\$
  • 1
    \$\begingroup\$ Even a generic repository is disputable. What are the driving forces behind your architecture? It's hard to give any advice as to what is "normal" without knowing why you need all these layers. \$\endgroup\$ – Gert Arnold Jul 25 '17 at 22:32
  • \$\begingroup\$ So the common-service just adds the save operation to the repository? This doesn't look right. Why did you implement it this way? \$\endgroup\$ – t3chb0t Jul 26 '17 at 4:20
  • \$\begingroup\$ @t3chb0t I need Repository for access to context (i think use dbcontext in 'common' or another service not good way) i use common service to avoid duplication same code, but my service can contains custom code, so i dont know i need create empty service and usage common service as base class, or need usage always commonserivce and create 'custom' service only when i have additional logic? \$\endgroup\$ – Artem Polishchuk Jul 30 '17 at 7:49
  • 1
    \$\begingroup\$ What happens when you need to create a stored procedure (or w/e db equivalent you're using) because EF doesn't perform well enough (or insert some other reason). Are you adding that function to the repository? If so, is it then still an IGenericRepository or just an IRepository? Just pointing out that there's a decent chance there will be an exception in this architecture, best to know how you're going to handle it ahead of time. I use nearly this exact architecture and I've been running into exceptions ever since. \$\endgroup\$ – Shelby115 May 29 '18 at 18:12
3
\$\begingroup\$

Review

  • Your design uses a dedicated repository and service for each entity. This might look like a great idea at first, but you'll soon realise the drawbacks of this design.
  • Having a service as a glorified wrapper of a repository doesn't add much value to the service.
  • I would question whether a service should yield IQueryable. I find a service to be in charge of encapsulation of your domain. Return materialized collections instead.
  • Endpoints that require data lookup should directly call the repository, while endpoints that require business logic should pass the service.

Repository

The repository is the lower layer of the two. I could imagine a repository for an entity makes sense. However, you might get some conflicting concerns. Let's say we have an entity Offer with a list of OfferLines. Which repository is responsible for what when we save an offer?

  • OfferRepository saves offer, and offer lines are saved by OfferLineRepository?
  • OfferRepository saves offer together with all child entities?
  • Both repositories allow to save offer lines?

Service

A service is higher level layer. Defining a service by entity might be too technical. Consider a service as a facade for a feature, or a set of related concepts. Having an OfferService and OfferLineService don't make sense to me.

Aggregate Root

A more meaningful design is to pick an aggregate root for a set of entities. These entities may include classes that represent tables, views, complex types and memory-only classes that all share a same concept. I would design both layers this way: OfferRepository and OfferService. OfferLines are part of the aggregate root Offer.

So is it worth using common services and repositories... I think it is, but focus on grouping by functionality, not by entity type.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.