Home > Mobile >  What is the correct way to represent a generic entity in Clean Architecture
What is the correct way to represent a generic entity in Clean Architecture

Time:08-12

I am currently developing a set of C# libraries that contain interfaces and other utilities to use in other Clean Architecture projects. They are 4 in total:

  • Domain (Entities, Repositories interfaces, etc.)
  • Application (Commands, Queries, Handlers, Mappers, Services, etc.)
  • Infrastructure (Implementations of various interfaces)
  • Api (Web Apis, mainly Controllers)

As I would like to be able to use different Databases for different purposes (SQL, Document, Key-Value, etc.), I tried to code a generic Entity interface at the Domain layer. Then, I coded some generic repositories based on the generic Entity.

public interface IEntity
    {
    }

public interface IRepository<T, ID> where T : class
    {
    }

public interface ICrudRepository<T, ID> : IRepository<T, ID> where T : class
    {
        void InsertOne(T entity);

        Task InsertOneAsync(T entity);

        void InsertMany(IEnumerable<T> entities);

        Task InsertManyAsync(IEnumerable<T> entities);

        void ReplaceOne(T entity);

        Task ReplaceOneAsync(T entity);

        void DeleteOne(Expression<Func<T, bool>> filterExpression);

        Task DeleteOneAsync(Expression<Func<T, bool>> filterExpression);

        void DeleteById(ID id);

        Task DeleteByIdAsync(ID id);

        void DeleteMany(Expression<Func<T, bool>> filterExpression);

        Task DeleteManyAsync(Expression<Func<T, bool>> filterExpression);
    }

Coming to the Infrastructure layer, if I understand correctly the Clean Architecture paradigm, I should place at this level implementations of Repositories interfaces. Unlike the Domain layer, at this level I can and should code implementations tied to specific technologies and databases. So I added this implementation.

public class MongoRepository<TEntity> : ICrudRepository<TEntity, ObjectId> where TEntity : class, IMongoEntity
    {
        private readonly IMongoCollection<TEntity> _collection;

        public MongoRepository(IMongoCollection<TEntity> collection)
        {
            // TODO: Settings from ENV Variables in Docker Container
            _collection = collection;
        }

        public virtual void InsertOne(TEntity entity) => _collection.InsertOne(entity);

        public virtual async Task InsertOneAsync(TEntity entity) => await _collection.InsertOneAsync(entity);

        public void InsertMany(IEnumerable<TEntity> entities) => _collection.InsertMany(entities);

        public virtual async Task InsertManyAsync(IEnumerable<TEntity> entities) => await _collection.InsertManyAsync(entities);

        public void ReplaceOne(TEntity entity) => _collection.FindOneAndReplace(Builders<TEntity>.Filter.Eq(previous => previous.Id, entity.Id), entity);

        public virtual async Task ReplaceOneAsync(TEntity entity) 
            => await _collection.FindOneAndReplaceAsync(Builders<TEntity>.Filter.Eq(previous => previous.Id, entity.Id), entity);

        public void DeleteOne(Expression<Func<TEntity, bool>> filterExpression) => _collection.FindOneAndDelete(filterExpression);

        public async Task DeleteOneAsync(Expression<Func<TEntity, bool>> filterExpression) => await _collection.FindOneAndDeleteAsync(filterExpression);

        public void DeleteById(ObjectId id) => _collection.FindOneAndDelete(Builders<TEntity>.Filter.Eq(entity => entity.Id, id));

        public async Task DeleteByIdAsync(ObjectId id) => await _collection.FindOneAndDeleteAsync(Builders<TEntity>.Filter.Eq(entity => entity.Id, id));

        public void DeleteMany(Expression<Func<TEntity, bool>> filterExpression) => _collection.DeleteMany(filterExpression);

        public async Task DeleteManyAsync(Expression<Func<TEntity, bool>> filterExpression) => await _collection.DeleteManyAsync(filterExpression);
    }

As you can see, this implementation references an interface named IMongoEntity.

public interface IMongoEntity : IEntity
    {
        [BsonId]
        [BsonRepresentation(BsonType.String)]
        ObjectId Id { get; set; }
    }

This interface extends the Domain layer generic IEntity, and it is currently defined in the Infrastructure layer. The reason is that this kind of entity is tied to a specific Database, for instance MongoDB. At the same time, I had to define it in this set of libraries beacuse multiple projects might reference the library and all should implement IMongoEntity (if they have to query MongoDB, of course). One final consideration, and the reason behind this question, is that in the current state an example project should have Domain layer referencing the library's Infrastructure layer. I don't think it's a good idea, but still I could not find a solution so far.

TLDR: If I have a generic Entity at the Domain layer, what is the correct way in Clean Architecture to implement it for specific Databases? At which layer should I place these implementations?

CodePudding user response:

First, I never use my "orm entities" as my "dto" (aka "poco") objects. I separate these.

The above is a mild holy war.

..

Second, I have a slightly different layering.

(below is bottom to top)

Domain Layer (here are the poco's, and NO REFERENCES to other heavy libraries)

..

​IDomainDataLayer (code to an interface, not a concrete)

References ->​ Domain

..

ConcreteDomainDataLayer (Today, it might be NHibernate, Dapper or EF-Core, but tomorrow you might change) (and here are my Orm-ENTITIES definitions)

References: ->​Domain, IDomainDataLayer

..

BusinessLogicLayer

References -> (Domain, IDomainDataLayer). (and DO NOT REFERENCE -> ConcreteDomainDataLayer)

..

(Top Layers)

RestLayer

Console Line Exe

In my INTERFACES domain-data-layer I have

DomainDataLayer.Interfaces.BaseContracts
{


public interface IDataRepository<in TKey, TPoco>
{
    Task<int> GetCountAsync(CancellationToken token);

    Task<IEnumerable<TPoco>> GetAllAsync(CancellationToken token);

    Task<TPoco> GetSingleAsync(TKey keyValue, CancellationToken token);

    Task<TPoco> AddAsync(TPoco poco, CancellationToken token);

    Task<TPoco> UpdateAsync(TPoco poco, CancellationToken token);

    Task<int> DeleteAsync(TKey keyValue, CancellationToken token);
}

and let's pick some kind of concrete

public class EmployeeDomainDataLayer : IDataRepository<long, EmployeePoco>

It is imperative that this is based on the TPoco (dto) and NOT the Orm-Entity.

Now, where does the ORM come into play?

When I write a ConcreteDomainDataLayer (EF for example), the "public contract" is all about the Poco's.

Inside my code for any ConcreteDomainDataLayer, I define my OrmEntities, that usually "mimic" my Poco's (at least in the beginning).

and I pick one method of the IDataRepository

  public async Task<EmployeePoco> GetSingleAsync(long keyValue, CancellationToken token)
{
     // use the keyValue to look up the EmployeeOrmEntity from the dbContext (here is where my await call will be)
     //convert the EmployeeOrmEntity into an EmployeePoco
      
     // return the EmployeePoco

}

I do not do "manual conversion".

I actually have a decent "base class" using : MapsterMapper

using MapsterMapper;

namespace DomainDataLayer.EntityFramework.Converters { public class ConverterBase<TEntity, TVDto> : IConverter<TEntity, TVDto> { public const string ErrorMessageIMapperNull = "IMapper is null";

    private readonly IMapper mapper;

    public ConverterBase(IMapper mapper)
    {
        this.mapper = mapper ?? throw new ArgumentNullException(ErrorMessageIMapperNull, (Exception)null);
    }

    public virtual TVDto ConvertToDto(TEntity entity)
    {
        return this.mapper.Map<TVDto>(entity);
    }

    public virtual ICollection<TVDto> ConvertToDtos(ICollection<TEntity> entities)
    {
        return this.mapper.Map<ICollection<TVDto>>(entities);
    }

    public virtual ICollection<TEntity> ConvertToEntities(ICollection<TVDto> dtos)
    {
        return this.mapper.Map<ICollection<TEntity>>(dtos);
    }

    public virtual TEntity ConvertToEntity(TVDto dto)
    {
        return this.mapper.Map<TEntity>(dto);
    }
}

and I define a subclass

public class EmployeeConverter : ConverterBase<EmployeeEntity, Poco>,
    IEmployeeConverter
{
    public EmployeeConverter(IMapper mapper) : base(mapper)
    {
    }
}

and this gets injected into my EmployeeDomainDataLayer .. to handle that "concern".

How does this help me?

When I need to change to a different "provider" (let's say I move from EF to a NoSql solution)......

my DomainDataLayer.INTERFACES do not change. Thus my business logic does not change.

I substitute a different ConcreteDomainDataLayer(NoSql version) for my DomainDataLayer.INTERFACES (through IoC control) .. and I've isolated what I need to change.

In the beginning, the separation of Orm-Entities and POCO's may appear to be overkill.

But over time (code maintenance) as the orm-entity and poco might diverge...this keeps things very clean and separated.

sidenote, I write code for long term maintenance. I am not interested in "rapid, get er done" coding practices.

..

I was compelled to figure out a clean architecture after my diabolical time when my company was acquired and we had to switch from RdbmsOne to ANOTHER_RDBMS, and it was a nightmare. People had violated the rules of layering left and right. Now, since I've done this, I know exactly what I need to create to support a different backend-datastore (rdbms, nosql, redis-cache), it does not matter too much, as I have all my business logic contracts based on the domain-data-layer.INTERFACES that is based on pocos, not orm-entities.

CodePudding user response:

We have been struggling with the same concerns quite a lot in my previous company but the problem is simple. (Not the solution though)

public interface IMongoEntity : IEntity
{
    [BsonId] // Infrastructure concern
    [BsonRepresentation(BsonType.String)] // Infrastructure concern
    ObjectId Id { get; set; } // Domain concern
}

Boom. You just coupled your infrastructure with your domain logic but there is no other way if you want to use Attributes.

One simple approach is to have some unavoidable duplicate property code.

Domain layer.

public interface IUser
{
   int Id
}

Infra layer

public class MongoUser : IUser , IEntity
{
   [Your infra related attributes here]
   int Id
}

This way also helps if you need to do some kind of transformation in your raw data . For example :

public class MongoUser : IUser , IEntity
{
   [Your infra related attributes here]
   object _id
   int Id => CustomTransformObjectToInt(this._id);
}

Another way in case your infrastructure allows it (Like entity framework), is to use seperate FLUENT configuration code.

Domain layer

public class User : IEntity
{
   int Id
}

Infra layer

modelBuilder.Entity<User>()
        .Property(u => u.Id)
        .IsRequired();

This way you get to keep your Domain properties seperate from the DataAccess instructions.

Although some libs like Dapper do not offer such fluent options so u might also try work with some kind of Custom Attribute & Reflection wizardry but this is a dark path.......

  • Related