首页 > 其他分享 >OnionArch-NorthwindTraders,sample-dotnet-core-cqrs-api

OnionArch-NorthwindTraders,sample-dotnet-core-cqrs-api

时间:2023-01-30 23:11:14浏览次数:57  
标签:core 实体 接口 领域 sample NorthwindTraders entry Entity public

NorthwindTraders sample-dotnet-core-cqrs-api 项目

OnionArch - 采用DDD+CQRS+.Net 7.0实现的洋葱架构

 

博主最近失业在家,找工作之余,看了一些关于洋葱(整洁)架构的资料和项目,有感而发,自己动手写了个洋葱架构解决方案,起名叫OnionArch。基于最新的.Net 7.0 RC1, 数据库采用PostgreSQL, 目前实现了包括多租户在内的12个特性。

该架构解决方案主要参考了NorthwindTraders sample-dotnet-core-cqrs-api 项目, B站上杨中科的课程代码以及博主的一些项目经验。

洋葱架构的示意图如下:

 

一、OnionArch 解决方案说明

解决方案截图如下:

 

可以看到,该解决方案轻量化实现了洋葱架构,每个层都只用一个项目表示。建议将该解决方案作为单个微服务使用,不建议在领域层包含太多的领域根。

源代码分为四个项目:

1. OnionArch.Domain

- 核心领域层,类库项目,其主要职责实现每个领域内的业务逻辑。设计每个领域的实体(Entity),值对象、领域事件和领域服务,在领域服务中封装业务逻辑,为应用层服务。
- 领域层也包含数据库仓储接口,缓存接口、工作单元接口、基础实体、基础领域跟实体、数据分页实体的定义,以及自定义异常等。

2. OnionArch.Infrastructure

- 基础架构层,类库项目,其主要职责是实现领域层定义的各种接口适配器(Adapter)。例如数据库仓储接口、工作单元接口和缓存接口,以及领域层需要的其它系统集成接口。
- 基础架构层也包含Entity Framework基础DbConext、ORM配置的定义和数据迁移记录。

3. OnionArch.Application

- 应用(业务用例)层,类库项目,其主要职责是通过调用领域层服务实现业务用例。一个业务用例通过调用一个或多个领域层服务实现。不建议在本层实现业务逻辑。
- 应用(业务用例)层也包含业务用例实体(Model)、Model和Entity的映射关系定义,业务实基础命令接口和查询接口的定义(CQRS),包含公共MediatR管道(AOP)处理和公共Handler的处理逻辑。

4. OnionArch.GrpcService

- 界面(API)层,GRPC接口项目,用于实现GRPC接口。通过MediatR特定业务用例实体(Model)消息来调用应用层的业务用例。
- 界面(API)层也包含对领域层接口的实现,例如通过HttpContext获取当前租户和账号登录信息。

二、OnionArch已实现特性说明

1.支持多租户(通过租户字段)

基于Entity Framework实体过滤器和实现对租户数据的查询过滤

复制代码
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//加载配置
modelBuilder.ApplyConfigurationsFromAssembly(typeof(TDbContext).Assembly);

//为每个继承BaseEntity实体增加租户过滤器
// Set BaseEntity rules to all loaded entity types
foreach (var entityType in GetBaseEntityTypes(modelBuilder))
{
var method = SetGlobalQueryMethod.MakeGenericMethod(entityType);
method.Invoke(this, new object[] { modelBuilder, entityType });
}
}
复制代码

在BaseDbContext文件的SaveChanges之前对实体租户字段赋值

复制代码
//为每个继承BaseEntity的实体的Id主键和TenantId赋值
var baseEntities = ChangeTracker.Entries<BaseEntity>();
foreach (var entry in baseEntities)
{
switch (entry.State)
{
case EntityState.Added:
if (entry.Entity.Id == Guid.Empty)
entry.Entity.Id = Guid.NewGuid();
if (entry.Entity.TenantId == Guid.Empty)
entry.Entity.TenantId = _currentTenantService.TenantId;
break;
}
}
复制代码

多租户支持全部在底层实现,包括租户字段的索引配置等。开发人员不用关心多租户部分的处理逻辑,只关注业务领域逻辑也业务用例逻辑即可。

2.通用仓储和缓存接口

实现了泛型通用仓储接口,批量更新和删除方法基于最新的Entity Framework 7.0 RC1,为提高查询效率,查询方法全部返回IQueryable,包括分页查询,方便和其它实体连接后再筛选查询字段。

 View Code

3.领域事件自动发布和保存

在BaseDbContext文件的SaveChanges之前从实体中获取领域事件并发布领域事件和保存领域事件通知,以备后查。

复制代码
//所有包含领域事件的领域跟实体
var haveEventEntities = domainRootEntities.Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()).ToList();
//所有的领域事件
var domainEvents = haveEventEntities
.SelectMany(x => x.Entity.DomainEvents)
.ToList();
//根据领域事件生成领域事件通知
var domainEventNotifications = new List<DomainEventNotification>();
foreach (var domainEvent in domainEvents)
{
domainEventNotifications.Add(new DomainEventNotification(nowTime, _currentUserService.UserId, domainEvent.EventType, JsonConvert.SerializeObject(domainEvent)));
}
//清除所有领域根实体的领域事件
haveEventEntities
.ForEach(entity => entity.Entity.ClearDomainEvents());
//生成领域事件任务并执行
var tasks = domainEvents
.Select(async (domainEvent) =>
{
await _mediator.Publish(domainEvent);
});
await Task.WhenAll(tasks);
//保存领域事件通知到数据表中
DomainEventNotifications.AddRange(domainEventNotifications);
复制代码

领域事件发布和通知保存在底层实现。开发人员不用关心领域事件发布和保存逻辑,只关注于领域事件的定义和处理即可。

4.领域根实体审计信息自动记录

在BaseDbContext文件的Savechanges之前对记录领域根实体的审计信息。

//为每个继承AggregateRootEntity领域跟的实体的AddedBy,Added,LastModifiedBy,LastModified赋值
//为删除的实体生成实体删除领域事件

复制代码
DateTime nowTime = DateTime.UtcNow;
var domainRootEntities = ChangeTracker.Entries<AggregateRootEntity>();
foreach (var entry in domainRootEntities)
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.AddedBy = _currentUserService.UserId;
entry.Entity.Added = nowTime;
break;
case EntityState.Modified:
entry.Entity.LastModifiedBy = _currentUserService.UserId;
entry.Entity.LastModified = nowTime;
break;
case EntityState.Deleted:
EntityDeletedDomainEvent entityDeletedDomainEvent = new EntityDeletedDomainEvent(
_currentUserService.UserId,
entry.Entity.GetType().Name,
entry.Entity.Id,
JsonConvert.SerializeObject(entry.Entity)
);
entry.Entity.AddDomainEvent(entityDeletedDomainEvent);
break;
}
}
复制代码

领域根实体审计信息记录在底层实现。开发人员不用关心审计字段的处理逻辑。

5. 回收站式软删除

采用回收站式软删除而不采用删除字段的软删除方式,是为了避免垃圾数据和多次删除造成的唯一索引问题。
自动生成和发布实体删除的领域事件,代码如上。
通过MediatR Handler,接收实体删除领域事件,将已删除的实体保存到回收站中。

复制代码
public class EntityDeletedDomainEventHandler : INotificationHandler<EntityDeletedDomainEvent>
{
private readonly RecycleDomainService _domainEventService;

public EntityDeletedDomainEventHandler(RecycleDomainService domainEventService)
{
_domainEventService = domainEventService;
}


public async Task Handle(EntityDeletedDomainEvent notification, CancellationToken cancellationToken)
{
var eventData = JsonSerializer.Serialize(notification);
RecycledEntity entity = new RecycledEntity(notification.OccurredOn, notification.OccurredBy, notification.EntityType, notification.EntityId, notification.EntityData);
await _domainEventService.AddRecycledEntity(entity);
}
}
复制代码

6.CQRS(命令查询分离)

通过MediatR IRequest 实现了ICommand接口和Iquery接口,业务用例请求命令或者查询继承该接口即可。

复制代码
public interface ICommand : IRequest
{
}

public interface ICommand<out TResult> : IRequest<TResult>
{
}
public interface IQuery<out TResult> : IRequest<TResult>
{

}
public class AddCategoryCommand : ICommand
{
public AddCategoryRequest Model { get; set; }
}
复制代码

代码中的AddCategoryCommand 增加类别命令继承ICommand。

7.自动工作单元Commit

通过MediatR 管道实现了业务Command用例完成后自动Commit,开发人员不需要手动提交。

复制代码
public class UnitOfWorkProcessor<TRequest, TResponse> : IRequestPostProcessor<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly IUnitOfWork _unitOfWork;

public UnitOfWorkProcessor(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Process(TRequest request, TResponse response, CancellationToken cancellationToken)
{
if (request is ICommand || request is ICommand<TResponse>)
{
await _unitOfWork.CommitAsync();
}
}
}
复制代码

8.GRPC Message做为业务用例实体

通过将GRPC proto文件放入Application项目,重用其生成的message作为业务用例实体(Model)。

public class AddCategoryCommand : ICommand
{
public AddCategoryRequest Model { get; set; }
}

其中AddCategoryRequest 为proto生成的message。

9.通用CURD业务用例

在应用层分别实现了CURD的Command(增改删)和Query(查询) Handler。

 View Code

开发人员只需要在GRPC层简单调用即可实现CURD业务。

复制代码
public async override Task<AddProductReply> AddProduct(AddProductRequest request, ServerCallContext context)
{
CUDCommand<AddProductRequest> addProductCommand = new CUDCommand<AddProductRequest>();
addProductCommand.Id = Guid.NewGuid();
addProductCommand.Model = request;
addProductCommand.Operation = "C";
await _mediator.Send(addProductCommand);
return new AddProductReply()
{
Message = "Add Product sucess"
};
}
复制代码

10. 业务实体验证

通过FluentValidation和MediatR 管道实现业务实体自动验证,并自动抛出自定义异常。

 View Code

开发人员只需要定义验证规则即可

复制代码
public class AddCategoryCommandValidator : AbstractValidator<AddCategoryCommand>
{
public AddCategoryCommandValidator()
{
RuleFor(x => x.Model.CategoryName).NotEmpty().WithMessage(p => "类别名称不能为空.");
}
}
复制代码

11.请求日志和性能日志记录

基于MediatR 管道实现请求日志和性能日志记录。

 View Code

12. 全局异常捕获记录

基于MediatR 异常接口实现异常捕获。

 View Code

三、相关技术如下

* .NET Core 7.0 RC1

* ASP.NET Core 7.0 RC1

* Entity Framework Core 7.0 RC1

* MediatR 10.0.1

* Npgsql.EntityFrameworkCore.PostgreSQL 7.0.0-rc.1

* Newtonsoft.Json 13.0.1

* Mapster 7.4.0-pre03

* FluentValidation.AspNetCore 11.2.2

* GRPC.Core 2.46.5

标签:core,实体,接口,领域,sample,NorthwindTraders,entry,Entity,public
From: https://www.cnblogs.com/Leo_wl/p/17077499.html

相关文章

  • .net core 下使用 Kafka 生产者批量发送给消息处理,使用事务(四)
    生产者批量发送消息,使用事务,要么全部失败要么全部成功重要说明事物id必须要设置producerConfig.TransactionalId=Guid.NewGuid().ToString();//必须设置事物id 1......
  • 基于.net core的Azure function 如何使用.net framework所支持的编码
    在azurefunction中通过http请求call第三方api时,response返回是一堆中文乱码,发现数据格式使用的是"gb2312"编码因此在StreamReader的时候,增加了“gb2312”的encoding,代码......
  • ASP.NET Core RESTful学习理解
    一、了解什么是RESTREST是“REpresentationalStateTransfer”的缩写,表述性状态传递;REST是一种软件架构风格,用于构造简单、可靠、高性能的WEB应用程序;REST中,资源(Resou......
  • EFCore build failure
    今日学习源代码,里面按照业务划分了6个微服务,挨个执行add-migrationinit时提示buildfailure,无其他任何提示。Ctrl+Shift+B生成解决方案后显示出是另外一个类库的问题,......
  • coredns使用etcd
    前言CoreDNS使用ETCD存储主机记录。etcd安装略过。Corefile内容.:53{#绑定本机IPbind192.168.1.2#etcd地址etcd{path/coredns......
  • CF1787H Codeforces Scoreboard 题解
    鬼知道怎么会撞题的,甚至是没听过的OJ。首先不考虑对\(a_i\)取\(\max\),显然直接按照\(k\)降序排序最优。接下来考虑\(a_i\)的限制,如果取到了\(a_i\)一定放在最......
  • coredns mysql 扩展使用+readyset 试用
    基于db进行dns记录的管理还是比较有用的,尤其在一些开发环境中,以下是一个使用同时也会尝试集成readyset(但是木有成功,应该是mysql编码兼容的问题)添加&构建插件方法比较简......
  • 在asp.net core web api中添加efcore使用codefirst
    首先创建webapi项目,我这里使用的版本是.net6  在nuget中添加对应的工具包 红框标出来的是对应的数据库扩展包,mysql用mysql版,sqlserver用sqlserver版,选择正确的版......
  • Entity Framework Core Error :证书链是由不受信任的颁发机构颁发的
    错误信息:Aconnectionwassuccessfullyestablishedwiththeserver,butthenanerroroccurredduringtheloginprocess.(provider:SSLProvider,error:0-证书......
  • 第六十章 使用 ^PERFSAMPLE 监控进程 - 预定义分析示例
    第六十章使用^PERFSAMPLE监控进程-预定义分析示例预定义分析示例以下是从过程状态维度开始的分析示例。在此示例中,^PERFSAMPLE在319994个样本中找到了76755个......