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

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

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

洋葱架构的示意图如下:

e9e1c91ca561c82c6651d0d65eada54f.png

一、OnionArch 解决方案说明

解决方案截图如下:
c27f246c19a28b7138dbbcea6d292c61.png

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

源代码分为四个项目:

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,包括分页查询,方便和其它实体连接后再筛选查询字段。

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。

开发人员只需要在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 管道实现业务实体自动验证,并自动抛出自定义异常。

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

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

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

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

12. 全局异常捕获记录

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

三、相关技术如下

* .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

四、 找工作

博主有10年以上的软件技术实施经验(Tech Leader),专注于软件架构设计、软件开发和构建,专注于微服务和云原生(K8s)架构, .Net Core\Java开发和Devops。

博主有10年以上的软件交付管理经验(Project Manager,Product Ower),专注于敏捷(Scrum)项目管理、软件产品业务分析和原型设计。

博主能熟练配置和使用 Microsoft Azure 和Microsoft 365 云平台,获得相关微软认证和证书。

我家在广州,也可以去深圳工作。做架构和项目管理都可以,希望能从事稳定行业的业务数字化转型。有工作机会推荐的朋友可以加我微信 15920128707,微信名字叫Jerry.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/282113.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全新升级的AOP框架Dora.Interception[2]: 基于约定的拦截器定义方式

Dora.Interception&#xff08;github地址&#xff0c;觉得不错不妨给一颗星&#xff09;有别于其他AOP框架的最大的一个特点就是采用针对“约定”的拦截器定义方式。如果我们为拦截器定义了一个接口或者基类&#xff0c;那么拦截方法将失去任意注册依赖服务的灵活性。除此之外…

加快Android Studio的编译速度

从Eclipse切换到Android Studio后&#xff0c;感觉Android Studio的build速度比Eclipse慢很多&#xff0c;以下几个方法可以提高Android Studio的编译速度使用Gradle 2.4Gradle 2.4对执行性能有很大的优化&#xff0c;但Android Studio现在默认使用的是Gradle 2.2,所以我们需要…

PaddleOCR在 Linux下的webAPI部署方案

很多小伙伴在使用OCR时都希望能采用API的方式调用&#xff0c;这样就可以跨端跨平台了。本文将介绍一种基于python的PaddleOCR识别WebAPI部署方案。喜欢的可以关注公众号&#xff0c;获取更多内容。一、 Linux环境下部署1.环境要求操作系统&#xff1a;CenterOS7&#xff1b;主…

影响程序员生涯的三个错误观念,你千万不要犯!

程序员在社会上&#xff0c;到底是怎样一个生活群体&#xff1f;是否能找到自己方向&#xff1f;其实&#xff0c;路一直都在那里&#xff0c;只是你看不到而已&#xff01; 当初的你&#xff0c;可能一直被一些技术牵着鼻子走&#xff0c;并不是自己在做着自己想做的&#xff…

心电图计算心率公式_心电图到底能反应啥问题,看过之后你也能当“医生”

只要是经历过健康体检的健康人&#xff0c;或者做过手术的患者&#xff0c;基本都做过心电图检查。都说久病成医&#xff0c;所以有些人对血、尿常规等各项检查的结果都门清儿得很&#xff0c;最起码看一眼也能说出个大概齐。偏偏心电图这种常做的检查&#xff0c;不但老病号如…

获取正在运行的服务

手机上安装的App&#xff0c;在后台运行着很多不同功能的服务&#xff0c;最常见的例如消息推送相关的服务。如何查看这些服务&#xff1f;如何判断某个服务是否正在运行&#xff1f;如何停止某一个服务呢&#xff1f;请看下面的方法&#xff1a; package com.example.servicel…

开发composer包

一、初始化&#xff08;生成composer.json文件&#xff09; composer init#输入你要创建的composer包项目命名空间 Package name (<vendor>/<name>) [root/tiny-laravel]: #haveyb/tiny-laravel #输入composer包的描述 Description []:#this is a tiny laravel h…

Linux本地yum源配置以及使用yum源安装gcc编译环境

本文档是图文安装本地yum源的教程&#xff0c;以安装gcc编译环境为例。 适用范围&#xff1a;所有的cetos,红帽,fedroa版本 适用人群&#xff1a;有一点linux基础的小白 范例系统版本&#xff1a;CentOS Linux release 7.3.1611 (Core) 范例环境&#xff1a;vmware 虚拟机 安装…

word如何设置上标形式_如何在word中设置特殊页码

获取更多业界资讯和深度好文● 点击蓝字关注我们 ●在日常工作中&#xff0c;我们编辑的word文档经常需要设置页码&#xff0c;但有时文档的第一页是封面&#xff0c;第二页才是正文&#xff0c;或者第二页是目录&#xff0c;第三页才是正文&#xff0c;如下图所示&#xff0c;…

发布composer包到 Packagist,并设置自动同步(从github到Packagist)

一、发布composer包 1、将我们写好的项目包发布到github上 这一步不赘述&#xff0c;应该都会。 但是需要注意的是&#xff0c;我们一定要为我们的项目包打上tag之后再提交&#xff0c;否则 我们composer require时可能会报错 Could not find a version of package。 # 设置…

教你在CorelDRAW中导入位图

在CorelDRAW软件中不能直接打开位图图像&#xff0c;在实际操作中&#xff0c;用户需要使用导入位图图像的方法进行操作。导入位图图像时&#xff0c;可以导入整幅图像&#xff0c;也可以在导入的过程中对图像进行裁剪&#xff0c;或重新取样图像&#xff0c;导入整幅位图图像时…

.NET 6 中将 ASP.NET Core 注册成 Windows Service

前言使用 Visual Studio 中的 Worker Service项目模板:我们很容易创建出 Windows Service&#xff1a;IHost host Host.CreateDefaultBuilder(args).UseWindowsService().ConfigureServices(services >{services.AddHostedService<Worker>();}).Build();await host.R…

19.12 添加自定义监控项目 配置邮件告警 测试告警

9月12日任务19.12 添加自定义监控项目19.13/19.14 配置邮件告警19.15 测试告警19.16 不发邮件的问题处理19.12 添加自定义监控项目需求&#xff1a;监控某台web的80端口连接数&#xff0c;并出图两步&#xff1a;1&#xff09;zabbix监控中心创建监控项目&#xff1b;2&#xf…

wab框架

http协议 一、http简介 1.HTTP是一个基于TCP/IP通信协议来传递数据&#xff08;HTML 文件, 图片文件, 查询结果等&#xff09;。 2.HTTP是一个属于应用层的面向对象的协议&#xff0c;由于其简捷、快速的方式&#xff0c;适用于分布式超媒体信息系统。它于1990年提出&#xff0…

c++ 二维矩阵 转vector_Python线性代数学习笔记——矩阵的基本运算和基本性质,实现矩阵的基本运算...

当学习完矩阵的定义以后&#xff0c;我们来学习矩阵的基本运算&#xff0c;与基本性质矩阵的基本运算&#xff1a;矩阵的加法&#xff0c;每一个对应元素相加&#xff0c;对应结果的矩阵例子&#xff1a;矩阵A和矩阵B表示的是同学上学期和下学期的课程的成绩&#xff0c;两个矩…

android 4.4以上能够实现的沉浸式状态栏效果

仅仅有android4.4以及以上的版本号才支持状态栏沉浸效果 先把程序执行在4.4下面的手机上,看下效果: 在4.4以上的效果: 当然图片也是能够作为背景的.效果: 代码: if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {Window window getWindow();window.setFlags(Wind…

为abp vnext生成C#客户端给非abp第三方net程序使用

abp vnext提供了动态C#API客户端和静态C#API客户端来调用abp项目的接口&#xff0c;但是有局限性&#xff1b;要使用动态C#API客户端的项目必须也是ABP vnext的项目。静态C#API客户端也依赖abp的包&#xff0c;如下图为的静态客户端依赖于 Volo.Abp.DependencyInjection、Volo.…

C#项目代码规范

目的 1.方便代码的交流和维护。 2.不影响编码的效率&#xff0c;不与大众习惯冲突。 3.使代码更美观、阅读更方便。 4.使代码的逻辑更清晰、更易于理解。 在C#中通常使用的两种编码方式如下 Camel(驼峰式)&#xff1a; 大小写形式&#xff0d;除了第一个单词&#xff0c;所有单…

.NET MAUI实战 FolderPicker

1.概要最近在迁移 GeneralUpdate.Tool的时候需要用到文件夹选择&#xff0c;在MAUI中可以使用FolderPicker进行选择。注意&#xff0c;和上篇文章的文件选择不一样。因为在.NET MAUI中目前还没有傻瓜式直接可用的FolderPicker供开发者使用所以需要自己动手做一些修改。完整示例…

h5外卖源码php_校园食堂外卖APP走红 更多APP定制开发上一品威客网

近日&#xff0c;西安一高校推出了一款校园食堂外卖APP走红网络。该APP涵盖学校食堂的所有饭菜&#xff0c;并可给该校的师生提供校园食堂饭菜外卖服务。饭菜价格与食堂统一&#xff0c;且仅供该校内的师生使用。 目前开发校园外卖订餐系统可谓是一个较热门的创业项目&#xff…