--- name: dedsiddd-dotnet-coding description: 用于在 DedsiDDD + .NET 项目中,按统一约定生成 DDD/CQRS 常见代码骨架(领域模型、DTO、EF Core 映射、仓储、Commands、Queries、Controller)。 --- ## 范围与目标 本 Skill 用于在 DedsiDDD + .NET 项目中生成可编译、可落地的代码骨架。 **冲突优先级**:本 SKILL 约定 > 现有仓库约定 > 一般 DDD/CQRS 习惯。 ## 交互协议(强制) - 信息不足先问(见"澄清问题清单") - 改造代码给"最小补丁" - 按文件分组输出 - 占位符 `[Project]`/`[Entity]`/`[DbContext]` 必须替换为实际命名 - 依赖方向不可破坏 ## 输入与输出 ### 澄清问题清单 1. 模块边界:实体属于哪个模块?命名空间与目录? 2. 路由风格:Controller 使用 action route 还是资源路由? 3. 聚合信息:实体单/复数名、主键类型(默认 string/ULID)、是否聚合根 4. 字段清单:字段名/类型/必填/长度/默认值/enum/值对象 5. 关系与集合:一对多/多对多、集合唯一性约束、更新策略(增量 vs Clear+Add) 6. 数据库映射:表名、Schema、索引/唯一约束、是否软删 7. API 契约:需要哪些端点(Get/Paged/Create/Update/Delete/Export) 8. 返回结构:是否有统一的 ApiResponse/Result 包装? ### 缺省策略 - 主键:默认 `string`,生成方式 `Ulid.NewUlid().ToString()` - 分页:使用 `DedsiPagedRequestDto` - 更新集合:默认 Clear + Add - 导出组件:仅当仓库已存在 MiniExcel 时提供落地代码 ### 交付标准 - 明确的文件清单与落点(Domain/Infrastructure/UseCase/HttpApi) - 每个文件的代码模板或最小变更补丁 - 所有公开入口包含 `CancellationToken` 并透传到 EF Core/仓储 - 分页查询顺序:筛选 → Count → 排序 →(非导出则分页)→ 投影 → ToList - Controller 能编译并能调用 Query/Mediator;Command/Query 返回类型与 Controller 对齐 ## 项目结构 ### 架构分层 - **领域**(`src/[Project].Domain`):核心领域对象、常量、领域模块 - **基础设施**(`src/[Project].Infrastructure`):EF Core 持久化、DbContext、实体映射、仓储 - **用例**(`src/[Project].UseCase`):CQRS 命令/查询与编排 - **HttpApi**(`src/[Project].HttpApi`):暴露控制器 - **宿主**(`host/[Project].Host`):ASP.NET Core Web 宿主 ### 依赖关系 ``` [Project].Host → [Project].HttpApi → [Project].UseCase → [Project].Domain ↓ [Project].Infrastructure → [Project].Domain ``` ### 存放位置速查 | 生成块 | 放置项目 | 目录 | 典型文件 | | --- | --- | --- | --- | | 领域模型 | `src/[Project].Domain` | `[Entities]/` | `[Entity].cs` | | DTO | `src/[Project].UseCase` | `[Entities]/Dtos/` | `[Entity]Dto.cs` / `[Entity]CreateUpdateDto.cs` | | DbContext | `src/[Project].Infrastructure` | `EntityFrameworkCore/` | `[Project]DbContext.cs` | | EF Core 映射 | `src/[Project].Infrastructure` | `EntityFrameworkCore/EntityConfigurations/` | `[Entity]Configuration.cs` | | 仓储 | `src/[Project].Infrastructure` | `Repositories/` | `[Entity]Repository.cs` | | Commands | `src/[Project].UseCase` | `[Entities]/CommandHandlers/` | `Create[Entity]CommandHandler.cs` | | Queries | `src/[Project].UseCase` | `[Entities]/Queries/` | `[Entity]Query.cs` / `[Entity]PagedQuery.cs` | | Controller | `src/[Project].HttpApi` | 随项目现有组织 | `[Entity]Controller.cs` | ### 最小交付清单 1. Domain:`[Entity].cs` 2. Infrastructure:`[Project]DbContext.cs` 增加 `DbSet<[Entity]>`、`[Entity]Configuration.cs`、`[Entity]Repository.cs` 3. UseCase:DTO、Command/Query 4. HttpApi:Controller + Requests(推荐) ## 通用约定 - **占位符**:`[Entity]` / `[Project]` / `[DbContext]` 按实际业务替换 - **文件聚合原则**:Command/Handler 同文件;Query 接口/实现同文件;Repository 接口/实现同文件 - **Enum 约束**:所有 `enum` 必须显式赋值,第一个业务值从 `1` 开始 - **CancellationToken**:所有公开 API/Query/Command 必须接收并透传到 EF Core/仓储 - **XML 注释**:Controller、Query 接口/实现、DTO 必须有 XML 注释;实现类可用 `/// ` ## 推荐工作流 1. 澄清边界与不变式 2. 写领域模型(Domain) 3. 定义 DTO(Contract) 4. 配置 EF Core(DbContext + EntityConfiguration) 5. 生成仓储(Repository) 6. 实现 Commands(写侧) 7. 实现 Queries(读侧) 8. 实现 Controller(API 层) 9. 一致性检查 --- ## 生成领域模型 ### 规则 - 聚合根继承 `DedsiAggregateRoot` - 必须包含 `protected` 无参构造函数(供 ORM) - 属性使用 `private set` - 状态变更通过聚合根方法:`Change+属性名`、`Add+元素名`、`Remove+元素名`、`Clear+集合名` - 入参校验:字符串 `Check.NotNullOrWhiteSpace`,引用类型 `Check.NotNull`,枚举值合法性检查 - 幂等性:新旧值相同直接返回 - 维护不变式:不满足时抛 `BusinessException` ### 模板 ```csharp using Volo.Abp; using Dedsi.Ddd.Domain.Entities; /// /// [Entity] /// public class [Entity] : DedsiAggregateRoot { protected [Entity]() { } public [Entity](string id, string requiredField) : base(id) { ChangeRequiredField(requiredField); CreationTime = DateTime.Now; } public DateTime CreationTime { get; private set; } /// /// 中文注释 /// public string RequiredField { get; private set; } public void ChangeRequiredField(string value) { RequiredField = Check.NotNullOrWhiteSpace(value, nameof(RequiredField)); } public ICollection<[Child]> Children { get; private set; } = []; public void AddChild([Child] child) { Children.Add(Check.NotNull(child, nameof([Child]))); } public void ClearChildren() { Children.Clear(); } } /// /// [Entity] /// public class [Child] { public string [Entity]Id { get; private set; } protected [Child]() { } public [Child](string id, string requiredField) : base(id) { ChangeRequiredField(requiredField); } /// /// 中文注释 /// public string RequiredField { get; private set; } public void ChangeRequiredField(string value) { RequiredField = Check.NotNullOrWhiteSpace(value, nameof(value)); } } ``` --- ## 生成 DTO ### 规则 - 展示 DTO:`[Entity]Dto`,使用 `public get; set;` - 创建/更新 DTO:`[Entity]CreateUpdateDto`,使用 `public get; set;` - 每个字段/属性必须有 XML 注释 - DTO 不直接暴露领域对象类型(为集合/复杂对象单独定义 DTO) ### 模板 ```csharp /// /// [Entity]Dto /// public class [Entity]Dto { /// /// 标识 /// public string Id { get; set; } /// /// 创建时间 /// public DateTime CreationTime { get; set; } /// /// 必填字段 /// public string RequiredField { get; set; } /// /// 可空字段 /// public string? OptionalField { get; set; } /// /// 列表字段 /// public IEnumerable<[Child]Dto> Children { get; set; } = []; } /// /// [Entity]CreateUpdateDto /// public class [Entity]CreateUpdateDto { /// /// 必填字段 /// public string RequiredField { get; set; } /// /// 可空字段 /// public string? OptionalField { get; set; } /// /// 列表字段 /// public IEnumerable<[Child]CreateUpdateDto> Children { get; set; } = []; } /// /// [Entity]PagedInputDto /// public class [Entity]PagedInputDto : DedsiPagedRequestDto { /// /// 关键字 /// public string? Keyword { get; set; } } /// /// [Entity]PagedRowDto /// public class [Entity]PagedRowDto { /// /// 主键 /// public string Id { get; set; } = default!; /// /// 示例字段 /// public string? Example { get; set; } } /// /// [Entity]PagedResultDto /// public class [Entity]PagedResultDto : DedsiPagedResultDto<[Entity]PagedRowDto>; ``` --- ## 配置数据库 ### DbContext 规则 - 命名:聚合根复数形式(例如 `Risks`) - 类型:`DbSet<聚合根>` - 位置:`I[Project]DbContext` / `[Project]DbContext` - 标注:`[ConnectionStringName([Project]DomainConsts.ConnectionStringName)]` - 接口:`I[Project]DbContext : IDedsiEfCoreDbContext` - 实现:`[Project]DbContext : DedsiEfCoreDbContext<[Project]DbContext>, I[Project]DbContext` - OnModelCreating:空值校验 → `ApplyConfigurationsFromAssembly` → `base.OnModelCreating` ### EntityConfiguration 规则 - 每个实体一个配置类:`[Entity]Configuration`,放在 `EntityFrameworkCore/EntityConfigurations` - `internal class`,实现 `IEntityTypeConfiguration<[EntityName]>` ### 模板 ```csharp // DbContext using Dedsi.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; namespace [Project].Infrastructure.EntityFrameworkCore; [ConnectionStringName([Project]DomainConsts.ConnectionStringName)] public interface I[Project]DbContext : IDedsiEfCoreDbContext { DbSet<[Entity]> [Entities] { get; } } [ConnectionStringName([Project]DomainConsts.ConnectionStringName)] public class [Project]DbContext(DbContextOptions<[Project]DbContext> options) : DedsiEfCoreDbContext<[Project]DbContext>(options), I[Project]DbContext { public DbSet<[Entity]> [Entities] { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { ArgumentNullException.ThrowIfNull(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(typeof([Project]DbContext).Assembly); base.OnModelCreating(modelBuilder); } } // EntityConfiguration using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace [Project].Infrastructure.EntityFrameworkCore.EntityConfigurations; internal class [Entity]Configuration : IEntityTypeConfiguration<[Entity]> { public void Configure(EntityTypeBuilder<[Entity]> builder) { builder.ToTable("[Entities]", [Project]DomainConsts.DbSchemaName); builder.HasKey(e => e.Id); builder.Property(e => e.RequiredField) .IsRequired() .HasMaxLength(128); } } ``` --- ## 生成仓储 ### 规则 - 接口:`I[Entity]Repository : IDedsiCqrsRepository<[Entity], KeyType>` - 实现:`[Entity]Repository : DedsiDddEfCoreRepository` - 接口和实现必须在同一个文件中 - 构造函数注入 `IDbContextProvider` ### 模板 ```csharp using Dedsi.Ddd.Domain.Repositories; using Dedsi.EntityFrameworkCore.Repositories; using Volo.Abp.EntityFrameworkCore; using [Project].Infrastructure.EntityFrameworkCore; namespace [Project].Infrastructure.Repositories; /// /// [Entity] 仓储 /// public interface I[Entity]Repository : IDedsiCqrsRepository<[Entity], string>; /// /// [Entity] 仓储 /// public class [Entity]Repository(IDbContextProvider<[Project]DbContext> dbContextProvider) : DedsiDddEfCoreRepository<[Project]DbContext, [Entity], string>(dbContextProvider), I[Entity]Repository; ``` --- ## 生成命令 (Commands) ### 规则 - Create/Update/Delete 拆分为三个独立文件 - Command 与 Handler 必须在同文件 - 继承 `DedsiCommandHandler` - 全链路透传 `CancellationToken` ### 模板 ```csharp // Create using Dedsi.Ddd.CQRS.CommandHandlers; using Dedsi.Ddd.CQRS.Commands; /// /// 创建 [Entity] 命令 /// public record Create[Entity]Command([Entity]CreateUpdateDto Dto) : DedsiCommand; /// /// 创建 [Entity] 命令处理器 /// public class Create[Entity]CommandHandler(I[Entity]Repository repository) : DedsiCommandHandler { public override async Task Handle(Create[Entity]Command command, CancellationToken cancellationToken) { var domainId = Ulid.NewUlid().ToString(); var domain = new [Entity](domainId, command.Dto.RequiredField); await repository.InsertAsync(domain, autoSave: true, cancellationToken); return domainId; } } /// /// 更新 [Entity] 命令 /// public record Update[Entity]Command(string Id, [Entity]CreateUpdateDto Dto) : DedsiCommand; /// /// 更新 [Entity] 命令处理器 /// public class Update[Entity]CommandHandler(I[Entity]Repository repository) : DedsiCommandHandler { public override async Task Handle(Update[Entity]Command command, CancellationToken cancellationToken) { var domain = await repository.GetAsync(e => e.Id == command.Id, true, cancellationToken); domain.ChangeRequiredField(command.Dto.RequiredField); await repository.UpdateAsync(domain, autoSave: true, cancellationToken); return true; } } // Delete /// /// 删除 [Entity] 命令 /// public record Delete[Entity]Command(string Id) : DedsiCommand; /// /// 删除 [Entity] 命令处理器 /// public class Delete[Entity]CommandHandler(I[Entity]Repository repository) : DedsiCommandHandler { public override async Task Handle(Delete[Entity]Command command, CancellationToken cancellationToken) { var domain = await repository.GetAsync(command.Id, true, cancellationToken); await repository.DeleteAsync(domain, true, cancellationToken); return true; } } ``` --- ## 生成查询 (Queries) ### 规则 - 接口与实现同文件 - 查询注入 DbContext 接口 - 分页查询必须 `AsNoTracking()` - 固定顺序:筛选 → Count → 排序 →(非导出则分页)→ 投影 → ToList - `id` 入参必须用于谓词 - Query 接口/实现/方法必须有 XML 注释 ### 模板 #### 分页查询 ```csharp using Dedsi.Ddd.Application.Contracts.Dtos; using Dedsi.Ddd.Domain.Queries; using Microsoft.EntityFrameworkCore; using Volo.Abp.Linq; /// /// [Entity] 分页查询 /// /// public class [Entity]PagedQuery(I[Project]DbContext [Project]DbContext) : IDedsiQuery { /// /// [Entity] 分页条件查询 /// /// /// /// public async Task<[Entity]PagedResultDto> PagedQueryAsync([Entity]PagedInputDto input, CancellationToken cancellationToken) { var query = [Project]DbContext .[Entities] .AsNoTracking() .WhereIf(!string.IsNullOrWhiteSpace(input.Keyword), e => e.RequiredField.Contains(input.Keyword!)); var totalCount = await query.CountAsync(cancellationToken); query = query.OrderByDescending(e => e.CreationTime); if (!input.IsExport) { query = query.PageBy(input.GetSkipCount(), input.PageSize); } var items = await query .Select(e => new [Entity]PagedRowDto { Id = e.Id, Example = e.RequiredField }) .ToListAsync(cancellationToken); return new [Entity]PagedResultDto { TotalCount = totalCount, Items = items }; } } ``` #### 单个查询 ```csharp using Dedsi.Ddd.Domain.Queries; /// /// [Entity] 查询 /// /// /// public class [Entity]Query( I[Project]DbContext [Project]DbContext, I[Entity]Repository [Entity]Repository) : IDedsiQuery { /// /// 获取详情 /// /// /// /// public async Task<[Entity]Dto> GetAsync(string id, CancellationToken cancellationToken) { var domain = await [Entity]Repository.GetAsync(e => e.Id == id, true, cancellationToken); return new [Entity]Dto { Id = domain.Id, CreationTime = domain.CreationTime, RequiredField = domain.RequiredField, OptionalField = domain.OptionalField, Children = domain.Children.Select(c => new [Child]Dto { /* 字段映射 */ }) }; } } ``` --- ## 生成控制器 (Controller) ### 规则 - 继承项目基础控制器 - 注入 `I[Entity]Query`、`I[Entity]PagedQuery`、`IDedsiMediator` - `CancellationToken` 使用 `HttpContext.RequestAborted` - 路由风格跟随仓库现有 Controller - Update Body 使用 `CreateUpdateDto`(写入契约),不使用 `Dto`(展示契约) - 导出:仅当仓库已引用 MiniExcel 时落地代码 ### Request 模型(推荐) ```csharp /// /// 创建请求对象 /// public record Create[Entity]Request([Entity]CreateUpdateDto [Entity]); /// /// 修改请求对象 /// public record Update[Entity]Request([Entity]CreateUpdateDto [Entity]); ``` ### 模板 ```csharp using Dedsi.Ddd.CQRS.Mediators; using Microsoft.AspNetCore.Mvc; using MiniExcelLibs; using MiniExcelLibs.OpenXml; /// /// [Entity] /// public class [Entity]Controller( [Entity]Query [Entity]Query, [Entity]PagedQuery [Entity]PagedQuery, IDedsiMediator dedsiMediator) : [Project]Controller { /// /// 分页查询 /// [HttpPost] public Task<[Entity]PagedResultDto> PagedQueryAsync([FromBody] [Entity]PagedInputDto input) { input.IsExport = false; return [Entity]PagedQuery.PagedQueryAsync(input, HttpContext.RequestAborted); } /// /// 导出 Excel /// [HttpPost("export")] public async Task ExportExcelAsync([FromBody] [Entity]PagedInputDto input) { input.IsExport = true; var result = await [Entity]PagedQuery.PagedQueryAsync(input, HttpContext.RequestAborted); var stream = new MemoryStream(); await stream.SaveAsAsync(result.Items, cancellationToken: HttpContext.RequestAborted); stream.Seek(0, SeekOrigin.Begin); return File( stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $"[Entity]-{DateTime.Now:yyyyMMddHHmmss}.xlsx" ); } /// /// 查询详情 /// [HttpGet("{id}")] public Task<[Entity]Dto> GetAsync([FromRoute] string id) => [Entity]Query.GetAsync(id, HttpContext.RequestAborted); /// /// 创建 /// [HttpPost] public Task CreateAsync([FromBody] Create[Entity]Request request) => dedsiMediator.SendAsync(new Create[Entity]Command(request.[Entity]), HttpContext.RequestAborted); /// /// 修改 /// [HttpPost("{id}")] public Task UpdateAsync([FromRoute] string id, [FromBody] Update[Entity]Request request) => dedsiMediator.SendAsync(new Update[Entity]Command(id, request.[Entity]), HttpContext.RequestAborted); /// /// 删除 /// [HttpPost("{id}")] public Task DeleteAsync([FromRoute] string id) => dedsiMediator.SendAsync(new Delete[Entity]Command(id), HttpContext.RequestAborted); } /// /// 创建请求对象 /// public record Create[Entity]Request([Entity]CreateUpdateDto [Entity]); /// /// 修改请求对象 /// public record Update[Entity]Request([Entity]CreateUpdateDto [Entity]); ``` --- ## 常见坑与验收清单 ### 常见坑 - Update 端点 Body 误用展示 `Dto`:必须用 `CreateUpdateDto` - Query 忘记 `AsNoTracking()` - 分页顺序错误:必须"筛选 → Count → 排序 →(非导出则分页)→ 投影 → ToList" - `CancellationToken` 断链 - `id` 入参未用于谓词 - enum 未显式赋值 ### 验收清单 1. 文件落点与依赖方向正确 2. Command/Handler、Query 接口/实现、Repository 接口/实现均同文件 3. Controller/Query/Command 显式接收并透传 `CancellationToken` 4. Create/Update/Delete 的返回类型与 Command 返回类型一致 5. Update/Delete 路由不冲突 6. PagedQuery 支持导出模式 7. Create/Update 的 Body 使用 `CreateUpdateDto` 8. 分页查询实现顺序正确