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:
Is it good practice that I use the shared service as a base class to avoid duplicate code?
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 (injectICommonService<EntityName>
and usage and create specific service classes only when there is a specific 'non-genericable' logic in that?
1 Answer 1
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.
Explore related questions
See similar questions with these tags.
IGenericRepository
or just anIRepository
? 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\$