我目前正在开发一组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这样的一些库不提供如此流畅的选项,所以你也可以尝试使用某种自定义属性&反射魔法,但这是一条黑暗的道路。。。。。。。