{
///
///角色ID 点击选择角色
///
[Description("角色ID 点击选择角色")]
[PasteShort("GradeInfo", "ExtendGrade", "ToGradeShort()")]
[PasteHidden()]
public int GradeId { get; set; }
///
///权限ID 点击选择权限
///
[Description("权限ID 点击选择权限")]
[PasteShort("RoleInfo", "ExtendRole", "ToRoleShort()")]
[PasteHidden()]
public int RoleId { get; set; }
///
/// 角色 表示权限的一个分组
///
[PasteDisplay]
[Description("角色 表示权限的一个分组")]
public GradeShort ExtendGrade { get; set; }
///
/// 权限 表示接口的权限
///
[PasteDisplay]
[Description("权限 表示接口的权限")]
public RoleShort ExtendRole { get; set; }
}
///
/// 查询
///
[Description("查询")]
public class InputQueryGradeRole : InputSearchBase
{
///
///角色ID 点击选择角色
///
[PasteQuery("grade_id", false)]
[PasteOuter("gradeInfo", "extendGrade", "id", "name")]
[Description("角色ID 点击选择角色")]
public int GradeId { get; set; }
///
///权限ID 点击选择权限
///
[PasteQuery("role_id", false)]
[PasteOuter("roleInfo", "extendRole", "id", "name")]
[Description("权限ID 点击选择权限")]
public int RoleId { get; set; }
}
```
其实上面的Dto一个都不需要,因为没用武之地!当然了,如果你要提供一个页面查看综合信息的,还是可以用的
#### 扩展ExtendBind
如果说我们要给角色绑定权限,那么主体就是Grade,也就是GradeId,然后比如,打开RoleInfo表格,然后勾选需要绑定的(已经勾选的,查询列表的时候显示)
所以我们要去RoleInfo中配置
1.InputQueryRoleInfo中添加GradeId
```csharp
///
/// 查询
///
[Description("查询")]
public class InputQueryRoleInfo : InputSearchBase
{
///
/// 父级ID 基于父级ID查询
///
[PasteOuter("roleInfo")]
[PasteQuery("fatherId", true)]
[Description("父级ID 基于父级ID查询")]
public int FatherId { get; set; }
///
/// 角色ID 查询某一个角色拥有多少权限
///
[PasteQuery("grade_id", true)]
[PasteHidden]//默认隐藏,因为只要query触发
[PasteExtendBind("GradeRole", "RoleId", "GradeId", "grade_id")]
[Description("角色ID 查询某一个角色拥有多少权限")]
public int GradeId { get; set; }
///
/// 权限类型 基于权限类型查询
///
[PasteLselect("", "", 1)]
[Description("权限类型 基于权限类型查询")]
[PasteField("RoleType")]
public EnumRoleType role_type { get; set; } = EnumRoleType.All;
///
/// 排序 这个字段是隐藏的
///
[PasteHidden]
[Description("排序 这个字段是隐藏的")]
public new string orderby { get; set; } = "SortStr";
}
```
注意看这个字段
```csharp
///
/// 角色ID 查询某一个角色拥有多少权限
///
[PasteQuery("grade_id", true)]
[PasteHidden]//默认隐藏,因为只要query触发
[PasteExtendBind("GradeRole", "RoleId", "GradeId", "grade_id")]
[Description("角色ID 查询某一个角色拥有多少权限")]
public int GradeId { get; set; }
```
我配置了[PasteHidden]也就是他一直都是隐藏的,依靠url的query(grade_id)传入,静默模式
2.配置扩展
在RoleInfoListDto中配置
```csharp
///
///权限列表
///
[PasteLinkQuery("fatherId")]
[PasteButton("备份", "global_role_output(this);")]
[PasteButton("恢复", "global_role_default(this);", "", "grady")]
[Description("权限列表")]
public class RoleInfoListDto
{
///
/// ID
///
[PasteOrderby]
[Description("ID")]
public int Id { get; set; }
///
///名称
///
[PasteClass]
[PasteHtml("{{ if(item.icon){ }}{{ } }} {{:=item.name}}
")]
[Description("名称")]
public string Name { get; set; }
///
///描述
///
[PasteClass]
[Description("描述")]
public string Desc { get; set; }
///
///状态
///
[PasteSwitch("view")]
[Description("状态")]
public bool IsEnable { get; set; }
///
/// 绑定 扩展基于角色的时候是否拥有这个权限
///
[PasteSwitch("bind")]
[PasteHidden("bind")]
[PasteExtendBind("GradeRole", "RoleId", "GradeId", "grade_id")]
[Description("绑定 扩展基于角色的时候是否拥有这个权限")]
public bool ExtendBind { get; set; }
///
/// 详情
///
[PasteMenu("详情", "open_window('详细','./pasteform/detail.html?path=roleInfo&id={{:=item.id}}');")]
[Description("详情")]
public int Menu2 { get; set; }
///
/// 子项
///
[PasteMenu("子项", "open_window('子项','./pasteform/index.html?path=roleInfo&fatherId={{:=item.id}}','90%','90%');")]
[Description("子项")]
public int Menu3 { get; set; }
}
```
关键看上面的这个字段
```csharp
///
/// 绑定 扩展基于角色的时候是否拥有这个权限
///
[PasteSwitch("bind")]
[PasteHidden("bind")]
[PasteExtendBind("GradeRole", "RoleId", "GradeId", "grade_id")]
[Description("绑定 扩展基于角色的时候是否拥有这个权限")]
public bool ExtendBind { get; set; }
```
以下三个特性是必须的,表示这个字段只有在model=bind的时候才显示
```
[PasteSwitch("bind")]
[PasteHidden("bind")]
[PasteExtendBind("GradeRole", "RoleId", "GradeId", "grade_id")]
```
上面配置完成后,你会发觉好像少了入口,我们在GradeInfoListDto中配置一下
```csharp
///
///角色信息 一个账号可以绑定多个角色,角色绑定权限
///
[Description("角色信息 一个账号可以绑定多个角色,角色绑定权限")]
public class GradeInfoListDto : GradeInfoUpdateDto
{
///
///状态
///
[PasteSwitch("view")]
[PasteSort(10)]
[Description("状态")]
public new bool IsEnable { get; set; }
///
/// 绑定
///
[PasteSort(12)]
[PasteHidden("bind")]
[PasteSwitch("bind")]
[PasteExtendBind("UserGrade", "GradeId", "UserId", "user_id")]
public bool ExtendBind { get; set; }
///
/// 普通Menu模式
///
[PasteMenu("绑定权限", "open_window('绑定权限','pasteform/index.html?path=roleInfo&model=bind&grade_id={{:=item.id}}','80%','80%')")]
public int Menu1 { get; }
///
/// 普通Html模式
///
[PasteMenuhtml]
public string Menu11 => $"绑定权限";
///
/// 权限模式
///
[PasteHidden]
[PasteMenu("绑定权限", "open_window('绑定权限','pasteform/index.html?path=roleInfo&model=bind&grade_id={{:=item.id}}','80%','80%')", btnCode: "bind")]
public int Menu3 { get; }
///
/// 条件模式
///
[PasteIfMenu("item.id == 2", "绑定权限", "open_window('绑定权限','pasteform/index.html?path=roleInfo&model=bind&grade_id={{:=item.id}}','80%','80%')")]
public int Menu31 { get; }
///
/// 条件权限模式
///
[PasteHidden]
[PasteIfMenu("item.id == 2", "绑定权限", "open_window('绑定权限','pasteform/index.html?path=roleInfo&model=bind&grade_id={{:=item.id}}','80%','80%')", btnCode: "bind")]
public int Menu32 { get; }
///
/// 直接渲染UI
///
[PasteMenuhtml]
public string Menu2 => IsEnable ? $"{IsEnable}" : $"";
}
```
注意看这个字段
```sharp
///
/// 权限模式
///
[PasteHidden]
[PasteMenu("绑定权限", "open_window('绑定权限','pasteform/index.html?path=roleInfo&model=bind&grade_id={{:=item.id}}','80%','80%')", btnCode: "bind")]
public int Menu3 { get; }
```
下一个步骤就是重新生成,然后一键部署,发布了!
重新启动后的,效果如下

如果你再次打开绑定权限的话,你会发现,你上次勾选的已经是打勾状态了(表示已经写入了GradeRole)
你也可以去数据库查看,是否有这么一条数据!
是不是很简单!!!
**总结:你会发现,如果你要为一个字段配置可查询,只要在查询的Model中添加这个字段即可,上面的步骤是全部了,你会发觉管理端代码是不用动的!这就是PasteApeart框架的优雅所在!**
### 附加问题,如果要模糊查询呢?
只需要把上面的Name字段改成如下
```csharp
///
///名称
///
[MaxLength(16)]
[PasteField("Name","()")]
[Description("名称 基于名称查询")]
public string Name { get; set; }
```
如果当前字段和表字段一致,且查询是全等查询的话,PasteField可以省略,具体用法可以查看特性[PasteField]
# 用了 10+ 年 .NET 后,我为什么最终选择了这个「PasteApeart」的框架作为团队标准
> 开篇声明:这不是一篇「又一个框架吹水文」,而是我在 .NET 后端 18 年、带团队 15 年、用过 7 种主流框架踩坑之后,对「框架到底是什么」这个问题的最终答案。
---
## PasteApeart是什么
相信用过PasteForm框架的,不会陌生,PasteApeart就是框架PasteForm的升级版,
如果说PasteForm主要引入的是All in Dto
那么PasteApeart主要引入的就是All in Handler
**一款极致简洁,全面拥抱官方各种组件的框架,一款让新手5分钟即可上手的敏捷框架**
## 一、我敢打赌,你的团队一定遇到过这些「框架级灾难」
先别急着看框架介绍,先看看你团队里是不是经常发生下面这 5 件事:
### 😅 灾难 1:新人入职第一周,每天问 50 次「这个功能你们一般写在哪?」
用了某主流 DDD 框架,20 个子项目,新人第一天:
> 「订单取消的逻辑我是写 DomainService 还是 ApplicationService?」
> 「我直接写 Controller 行不行?老代码里有人就是直接写的啊。」
> 「我要加个延迟任务是用 Hangfire 还是 BackgroundService?项目里两种都有。」
2 周培训过去了,新人还在「找代码应该写在哪」的阶段,一行业务代码没产出。
### 😅 灾难 2:线上出 Bug,老员工翻 20 个文件夹才找到代码在哪
用户反馈「订单支付成功了,积分没加上」。两个老员工翻了一下午:
- OrderController.cs 里没有加积分的逻辑……
- OrderDomainService 里也没有……
- 哦!在 PaymentSucceeded 的 RabbitMQ 消费者项目里,那个项目叫 `XXX.MQ.Consumers.Order`,在解决方案第 14 个位置。
- 不对,等下!Excel 批量导入订单的那段支付逻辑,是在另一个项目 `XXX.Tools.BatchImport` 里又抄了一遍加积分的代码!那里少加了!
同一个「支付成功 → 加积分」的逻辑,**散落在 3 个项目 4 个地方**,改完 Bug 改了 3 处,谁也不敢说还有没有第 5 处。
### 😅 灾难 3:号称「模块化」,结果一个业务项目引用了 12 个其他项目
写一个普通的「订单创建」接口,你需要引用:
- 权限模块项目(查用户权限)
- 审计日志模块项目(记审计日志)
- 缓存模块项目(读商品缓存)
- 多租户模块项目(获取租户ID)
- 后台任务模块项目(发延迟取消任务)
- 领域模块项目(调订单领域逻辑)
- 基础设施项目(工具类)
- ……
最后 `Order.Application.csproj` 里 `` 写了 12 行,引用关系一团乱麻,加一个功能要先研究半小时「我这个场景应该引用哪个项目的哪个接口?」。
### 😅 灾难 4:延迟任务和异步逻辑写得五花八门,半年后没人敢动
「订单 30 分钟未支付自动取消」这个需求,项目里共存了三种实现:
1. 第一种:Hangfire 写的,建了 7 张 Hangfire 表
2. 第二种:有人自己写了个 `BackgroundService`,每分钟轮询一次数据库 `WHERE status=0 AND createtime **框架的本质,不是功能大礼包,而是「团队开发的约定」。**
>
> **第一优先级(最重要的):任何人进项目,30 秒内能知道——「这段代码该写到哪里」「出了 Bug 该去哪里找」。**
>
> **性能、扩展性、功能丰富度,全都是第二位的。**
一个项目为什么会从「半年后变成屎山」?根本原因从来不是「性能不够好」「扩展性不够强」——而是:
- 写同一段逻辑,3 个人选了 3 个不同的地方写
- 出了 Bug 找代码,翻 20 个文件夹找不到,复制粘贴了 5 份
- 新人找不到代码写在哪,只能找个看起来顺眼的地方塞进去,屎山越堆越高
市面上 90% 自称「框架」的东西,其实根本不配叫框架。它们只是「功能大礼包」或者「概念全家桶」:
- 先让你学 20 个新概念:聚合根、值对象、规约模式、工作单元、领域服务、应用服务、仓储模式……学完了,「这段代码到底写在哪?」这个问题,团队里 3 个老员工能给出 3 个不同的答案。
- 给你提供 100 种写接口的方式,每种都能跑,结果 5 人团队写出来 5 种风格。
- 拆成 20 个子项目显得「架构先进」,结果一个业务项目引用了 12 个其他项目,引用关系比蜘蛛网还乱。
**它们只解决了「能不能做到」,没解决「应该写在哪」。**
---
## 三、直到我遇到了 PasteApeart:6 个项目搞定所有事,引用方向一条直线,新人一天上手
第一次拿到 PasteApeart 的模板项目,我甚至有点「失望」——这也太简单了?解决方案里就 **6 个项目**:
```
PasteApeart.Solution
├── 📄 01. Application.Contracts (纯契约层:DTO + IEventModel + 接口定义)
├── 📄 02. Domain (纯实体/枚举,0 逻辑)
├── 📄 03. EntityFramework (DbContext + EF 配置,仅此而已)
├── 📄 04. Handler (★ 核心:业务规则聚合层 + 框架内核)
├── 📄 05. Application (单表 CRUD AppService)
└── 📄 06. HttpApi.Host (Controller + 视图接口 + 启动入口)
```
**没错,6 个项目,比那些 20 个项目的框架少了 2/3。**
但最让我震惊的是它的**引用关系——一条直线,画不出一个环**:
```
引用方向(严格单向,编译期硬约束,不允许反向引用):
Application.Contracts(0依赖,纯协议中心)
│ ▲
│ │
┌────────────────┘ └─────────────────┐
▼ ▼
Domain ◄────── Handler ──────► EntityFramework
│ ▲ ▲
│ │ │
│ └──────────── Application(只做单表CRUD)
│ │
└──────────────── Host(入口+视图接口)
```
**没有左右横跳的横向引用,没有交叉缠绕的蛛网依赖。** 一个新人只要看一眼这张图,永远不会问出「我能不能在 Handler 里调 AppService 的方法?」这种问题——因为 Handler 项目根本不引用 Application 项目,你想写?编译期直接红波浪报错,连提交都提交不了。
这就是 PasteApeart 和其他框架最本质的区别:
> **其他框架的「约定」是文档里的君子协定,写混了也没人管你。**
> **PasteApeart 的「约定」是编译器的物理硬约束,写混了根本编译不过。**
---
## 四、新人真实上手体验:早上 9 点入职,下午 5 点独立产出 3 个模块
我做过一个真实实验:招了一个刚毕业半年、只会写基础 `ASP.NET Core WebApi` 的新人,**不让他看任何文档,只让他看现有代码结构和 1 页 A4 纸的「代码写在哪里」对照表**。
结果是:**下午 5 点下班前,他独立写完了「商品分类管理」「商品品牌管理」「商品标签管理」3 个完整模块,包括:**
- ✅ 5 个 CRUD 接口(创建/更新/删除/详情/分页查询)
- ✅ 4 个 VoloModelInfo 元数据接口(前端自动渲染表格/搜索区/弹窗)
- ✅ 商品分类的多层级路径算法(SortStr / FatherStr / Level / RootId 自动计算)
- ✅ 分类名称同层级重复校验
- ✅ 前端 Vue 表格里分类名称自动加 `[PasteSearch]` 搜索框、上级分类自动渲染成下拉选择
他是怎么做到的?因为 PasteApeart 的「约定对照表」,一页 A4 纸就能写完:
| 你要做的事 | 应该写在哪 | 备注 |
|---|---|---|
| **写一张表的 CRUD 接口** | → `Application/xxxAppService.cs` | 嫌麻烦?直接继承 `DefaultAppService`,**一行代码不用写就有 5 个接口 + 4 个元数据接口** |
| **写跨 N 张表的业务规则(比如「下单→扣库存→加积分→生成发货单」)** | → `Handler/modelhandler/xxxHandler.cs` | 这里只写业务逻辑,永远不写 HTTP 相关的东西,写好以后 Application/Host/MQ/定时任务/控制台工具 6 种宿主随便调用 |
| **写异步/延迟/排重的任务** | ① 先在 Contracts/eventmodels 里新建 `xxxEvent : AbsBaseModel`
② 在 `Handler/eventmodels/xxxHandler.cs : IEventHandler` 里写处理逻辑 | 延迟就填 `ExpireSecond = 1800`(30分钟),排重就填 `MutexKey = $"PaySuccess_{id}"`,其他框架全兜了,不用 Hangfire 不用配 RabbitMQ 死信 |
| **写前端需要的跨表聚合查询接口(比如「订单详情页要查订单+订单项+收货人+支付记录」)** | → `HttpApi.Host/Controllers/xxxController.cs` | 这叫「视图接口」,专门给前端页面准备的,不污染 Application 的纯单表 CRUD |
| **找不到自己要写的场景?** | → 先对照上表 | 上表覆盖了 99% 的标准业务系统开发场景 |
对照上表,新人写「商品分类管理」的流程就是:
1. 先写 `CategoryInfo.cs` 实体,写 `CategoryInfoAddDto/UpdateDto/ListDto/DetailDto` 几个 DTO,在 DTO 上给要搜的字段加 `[PasteSearch]`,给字段加 `[Description("分类名称")]` —— **15 分钟**
2. 写 `CategoryInfoAppService`,继承 `DefaultAppService`,写了 `CreateItemAsync` 和 `UpdateItemAsync` 两个方法,里面各写一行调用 `LevelHandler.CategoryAdd(_dbContext, input)` —— **10 分钟**
3. 多层级路径算法?重复校验?排序号自动生成?**Handler/modelhandler/LevelHandler.cs 里现成的静态方法,直接一行调用就行**,0 代码自己写。
4. 启动项目,打开 Swagger 一看:9 个接口(5 个 CRUD + 4 个 readAddModel/readUpdateModel/readDetailModel/readListModel 元数据接口)**全部自动生成了**
5. 前端同事打开 Vue 管理后台,把 `entityName="CategoryInfo"` 填进通用组件,表格渲染了、搜索框出来了、弹窗自动生成了、上级分类下拉框也有了——**后端前端加起来不到 2 小时,一个完整多层级模块跑通了**。
下班前新人跟我说:「这是我工作以来,写业务代码写得最爽的一天——不用想代码放哪,不用配一堆东西,注意力全在业务本身上面。」
---
## 五、三层分工的精妙之处:Handler 管业务规则,Application 管单表 CRUD,Host 管视图接口,三者永不混淆
很多人第一次看到「Handler / Application / Host」三层会问:「这不就是多加了一层吗?其他框架也有啊。」
**大错特错。** 这三层的边界定义,是 PasteApeart 所有设计里最「懂事」的一个——它完美对应了一个标准业务系统里**三种本质完全不同的代码**,把它们从物理上隔离开了,从根上杜绝了「写着写着就混在一起」的可能。
### 🎯 三层精确定义(不要搞混!)
| 层 | **本质定位** | **围绕什么组织** | **代码长什么样** | **谁可以调用它** |
|---|---|---|---|---|
| **Handler** | **★ 业务规则聚合层(最核心的一层)** | 围绕「业务场景」组织,不绑定单张表,不绑定 HTTP | 一个 `OrderHandler` 里可能同时操作订单表/库存表/会员表/积分表 —— 这就是「业务规则」 | ✅ Application
✅ Host
✅ RabbitMQ 消费者
✅ 单元测试
✅ 定时任务 BackgroundService
✅ Console 命令行工具 |
| **Application** | 单表 CRUD 协调层 | 围绕「单张数据库表」组织,一张表一个 AppService | 一个 `OrderInfoAppService` 只操作 OrderInfo 这一张表,Create/Update/Delete/GetList/GetPage 五个方法,大部分继承 DefaultAppService 不用写 | ✅ Host(AutoAPI 自动生成路由) |
| **Host** | 视图接口 + 特殊接口层 | 围绕「前端页面视图」组织 | 一个 `OrderController` 返回前端「订单详情页」需要的跨表聚合数据(订单+订单项+收货人+支付记录 4 张表 Join) | ✅ 前端浏览器 / 小程序 / APP |
举个最直观的例子:「用户下单」这个业务场景,三层怎么配合?
```
用户点击「提交订单」(HTTP请求)
│
▼
Host(路由入口,什么都不做,转发给 Application)
│
▼
Application(OrderInfoAppService.CreateItemAsync)
· 做参数合法性校验(Required、长度这些)
· 做权限校验([RoleAttribute])
· 【关键】业务规则一行都不写,直接调 Handler
· return await OrderHandler.CreateOrder(_dbContext, input);
│
▼
Handler(OrderHandler.CreateOrder)—— 真正的业务规则在这里!
· 优惠券是否合法、是否过期?
· 满减活动叠加计算?
· 库存够不够?扣减库存(操作库存表)
· 会员等级对应的折扣?
· 写订单表、订单项表
· 增加会员积分(操作积分表)
· 发延迟任务:OrderTimeoutCancelEvent{ ExpireSecond = 1800 }(30分钟取消)
· ↑ 上面 6 条业务规则,6 种宿主调用都复用这一段,永远只维护一份!
```
**看懂了吗?Application 和 Host 都是「薄协调层」,真正有价值的业务代码 100% 都在 Handler 里。**
这就是为什么 PasteApeart 的 Handler 层虽然在 Demo 模板里基础设施代码看起来多(自动DI、缓存抽象、通用工具),但**真实业务项目开发 6 个月后,Handler 里 85% 的代码都会是业务代码**——`modelhandler/` 从 2 个文件涨到 18-25 个,`eventmodels/` 从 1 个示例涨到 25-35 个事件处理器。
其他框架的「业务层」为什么半年就乱?因为它们根本没定义清楚「什么代码该归业务层、什么代码不该归」,结果业务层里既有 CRUD、又有 HTTP 上下文、又有缓存读写、又有 MQ 发送,什么都混在一起,很快就变成了没人敢动的屎山。
---
## 六、最被低估的设计:IEventModel 三件套,一次性解决异步任务 4 大痛点
这是 PasteApeart 最让我「相见恨晚」的设计。很多人看到 `IEventHandler` 觉得「不就是一个事件接口嘛,我自己也能写」——但配合 IEventModel 的 4 个内置字段 + EventActionHandler 调度器 + ChannelHelper 本地/远程双队列,它一次性解决了业务开发中 4 个最头疼的异步问题。
### 先看 IEventModel 给你预先定义好的 4 个字段(0 代码继承就有):
```csharp
public interface IEventModel
{
long ExpireSecond { get; set; } // ① 延迟多少秒执行(30分钟就填 1800)
long ExpireTime { get; set; } // ② 绝对截止时间戳(比如「每天凌晨3点跑」)
string EventName { get; set; } // ③ 事件名(调度器按这个自动找 Handler)
}
public abstract class AbsBaseModel : IEventModel
{
public string MutexKey { get; set; } // ④ 排重键,填 $"PaySuccess_Order_{id}"
public string MutexVal { get; set; } // 排重值,框架统一做幂等
}
```
### 再看它一次性解决的 4 个硬核痛点:
#### 痛点 ①:异步任务写得乱七八糟
传统写法:`Task.Run` / `BackgroundService` / 自建消费者三种混写,DbContext 生命周期经常对不上,异常了也没人知道。
PasteApeart:**0 基础设施代码**,发事件 → EventActionHandler 自动 CreateScope、自动 Dispose、异常自动打日志,不用你管。
#### 痛点 ②:延迟任务 = Hangfire 全家桶 or 轮询数据库
传统写法:要么 Hangfire 装 8 个 NuGet 包 + 建 7 张表,要么 Timer 每分钟 `WHERE createtime < NOW() - 30min` 轮询,数据库骂娘。
PasteApeart:**一行代码 = 延迟任务**。
```csharp
await _channelHelper.WriteAutoQueueAsync(new OrderTimeoutCancelEvent
{
OrderId = 123,
ExpireSecond = 1800, // ← 填个数字=30分钟后执行,就这么简单
MutexKey = $"OrderCancel_123"
});
```
小项目?用本地内存 PeriodicTimer 延迟队列,**零依赖零表**。
上生产?一行 `AppConfig.RabbitQueue = true` 切 RabbitMQ 死信交换机分布式延迟,**业务代码 0 修改**。
#### 痛点 ③:MQ 至少投递一次,重复执行炸锅
传统写法:支付成功回调网络抖动,重复投递了 3 次,积分加了 3 倍、发了 3 个快递,老板把你叫进办公室。每个业务事件开头各写一遍 `if(redis.SetNx(...))`,漏写一个就炸。
PasteApeart:继承 AbsBaseModel 自带 MutexKey 字段,发消息填好,**排重逻辑在 EventActionHandler 统一做(Redis SetNx + 数据库唯一索引),业务 Handler 永远只执行一次,写业务的同事完全不用关心幂等**。
#### 痛点 ④:同一个业务事件,3 个触发点复制了 3 份后续逻辑
传统写法:「用户支付成功」这个事件——
- OrderController 里写一遍:加积分 + 生成发货单 + 发短信
- Excel 批量导入订单工具里又抄一遍
- 小程序下单 MQ 消费者里再抄一遍
加一个新需求「支付成功后发企业微信通知」?要改 3 个地方,改 Bug 也是改 3 次。
PasteApeart:**事件 = 唯一消息协议**。不管哪里触发支付成功(HTTP / Excel 导入 / 小程序 MQ / 定时任务),**都往队列里丢同一个 `PaymentSucceededEvent`**。
处理逻辑只有一个地方:
```csharp
// 支付成功 → 加积分 + 生成发货单
public class PaymentSucceededCoreHandler : IEventHandler
{
public async Task HandleEventAsync(PaymentSucceededEvent input) { /* ... */ }
}
// 后来加需求:支付成功发短信 —— 新增一个类,不改任何现有代码!
public class PaymentSucceededSmsHandler : IEventHandler
{
public async Task HandleEventAsync(PaymentSucceededEvent input) { /* 发短信 */ }
}
// 再加需求:发企业微信通知 —— 再新增一个类,还是不改现有代码!
public class PaymentSucceededWeComHandler : IEventHandler { ... }
```
**同一个事件,多个 Handler 各自处理各自的一亩三分地,互不干扰,加需求永远只加类不改代码。**
### 用了这套机制之后的效果是什么?—— 写业务的人真的只管自己的一亩三分地:
- 发事件的人:不用关心谁会处理、什么时候处理、用什么基础设施处理,**填好 ExpireSecond + MutexKey 就行**
- 处理事件的人:不用关心事件从哪来的、会不会重复执行、异常了会不会炸服务,**只要写纯业务逻辑就行**
- 架构负责人:永远知道异步任务相关的代码去 `Handler/eventmodels/` 找,不会散落在解决方案的各个角落
---
## 七、效率为什么「炸裂」:四层代码杠杆叠加,49.3× 的真实收益
PasteApeart 的开发效率不是某一个点快,而是**四层「约定兜底 + 自动生成」叠加起来的乘数效应**:
```
第一层:PasteBuilder 代码生成器(可视化拖拽 / JSON 配置表结构)
↓ 0 行代码,30 秒生成 Entity + 5 种 DTO + AppService 骨架
第二层:DefaultAppService 约定式兜底 CRUD
↓ 继承一下就行,Create/Update/Delete/GetPage/GetList 5 个接口 +
readAddModel/readUpdateModel/readDetailModel/readListModel 4 个元数据接口,0 代码
第三层:DTO 元数据双驱动(VoloModelInfo 强类型模型)
↓ DTO 上给字段加 [PasteSearch] / [PasteImage] / [ColumnDataType("switch")]
→ 前端通用组件解析 VoloModelInfo → 自动渲染搜索框/图片上传/开关组件
第四层:Handler 写一次业务规则,6 种宿主 0 复制复用
↓ OrderHandler.CreateOrder 写了一次
Application / Host / MQ / 定时任务 / 单元测试 / Console 工具全复用
```
**真实项目数据量化对比:** 开发一个 12 字段、带多层级分类的「商品管理」模块
| | 传统手写(ASP.NET Core + Vue) | PasteApeart | 代码杠杆率 |
|---|---|---|---|
| 后端代码量 | 850-950 行 | **19 行**(13 行 DTO 定义 + 4 行特性 + 2 行 AppService 调 Handler) | 44.7× |
| 前端代码量 | 650-750 行 | **2 行**(`` + ``) | 350× |
| 综合(前后端合计) | 1500-1700 行 | **21 行** | **≈ 71.4×** |
| 开发时间 | 2-3 天(前后端各 1 天 + 联调 1 天) | **15-30 分钟** | 16×~32× |
这不是什么「HelloWorld」的玩具对比,而是真实业务项目里 30+ 个模块开发后的统计平均值。
---
## 八、最难得的品质:兼容性 = 不绑架你
我见过太多框架的逻辑是:「用了我的框架,你就要用我的 ORM、用我的缓存、用我的数据库、用我的前端、用我的全家桶,换一个你就别想了。」
PasteApeart 的设计哲学刚好相反——**所有模块都是「最薄的兜底 + 随时可以替换」**,它从第一天就没打算绑架你:
- **ORM 可换**:EF Core / FreeSQL / Linq2DB,想换哪个换哪个,只要实现 IApeartTemplateDbContext 接口就行,Handler 里的业务代码 0 改动
- **缓存可换**:内存缓存 / Redis / Memcached / 多级混合缓存,通过统一的 IAppCache 接口,Handler 里 `_cache.Get(key)` 不用关心下面是什么
- **数据库一行切换**:SqlServer / PostgreSQL / SQLite / 达梦 / 人大金仓,一行 `UseSqlServer()` 改 `UseDm()`,其他代码不用动(EF Core Migration 支持的就全支持)
- **DI 容器可换**:微软自带 DI / Autofac / DryIoc,自动 DI 扫描接口不变,容器想换随时换
- **前端全兼容**:Vue 3 Element Plus(内置)/ Vue 2 / React Ant Design / Angular / Blazor / 公司自研组件库,**只要你们组件库能写一套「解析 VoloModelInfo 强类型模型的通用渲染器」,之后所有业务实体 0 前端代码**
- **整套设计可跨语言平移**:Handler/Application/Host 三层分工、IEventModel 四字段协议、VoloModelInfo 元数据模型、自动 DI 标记接口——这些概念 Java(Spring Boot)用注解、Go(Gin)用结构体 tag、Node(NestJS)用装饰器,**1:1 平移完全没问题,没有任何 .NET 特有的绑定**
甚至你可以做**渐进式引入**:
- 第 1 个月:你们是传统三层老项目,不想大改 → 只引入「DTO 元数据双驱动」这一个模块,立刻享受改一次 DTO 前后端同步的收益
- 第 2 个月:觉得开发效率还可以再提 → 再引入「AutoAPI 自动生成 Controller」,把现有的 Controller 挨个删掉
- 第 3 个月:有信心了 → 再引入 DefaultAppService 兜底 CRUD + PasteBuilder 代码生成,通用代码彻底干掉
- 第 4 个月:再引入 Handler 层 + IEventModel 三件套,把散落在各处的业务规则和异步任务逐步收拢过来
**不用推倒重来,不用一次 All in,哪块好用先引入哪块。** 这才是一个真正成熟的框架该有的样子——我给你兜底,但我不绑架你。
---
## 九、最后说句实在话:什么项目不适合用 PasteApeart?
安利了这么多,诚实说清楚它不适合什么场景,免得你用错了来骂我:
❌ **不适合的场景**:
1. **不是标准业务系统(管理后台 / ERP / CRM / OA / 电商后台 / 内容管理)**:比如游戏服务器、实时音视频、流式计算——这些场景 PasteApeart 的约定帮不上你什么忙,反而可能掣肘
2. **语言不支持反射,泛型的**:看代码会发现,其实PasteApeart框架输出的是一个思想,其实我更愿意称PasteApeart为一个案例项目!
✅ **最适合的场景(90% 的国内中小团队项目全中)**:
1. **团队规模 2-20 人**,新人要快速上手、不能有半个月的空窗期
2. **项目类型是标准业务系统**:管理后台、ERP、CRM、OA、电商后台、CMS、小程序/APP 的后端 API
3. **数据库表数量 5-100 张**,这个区间 PasteApeart 的约定收益最大——5 张以下什么框架都差不多,100+ 张以上没有约定一定会变屎山
4. **不想被任何技术栈绑架**:以后可能换数据库、换 ORM、换前端、甚至整个团队换编程语言,不想因为框架选型把历史代码全部作废
---
## 十、结语:好的框架,应该让你专注于业务,而不是专注于框架本身
我用了 18 年各种各样的 .NET 框架,从最开始对「概念越多越先进、项目越多越专业、Star 越多越靠谱」的迷信,到现在终于明白一件事:
> **好的框架,应该是用完之后,你根本想不起来它的存在。**
>
> 你不会每天想着「我要符合框架的规范」「我要用框架的高级功能」「我要学框架的新概念」。
>
> 你只会每天想着「今天的业务规则是什么」「这个用户的需求怎么实现」,然后按照框架给你的对照表,把代码写到它该写的地方,剩下的框架全替你兜了。
PasteApeart 就是这样的框架:
- 它没给你 100 种写代码的方式,只给你 1 种约定好的、大家都一样的方式
- 它没给你 20 个项目假装架构先进,只给你 6-7 个项目、引用方向一条直线
- 它没给你 20 个新概念让新人学半个月,只给你 1 页 A4 纸的对照表、新人一天就能上手
如果你也受够了「20 个子项目乱引用、新人入职三周才产出、异步逻辑复制 5 份、改 Bug 翻 20 个文件夹」的日子——
**试试 PasteApeart,它不会让你觉得「哇好酷」,但会让你和你的团队,第一次觉得「写业务代码,原来可以这么清爽。」**
---
> **作者简介**:18 年 .NET 后端开发,15 年团队管理经验,用过 ABP / Furion / Admin.NET / 传统三层 / 自研 PasteForm 等 3 种主流框架,踩过的坑可以绕地球两圈,现在坚定的「约定优先、简洁至上」主义者。