Introduction
This is a continuation of my first post Implementing a Clean Architecture in ASP.NET Core 6, and describes the method I’ve used to achieve clear separation of data models and domain models. In this post, I’ll be expressing my views on the clear separation of persistence related models and business models and look into how we can effectively decouple the data and domain model when using EF Core.
First, domain model. Second, data model
Over the last few EF Core iterations, there was a great deal of effort made to cater to tactical Domain-Driven Design practices when working with data entities (i.e. private field accessors). However, I personally feel that, even though we are now able to “combine” a data model and a domain model into one EF Core entity, the separation is still not entirely clear, and more often than not, you end up having persistence details “leaking” into your business/domain models (i.e. navigation properties). This can quickly lead to high-coupling of your behavioral model with the database domain.
So my rule is this: Data entities and any persistence-related code should be kept ONLY in the Infrastructure layer and never be allowed to leave! For this reason, domain layer repositories shall never return anything that resembles a data entity. Conceptually, data entities are objects which represent the data in some form of persistence. They have no business – pun intended – in the domain layer. Data entities should be anemic POCO objects which are strictly a representation of whatever persistence method you are using. I don’t fancy the idea of having attributes relating to persistence or navigation properties in my domain model.
Additionally, you may need to persist some entities in your project, which are not conceptually part of your business domain layer. For example, application-wide configurations or tenant-specific parameters are very specific to the application itself and are best suited outside the application-independent business layer. However, from a management point-of-view you would still want to be able to add/remove/edit those entities from whatever presentation/UI medium you are using.
For both use-cases above, we’ll be using repositories to cover them.
Best of both worlds
The plan is to have two types of repositories, one that acts upon data entities and another that acts upon domain entities. For that, let first create a basic abstraction which both will depend on.
To achieve this, I have an IEntityRepository which operates on classes implementing the generic IEntity interface. Notice that the IEntityRepository also implements the IReadRepository and IWriteRepository generic interfaces.
Below is the code for all aforementioned interfaces:
public interface ** IEntity ** < TId > {
TId Id {
get;
}
}
public interface ** IReadRepository ** < T > {
bool Exists(Expression < Func < T, bool >> predicate);
Task < bool > ExistsAsync(Expression < Func < T, bool >> predicate);
IQueryable < T > GetAll();
IQueryable < T > GetBy(Expression < Func < T, bool >> predicate);
T GetFirst(Expression < Func < T, bool >> predicate);
Task < T > GetFirstAsync(Expression < Func < T, bool >> predicate);
T GetSingle(Expression < Func < T, bool >> predicate);
Task < T > GetSingleAsync(Expression < Func < T, bool >> predicate);
}
public interface ** IWriteRepository ** < T > : IRepository {
void Add(T entity);
ValueTask AddAsync(T entity);
void AddRange(IEnumerable < T > entities);
Task AddRangeAsync(IEnumerable < T > entities);
void Delete(T entity);
void DeleteRange(IEnumerable < T > entities);
void Update(T entity);
void UpdateRange(IEnumerable < T > entities);
}
public interface IRepository {
IUnitOfWork UnitOfWork {
get;
}
}
public interface ** IEntityRepository ** < T, TId > : IReadRepository < T > ,
IWriteRepository < T > where T: class, IEntity < TId > {
T Find(TId id);
Task < T > FindAsync(TId id);
}
The data model repository
So far we have a pretty basic, albeit rich, abstraction for a repository. Moving forward, what we want to do is make the distinction between data entities and domain entities become clear. Firstly, we’ll create a basic implementation of a repository for retrieving data entities from our persistence store. This repository will operate on classes implementing the generic IDataEntity marker interface.
The code for the interface and concrete implementation of the data entities repository is shown below:
public interface **IDataEntity**<TId> : IEntity<TId>{}
internal class ** DataEntityRepository ** < T, TId > : IEntityRepository < T, TId > where T: class, IDataEntity < TId > {
private readonly DbSet < T > _entities;
private readonly ApplicationDbContext _context;
public IUnitOfWork UnitOfWork => _context;
public DataEntityRepository(ApplicationDbContext context) {
_context = context ??
throw new ArgumentNullException(nameof(context));
_entities = context.Set < T > ();
}
public void Add(T entity) {
Guard.Against.Null(entity, nameof(entity));
_entities.Add(entity);
}
public async ValueTask AddAsync(T entity) {
Guard.Against.Null(entity, nameof(entity));
await _entities.AddAsync(entity);
}
public void AddRange(IEnumerable < T > entities) {
_entities.AddRange(entities);
}
public Task AddRangeAsync(IEnumerable < T > entities) {
return _entities.AddRangeAsync(entities);
}
public void Delete(T entity) {
Guard.Against.Null(entity, nameof(entity));
_entities.Remove(entity);
}
public void DeleteRange(IEnumerable < T > entities) {
Guard.Against.Null(entities, nameof(entities));
_entities.RemoveRange(entities);
}
public bool Exists(Expression < Func < T, bool >> predicate) {
return _entities.Any(predicate);
}
public Task < bool > ExistsAsync(Expression < Func < T, bool >> predicate) {
return _entities.AnyAsync(predicate);
}
public IQueryable < T > GetAll() {
return GetEntities();
}
public IQueryable < T > GetBy(Expression < Func < T, bool >> predicate) {
return GetEntities().Where(predicate);
}
public T GetFirst(Expression < Func < T, bool >> predicate) {
return GetEntities().FirstOrDefault(predicate);
}
public async Task < T > GetFirstAsync(Expression < Func < T, bool >> predicate) {
return await GetEntities().FirstOrDefaultAsync(predicate);
}
public T GetSingle(Expression < Func < T, bool >> predicate) {
return GetEntities().SingleOrDefault(predicate);
}
public async Task < T > GetSingleAsync(Expression < Func < T, bool >> predicate) {
return await GetEntities().SingleOrDefaultAsync(predicate);
}
public void Update(T entity) {
Guard.Against.Null(entity, nameof(entity));
_entities.Update(entity);
}
public void UpdateRange(IEnumerable < T > entities) {
Guard.Against.Null(entities, nameof(entities));
foreach(var entity in entities) {
Update(entity);
}
}
public T Find(TId id) {
return _entities.Find(id);
}
public async Task < T > FindAsync(TId id) {
return await _entities.FindAsync(id);
}
private IQueryable < T > GetEntities(bool asNoTracking = true) {
if (asNoTracking) return _entities.AsNoTracking();
return _entities;
}
}

Join the conversation! Your thoughts help the community grow.