在Clean Architecture中表示通用实体的正确方式是什么



我目前正在开发一组C#库,其中包含用于其他Clean Architecture项目的接口和其他实用程序。它们总共有4个:

  • 域(实体、存储库接口等)
  • 应用程序(命令、查询、处理程序、映射程序、服务等)
  • 基础设施(各种接口的实现)
  • Api(Web Api,主要是控制器)

由于我希望能够将不同的数据库用于不同的目的(SQL、文档、键值等),我尝试在域层编写一个通用实体接口。然后,我基于通用实体对一些通用存储库进行了编码。

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);
}

说到基础结构层,如果我正确理解了Clean Architecture范式,我应该将存储库接口的实现放在这个级别。与域层不同,在这个级别上,我可以也应该编写与特定技术和数据库相关的代码实现。所以我添加了这个实现。

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);
}

正如您所看到的,这个实现引用了一个名为IMongoEntity的接口。

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

此接口扩展了域层通用IEntity,并且它当前在基础结构层中定义。原因是这种实体绑定到特定的数据库,例如MongoDB。同时,我必须在这组库中定义它,因为多个项目可能引用该库,并且所有项目都应该实现IMongoEntity(当然,如果他们必须查询MongoDB)。最后一个考虑因素,也是这个问题背后的原因,是在当前状态下,示例项目应该具有引用库的基础结构层的域层。我认为这不是一个好主意,但到目前为止我仍然找不到解决方案。

TLDR:如果我在域层有一个通用实体,那么在Clean Architecture中为特定数据库实现它的正确方法是什么?我应该把这些实现放在哪一层?

首先,我从不使用我的"orm实体";作为我的";dto";(又称"poco")对象。我把这些分开。

以上是一场温和的圣战。

其次,我的层次略有不同。

(从下到上)

域层(这里是poco的,没有引用其他重型库)

​IDomainDataLayer(代码到接口,而不是具体的)

参考文献->​域

ConcreteDomainDataLayer(今天,它可能是NHibernate、Dapper或EF核心,但明天你可能会改变)(这是我的Orm实体定义)

参考文献:->​域,IDomainDataLayer

BusinessLogicLayer

参考文献->(域,IDomainDataLayer)。(并且不要引用->ConcreteDomainDataLayer)

(顶层)

RestLayer

控制台线路Exe

在我的INTERFACES域数据层中,我有

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);
}

让我们挑选一种混凝土

public class EmployeeDomainDataLayer : IDataRepository<long, EmployeePoco>

这必须基于TPoco(dto)而非Orm实体。

现在,ORM在哪里发挥作用?

当我编写ConcreteDomainDataLayer(例如EF)时;公共合同;都是关于波科的。

在我的任何ConcreteDomainDataLayer的代码中,我定义了我的OrmEntities,它通常是";模仿";我的Poco(至少在一开始)。

我选择了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
}

我不做";手动转换";。

事实上,我有一个体面的";基类";使用:MapsterMapper

使用MapsterMapper;

命名空间DomainDataLayer。EntityFramework。转换器{公共类ConverterBase<TEntity,TVDto>:IC转换器<TEntity、TVDto>{公共常量字符串ErrorMessageIMapperNull=";IMapper为空";;

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);
}
}

我定义了一个子类

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

并将其注入到我的EmployeeDomainDataLayer中。。以处理";关注";。

这对我有什么帮助?

当我需要换一个不同的";提供者";(假设我从EF转到NoSql解决方案)。。。。。。

我的DomainDataLayer。界面不变。因此,我的商业逻辑不会改变。

我用不同的ConcreteDomainDataLayer(NoSql版本)代替我的DomainDataLayer。接口(通过IoC控制)。。我已经隔离了我需要改变的地方。

一开始,Orm实体和POCO的分离可能显得有些过头了。

但随着时间的推移(代码维护),orm实体和poco可能会出现分歧。。。这样可以保持事物的清洁和分离。

旁注,我写的代码是为了长期维护。我对";快速完成";编码实践。

当我的公司被收购,我们不得不从RdbmsOne切换到ANOTHER_RDBMS时,我不得不在经历了一段可怕的时期后想出一个干净的架构,这简直是一场噩梦。人们违反了左右分层的规则。现在,由于我已经完成了这项工作,我确切地知道我需要创建什么来支持不同的后端数据存储(rdbms、nosql、redi-cache),这并不重要,因为我的所有业务逻辑契约都基于域数据层。基于pocos而非orm实体的接口。

在我以前的公司里,我们一直在为同样的问题而挣扎,但问题很简单。(但不是解决方案)

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

轰。您只是将基础结构与域逻辑相结合,但如果您想使用Attributes,则没有其他方法。

一个简单的方法是使用一些不可避免的重复属性代码。

域层

public interface IUser
{
int Id
}

底层

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

如果您需要对原始数据进行某种转换,这种方式也会有所帮助。例如:

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

如果基础设施允许,另一种方法(如实体框架)是使用单独的FLUENT配置代码。

域层

public class User : IEntity
{
int Id
}

底层

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

通过这种方式,您可以将域属性与DataAccess指令分开。

虽然像Dapper这样的一些库不提供如此流畅的选项,所以你也可以尝试使用某种自定义属性&反射魔法,但这是一条黑暗的道路。。。。。。。

最新更新