# PasteApeart **Repository Path**: pastecode/paste-apeart ## Basic Information - **Project Name**: PasteApeart - **Description**: PasteForm框架的升级版,包含IEventHandler模块,附带了案例,项目模板,项目源码,可以从Nuget中加载 - **Primary Language**: C# - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-08-11 - **Last Updated**: 2026-08-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 如果我说有一个框架比AI开发效率还高,你信不信? >在AI盛行的当下,如果有人和你说,有一个WEB框架,开发效率可以超过AI,你肯定会觉得我再说大话!!! >PasteApeart是一个超敏捷的开发框架,可以说也是AI的专属开发框架! ## 先来几个热身的需求,一起来看看几个业务实际场景 >假设系统中是这么定于的角色信息(GradeInfo),有权限信息(RoleInfo),我们用这个来举个例子 ## 角色信息 ![角色信息](./imgs/001.png) ### 需求一:添加角色按照名称查询 从上面可以看到,一开始是只有关键字查询的,我们要添加对名称的查询,只要一下2个步骤 #### 在InputQueryGradeInfo中添加如下代码 ```csharp /// /// 查询 /// [Description("查询")] public class InputQueryGradeInfo : InputSearchBase { /// ///名称 /// [MaxLength(16)] [Description("名称 基于名称查询")] public string Name { get; set; } /// /// 角色 用于为用户选择角色? /// [PasteHidden] [PasteQuery("user_id", true)] [PasteExtendBind("UserGrade", "GradeId", "UserId", "user_id")] public int user_id { get; set; } } ``` #### 重新生成项目,然后一键部署 ![一键部署](./imgs/002.png) 打开10几秒钟后,或者看通知,重新刷新下角色信息页面 ![名称查询](./imgs/003.png) 可以看到已经有这个名称的UI出来了,我们试一下查询 ![查询结果](./imgs/004.png) ### 需求二:为角色分配权限(绑定权限) 如果说一个角色要表示多个权限,那么我们需要引入一个新的表GradeRole #### 创建Entity(GradeRole) ```csharp /// /// 角色绑定权限 /// [Comment("角色绑定权限")] public class GradeRole : Entity { /// /// 角色ID 点击选择角色 /// [PasteShort("GradeInfo", "ExtendGrade", "ToShortGrade()")] public int GradeId { get; set; } /// /// 权限ID 点击选择权限 /// [PasteShort("RoleInfo", "ExtendRole", "ToShortRole()")] public int RoleId { get; set; } } ``` #### 生成Dto 针对PasteApeart(PasteForm)我们提供了PasteBuilder包含VS版本和VSCODE版本,也就是一个插件,直接生成对应的代码 ![查询结果](./imgs/005.png) 这样你就获得了对应的Dto文件 ```csharp /// ///角色绑定权限 /// [Description("角色绑定权限")] public class GradeRoleAddDto { /// ///角色ID 点击选择角色 /// [PasteQuery("grade_id", false)] [Description("角色ID 点击选择角色")] [PasteOuter("gradeInfo", "extendGrade", "id", "name")] [PasteShort("GradeInfo", "ExtendGrade", "ToGradeShort()")] public int GradeId { get; set; } /// ///权限ID 点击选择权限 /// [PasteQuery("role_id", false)] [Description("权限ID 点击选择权限")] [PasteOuter("roleInfo", "extendRole", "id", "name")] [PasteShort("RoleInfo", "ExtendRole", "ToRoleShort()")] public int RoleId { get; set; } } /// ///角色绑定权限 /// [Description("角色绑定权限")] public class GradeRoleUpdateDto : GradeRoleAddDto { /// ///ID /// [PasteHidden] [Description("ID")] public int Id { get; set; } /// /// 权限 表示接口的权限 /// [PasteDisplay] [Description("权限 表示接口的权限")] public RoleShort ExtendRole { get; set; } /// /// 角色 表示权限的一个分组 /// [PasteDisplay] [Description("角色 表示权限的一个分组")] public GradeShort ExtendGrade { get; set; } } /// ///角色绑定权限 /// [Description("角色绑定权限")] public class GradeRoleDto : GradeRoleUpdateDto { } /// ///角色绑定权限 /// [Description("角色绑定权限")] [PasteLinkQuery("role_id,grade_id")] public class GradeRoleListDto : EntityDto { /// ///角色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; } ``` 下一个步骤就是重新生成,然后一键部署,发布了! 重新启动后的,效果如下 ![查询结果](./imgs/006.png) 如果你再次打开绑定权限的话,你会发现,你上次勾选的已经是打勾状态了(表示已经写入了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 种主流框架,踩过的坑可以绕地球两圈,现在坚定的「约定优先、简洁至上」主义者。