From e29d731dd7d8b9a05bb14a8b95f2e27115d202dc Mon Sep 17 00:00:00 2001 From: Kimi CLI Date: Sun, 8 Mar 2026 15:49:20 +0800 Subject: [PATCH] docs: remove GSD Copilot instructions - Remove .github/instructions/ directory (14 files) - Remove .github/copilot-instructions.md - Remove scripts/EXAMPLES.md and README.md - Switch to CleanDDD workflow (cleanddd-* skills) --- .github/copilot-instructions.md | 183 ---------------- .../instructions/aggregate.instructions.md | 91 -------- .github/instructions/command.instructions.md | 89 -------- .../instructions/dbcontext.instructions.md | 40 ---- .../domain-event-handler.instructions.md | 54 ----- .../instructions/domain-event.instructions.md | 43 ---- .github/instructions/endpoint.instructions.md | 121 ---------- .../entity-configuration.instructions.md | 70 ------ ...ntegration-event-converter.instructions.md | 49 ----- .../integration-event-handler.instructions.md | 60 ----- .../integration-event.instructions.md | 44 ---- .github/instructions/query.instructions.md | 206 ------------------ .../instructions/repository.instructions.md | 91 -------- .../instructions/unit-testing.instructions.md | 205 ----------------- NuGet.Config | 8 + scripts/EXAMPLES.md | 151 ------------- scripts/README.md | 56 ----- 17 files changed, 8 insertions(+), 1553 deletions(-) delete mode 100644 .github/copilot-instructions.md delete mode 100644 .github/instructions/aggregate.instructions.md delete mode 100644 .github/instructions/command.instructions.md delete mode 100644 .github/instructions/dbcontext.instructions.md delete mode 100644 .github/instructions/domain-event-handler.instructions.md delete mode 100644 .github/instructions/domain-event.instructions.md delete mode 100644 .github/instructions/endpoint.instructions.md delete mode 100644 .github/instructions/entity-configuration.instructions.md delete mode 100644 .github/instructions/integration-event-converter.instructions.md delete mode 100644 .github/instructions/integration-event-handler.instructions.md delete mode 100644 .github/instructions/integration-event.instructions.md delete mode 100644 .github/instructions/query.instructions.md delete mode 100644 .github/instructions/repository.instructions.md delete mode 100644 .github/instructions/unit-testing.instructions.md create mode 100644 NuGet.Config delete mode 100644 scripts/EXAMPLES.md delete mode 100644 scripts/README.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index c9639d7..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,183 +0,0 @@ -你的任务是按照 *.instructions.md 描述的规范完成功能开发任务。 - -## 重要规则 - -- 优先遵循instructions文档描述的规范 - -## 最佳实践 - -- 优先使用主构造函数 -- 使用各类组件时,优先使用await关键字,使用Async方法 - -## 根据需要按照下列顺序完成工作 - -- 定义聚合、实体 -- 定义领域事件 -- 创建仓储接口与仓储实现 -- 配置实体映射 -- 定义命令与命令处理器 -- 定义查询与查询处理器 -- 定义Endpoints -- 定义领域事件处理器 -- 定义集成事件 -- 定义集成事件转换器 -- 定义集成事件处理器 - -## 项目结构 - -``` -Fengling.Member.sln -├── src/ -│ ├── Fengling.Member.Domain/ # 领域层 - 聚合根、实体、领域事件 -│ ├── Fengling.Member.Infrastructure/ # 基础设施层 - EF配置、仓储接口、仓储实现 -│ └── Fengling.Member.Web/ # 表现层 - API、应用服务 -└── test/ # 测试项目 - ├── Fengling.Member.Domain.UnitTests/ # 领域层测试项目 - ├── Fengling.Member.Infrastructure.UnitTests/ # 基础设施层测试项目 - └── Fengling.Member.Web.UnitTests/ # 表现层测试项目 -``` - -**分层依赖关系:** Web → Infrastructure → Domain (严格单向依赖) - - - -## 对于具体的开发工作,请参考以下详细指令文件: - -### 聚合与领域层 -- **聚合根开发**: 参考 `.github/instructions/aggregate.instructions.md` -- **领域事件定义**: 参考 `.github/instructions/domain-event.instructions.md` - -### 数据访问层 -- **仓储实现**: 参考 `.github/instructions/repository.instructions.md` -- **实体配置**: 参考 `.github/instructions/entity-configuration.instructions.md` -- **数据库上下文**: 参考 `.github/instructions/dbcontext.instructions.md` - -### 应用服务层 -- **命令处理**: 参考 `.github/instructions/command.instructions.md` -- **查询处理**: 参考 `.github/instructions/query.instructions.md` -- **领域事件处理**: 参考 `.github/instructions/domain-event-handler.instructions.md` - -### API表现层 -- **API端点**: 参考 `.github/instructions/endpoint.instructions.md` - -### 集成事件处理 -- **集成事件**: 参考 `.github/instructions/integration-event.instructions.md` -- **集成事件转换器**: 参考 `.github/instructions/integration-event-converter.instructions.md` -- **集成事件处理器**: 参考 `.github/instructions/integration-event-handler.instructions.md` - -### 测试 -- **单元测试**: 参考 `.github/instructions/unit-testing.instructions.md` - -### 最佳实践 -(遵循各模块对应的 *.instructions.md 文档;本节不再另行维护“通用最佳实践”文件以避免重复和漂移。) - -## 核心开发原则 - -### 文件组织 -- **聚合根** → `src/Fengling.Member.Domain/AggregatesModel/{AggregateFolder}/` -- **领域事件** → `src/Fengling.Member.Domain/DomainEvents/` -- **仓储** → `src/Fengling.Member.Infrastructure/Repositories/` -- **实体配置** → `src/Fengling.Member.Infrastructure/EntityConfigurations/` -- **命令与命令处理器** → `src/Fengling.Member.Web/Application/Commands/` -- **查询与查询处理器** → `src/Fengling.Member.Web/Application/Queries/` -- **API端点** → `src/Fengling.Member.Web/Endpoints/` -- **领域事件处理器** → `src/Fengling.Member.Web/Application/DomainEventHandlers/` -- **集成事件** → `src/Fengling.Member.Web/Application/IntegrationEvents/` -- **集成事件转换器** → `src/Fengling.Member.Web/Application/IntegrationEventConverters/` -- **集成事件处理器** → `src/Fengling.Member.Web/Application/IntegrationEventHandlers/` - -### 强制性要求 -- ✅ 所有聚合根使用强类型ID,且**不手动赋值ID**(依赖EF值生成器) -- ✅ 所有命令都要有对应的验证器 -- ✅ 领域事件在聚合发生改变时发布,仅聚合和实体可以发出领域事件 -- ✅ 遵循分层架构依赖关系 (Web → Infrastructure → Domain) -- ✅ 使用KnownException处理已知业务异常 -- ✅ 命令处理器**不调用SaveChanges**(框架自动处理) -- ✅ 仓储必须使用**异步方法**(GetAsync、AddAsync等) - -### 关键技术要求 -- **验证器**: 必须继承 `AbstractValidator` 而不是 `Validator` -- **领域事件处理器**: 实现 `Handle()` 方法而不是 `HandleAsync()` -- **FastEndpoints**: 使用构造函数注入 `IMediator`,使用 `Send.OkAsync()` 和 `.AsResponseData()`;端点采用属性特性配置(如 `[HttpPost]`、`[AllowAnonymous]`、`[Tags]`),不使用 `Configure()` -- **强类型ID**: 直接使用类型,避免 `.Value` 属性,依赖隐式转换 -- **仓储**: 通过构造函数参数访问 `ApplicationDbContext`,所有操作必须异步 -- **ID生成**: 使用EF Core值生成器,聚合根构造函数不设置ID - -## 异常处理原则 - -### KnownException使用规范 -在需要抛出业务异常的地方,必须使用 `KnownException` 而不是普通的 `Exception`: - -**正确示例:** -```csharp -// 在聚合根中 -public void OrderPaid() -{ - if (Paid) - { - throw new KnownException("Order has been paid"); - } - // 业务逻辑... -} - -// 在命令处理器中 -public async Task Handle(OrderPaidCommand request, CancellationToken cancellationToken) -{ - var order = await orderRepository.GetAsync(request.OrderId, cancellationToken) ?? - throw new KnownException($"未找到订单,OrderId = {request.OrderId}"); - order.OrderPaid(); - return order.Id; -} -``` - -**框架集成:** -- `KnownException` 会被框架自动转换为合适的HTTP状态码 -- 异常消息会直接返回给客户端 -- 支持本地化和错误码定制 - -## 常见using引用指南 - -### GlobalUsings.cs配置 -各层的常用引用已在GlobalUsings.cs中全局定义: - -**Web层** (`src/Fengling.Member.Web/GlobalUsings.cs`): -- `global using FluentValidation;` - 验证器 -- `global using MediatR;` - 命令处理器 -- `global using NetCorePal.Extensions.Primitives;` - KnownException等 -- `global using FastEndpoints;` - API端点 -- `global using NetCorePal.Extensions.Dto;` - ResponseData -- `global using NetCorePal.Extensions.Domain;` - 领域事件处理器 - -**Infrastructure层** (`src/Fengling.Member.Infrastructure/GlobalUsings.cs`): -- `global using Microsoft.EntityFrameworkCore;` - EF Core -- `global using Microsoft.EntityFrameworkCore.Metadata.Builders;` - 实体配置 -- `global using NetCorePal.Extensions.Primitives;` - 基础类型 - -**Domain层** (`src/Fengling.Member.Domain/GlobalUsings.cs`): -- `global using NetCorePal.Extensions.Domain;` - 领域基础类型 -- `global using NetCorePal.Extensions.Primitives;` - 强类型ID等 - -**Tests层** (`test/*/GlobalUsings.cs`): -- `global using Xunit;` - 测试框架 -- `global using NetCorePal.Extensions.Primitives;` - 测试中的异常处理 - -### 常见手动using引用 -当GlobalUsings未覆盖时,需要手动添加: - -**查询处理器**: -```csharp -using Fengling.Member.Domain.AggregatesModel.{AggregateFolder}; -using Fengling.Member.Infrastructure; -``` - -**实体配置**: -```csharp -using Fengling.Member.Domain.AggregatesModel.{AggregateFolder}; -``` - -**端点**: -```csharp -using Fengling.Member.Domain.AggregatesModel.{AggregateFolder}; -using Fengling.Member.Web.Application.Commands.{FeatureFolder}; -using Fengling.Member.Web.Application.Queries.{FeatureFolder}; -``` diff --git a/.github/instructions/aggregate.instructions.md b/.github/instructions/aggregate.instructions.md deleted file mode 100644 index 2b79cf9..0000000 --- a/.github/instructions/aggregate.instructions.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Domain/AggregatesModel/**/*.cs" ---- - -# 聚合与强类型ID开发指南 - -## 开发原则 - -### 必须 - -- **聚合根定义**: - - 聚合内必须有一个且只有一个聚合根。 - - 必须继承 `Entity` 并实现 `IAggregateRoot` 接口。 - - 必须有 `protected` 无参构造器供 EF Core 使用。 - - 所有属性使用 `private set`,并显式设置默认值。 - - 状态改变时发布领域事件,使用 `this.AddDomainEvent()`。 - - `Deleted` 属性表示软删除状态。 - - `RowVersion` 属性用于乐观并发控制。 -- **强类型ID定义**: - - 必须使用 `IInt64StronglyTypedId` 或 `IGuidStronglyTypedId`,优先使用 `IGuidStronglyTypedId`。 - - 必须使用 `partial record` 声明,让框架生成具体实现。 - - 必须是 `public` 类型。 - - 必须与聚合/实体在同一个文件中定义。 - - 命名格式必须为 `{EntityName}Id`。 -- **子实体定义**: - - 必须是 `public` 类。 - - 必须有一个无参构造器。 - - 必须有一个强类型ID,推荐使用 `IGuidStronglyTypedId`。 - - 必须继承自 `Entity`,并实现 `IEntity` 接口。 - - 聚合内允许多个子实体。 - -### 必须不要 - -- **依赖关系**: 聚合之间不相互依赖,避免直接引用其他聚合根,聚合之间也不共享子实体,使用领域事件或领域服务进行交互。 -- **命名**:聚合根类名不需要带后缀 `Aggregate`。 -- **ID设置**:无需手动设置ID的值,由 EF Core 值生成器自动生成。 - -## 文件命名规则 - -- 类文件应放置在 `src/Fengling.Member.Domain/AggregatesModel/{AggregateName}Aggregate/` 目录下。 -- 例如 `src/Fengling.Member.Domain/AggregatesModel/UserAggregate/User.cs`。 -- 每个聚合在独立文件夹中。 -- 聚合根类名与文件名一致。 -- 强类型ID与聚合根定义在同一文件中。 - -## 代码示例 - -文件: `src/Fengling.Member.Domain/AggregatesModel/UserAggregate/User.cs` - -```csharp -using Fengling.Member.Domain.DomainEvents; // 必需:引用领域事件 - -namespace Fengling.Member.Domain.AggregatesModel.UserAggregate; - -// 强类型ID定义 - 与聚合根在同一文件中 -public partial record UserId : IGuidStronglyTypedId; - -// 聚合根定义 -public class User : Entity, IAggregateRoot -{ - protected User() { } - - public User(string name, string email) - { - // 不手动设置ID,由EF Core值生成器自动生成 - Name = name; - Email = email; - this.AddDomainEvent(new UserCreatedDomainEvent(this)); - } - - #region Properties - - public string Name { get; private set; } = string.Empty; - public string Email { get; private set; } = string.Empty; - public Deleted Deleted { get; private set; } = new(); // 默认false - public RowVersion RowVersion { get; private set; } = new RowVersion(0); - - #endregion - - #region Methods - - // ...existing code... - public void ChangeEmail(string email) - { - Email = email; - this.AddDomainEvent(new UserEmailChangedDomainEvent(this)); - } - - #endregion -} -``` \ No newline at end of file diff --git a/.github/instructions/command.instructions.md b/.github/instructions/command.instructions.md deleted file mode 100644 index 6372850..0000000 --- a/.github/instructions/command.instructions.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Web/Application/Commands/**/*.cs" ---- - -# 命令与命令处理器开发指南 - -## 开发原则 - -### 必须 - -- **命令定义**: - - 使用 `record` 类型定义命令。 - - 无返回值命令实现 `ICommand` 接口。 - - 有返回值命令实现 `ICommand` 接口。 - - 必须为每个命令创建验证器,继承 `AbstractValidator`。 - - 命令处理器实现对应的 `ICommandHandler` 接口。 - - 命令处理器中必须通过仓储存取聚合数据。 - - 优先使用异步方法,所有仓储操作都应使用异步版本。 - - 将 `CancellationToken` 传递给所有异步操作。 - - 使用主构造函数注入所需的仓储或服务。 -- **异常处理**: - - 使用 `KnownException` 处理业务异常。 - - 业务验证失败时抛出明确的错误信息。 - -### 必须不要 - -- **事务管理**: - - 不要手动调用 `SaveChanges`,框架会自动在命令处理完成后调用。 - - 不要手动调用 `UpdateAsync`,如果实体是从仓储取出,则会自动跟踪变更。 -- **仓储使用**: - - 避免在命令处理器中进行复杂的查询操作(应使用查询端)。 - - 仓储方法名不应是通用数据访问,而应体现业务意图。 -- **重复引用**:无需重复添加 `GlobalUsings` 中已定义的 `using` 语句。 - -## 文件命名规则 - -- 类文件应放置在 `src/Fengling.Member.Web/Application/Commands/{Module}s/` 目录下(以模块复数形式命名,避免命名空间与聚合类名冲突)。 -- 命令文件名格式为 `{Action}{Entity}Command.cs`。 -- 同一个命令及其对应的验证器和处理器定义在同一文件中。 -- 不同的命令放在不同文件中。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Web/Application/Commands/Users/CreateUserCommand.cs` - -```csharp -using Fengling.Member.Domain.AggregatesModel.UserAggregate; -using Fengling.Member.Infrastructure.Repositories; - -namespace Fengling.Member.Web.Application.Commands.Users; - -public record CreateUserCommand(string Name, string Email) : ICommand; - -public class CreateUserCommandValidator : AbstractValidator -{ - public CreateUserCommandValidator() - { - RuleFor(x => x.Name) - .NotEmpty() - .WithMessage("用户名不能为空") - .MaximumLength(50) - .WithMessage("用户名不能超过50个字符"); - - RuleFor(x => x.Email) - .NotEmpty() - .WithMessage("邮箱不能为空") - .EmailAddress() - .WithMessage("邮箱格式不正确") - .MaximumLength(100) - .WithMessage("邮箱不能超过100个字符"); - } -} - -public class CreateUserCommandHandler(IUserRepository userRepository) - : ICommandHandler -{ - public async Task Handle(CreateUserCommand command, CancellationToken cancellationToken) - { - if (await userRepository.EmailExistsAsync(command.Email, cancellationToken)) - { - throw new KnownException("邮箱已存在"); - } - - var user = new User(command.Name, command.Email); - await userRepository.AddAsync(user, cancellationToken); - return user.Id; - } -} -``` diff --git a/.github/instructions/dbcontext.instructions.md b/.github/instructions/dbcontext.instructions.md deleted file mode 100644 index e9c3130..0000000 --- a/.github/instructions/dbcontext.instructions.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Infrastructure/ApplicationDbContext.cs" ---- - -# DbContext 添加聚合指南 - -## 开发原则 - -### 必须 - -- **命名空间**:在头部添加聚合根的命名空间。 -- **DbSet 定义**: - - 添加新聚合时在 DbSet 区域添加对应属性。 - - 使用 `=> Set()` 模式定义 DbSet。 -- **配置注册**:默认使用 `ApplyConfigurationsFromAssembly` 自动注册实体配置。 - -### 必须不要 - -- **额外配置**:无需手动注册实体配置,框架会自动扫描。 - -## 文件命名规则 - -- 文件在 `src/Fengling.Member.Infrastructure/ApplicationDbContext.cs`。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Infrastructure/ApplicationDbContext.cs` - -```csharp -public partial class ApplicationDbContext(DbContextOptions options, IMediator mediator) - : AppDbContextBase(options, mediator) -{ - // 现有的 DbSet - public DbSet Orders => Set(); - public DbSet DeliverRecords => Set(); - - // 添加新聚合的 DbSet - public DbSet Customers => Set(); -} -``` \ No newline at end of file diff --git a/.github/instructions/domain-event-handler.instructions.md b/.github/instructions/domain-event-handler.instructions.md deleted file mode 100644 index 26b73ff..0000000 --- a/.github/instructions/domain-event-handler.instructions.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Web/Application/DomainEventHandlers/*.cs" ---- - -# 领域事件处理器开发指南 - -## 开发原则 - -### 必须 - -- **处理器定义**: - - 必须实现 `IDomainEventHandler` 接口。 - - 实现方法:`public Task Handle(TEvent domainEvent, CancellationToken cancellationToken)`。 - - 每个文件仅包含一个事件处理器。 - - 使用主构造函数注入所需的仓储或服务。 - - **命名规范**:按 `{DomainEvent}DomainEventHandlerFor{Action}` 命名,语义清晰、单一目的。 -- **业务逻辑**: - - 在框架会将处理器中的命令作为事务的一部分执行。 - - 通过发送 Command(`IMediator.Send`)驱动聚合变化,而不是直接操作聚合或数据库。 - - 仅访问与该领域事件直接相关的数据或服务。 - - 尊重事务与取消:使用 `async/await`,传递并尊重 `CancellationToken`。 - -### 必须不要 - -- **直接操作**: - - 不直接通过仓储或 DbContext 修改数据(始终通过命令)。 -- **逻辑混合**: - - 不在一个文件中放多个处理器或混合不同事件的逻辑。 -- **性能与异常**: - - 不执行长时间阻塞操作,耗时操作应放置在集成事件处理器中。 - - 不吞掉异常或忽略 `CancellationToken`。 - -## 文件命名规则 - -- 类文件应放置在 `src/Fengling.Member.Web/Application/DomainEventHandlers/` 目录下。 -- 文件名格式为 `{DomainEvent}DomainEventHandlerFor{Action}.cs`,其中 `{Action}` 准确描述该 Handler 的目的。 -- 示例:`OrderCreatedDomainEventHandlerForSetPaymentInfo.cs`。 -- 类命名:`{DomainEvent}DomainEventHandlerFor{Action}`。 - -## 代码示例 -```csharp -using Fengling.Member.Web.Application.Commands; -using Fengling.Member.Web.Domain.DomainEvents; -public class OrderCreatedDomainEventHandlerForSetPaymentInfo(IMediator mediator) : - IDomainEventHandler -{ - public async Task Handle(OrderCreatedDomainEvent domainEvent, CancellationToken cancellationToken) - { - // 通过发送命令操作聚合,而不是直接操作服务 - var command = new SetPaymentInfoCommand(domainEvent.OrderId, domainEvent.PaymentInfo); - await mediator.Send(command, cancellationToken); - } -} -``` \ No newline at end of file diff --git a/.github/instructions/domain-event.instructions.md b/.github/instructions/domain-event.instructions.md deleted file mode 100644 index d6e508d..0000000 --- a/.github/instructions/domain-event.instructions.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Domain/DomainEvents/*.cs" ---- - -# 领域事件开发指南 - -## 开发原则 - -### 必须 - -- **事件定义**: - - 必须使用 `record` 类型。 - - 必须标记接口 `IDomainEvent`,无需额外实现。 - - 无额外信息传递需求时,将聚合作为构造函数参数。 -- **命名规范**: - - 使用过去式动词描述已发生的事情。 - - 格式:`{Entity}{Action}DomainEvent`。 - - 例如:`UserCreatedDomainEvent`、`OrderPaidDomainEvent`、`ProductUpdatedDomainEvent`。 - -### 必须不要 - -- **复杂逻辑**:领域事件本身不应包含业务逻辑,仅作为数据载体。 - -## 文件命名规则 - -- 类文件应放置在 `src/Fengling.Member.Domain/DomainEvents` 目录下。 -- 为每个聚合添加一个领域事件文件。 -- 文件名格式为 `{Aggregate}DomainEvents.cs`。 -- 一个领域事件文件中可以包含多个领域事件。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Domain/DomainEvents/UserDomainEvents.cs` - -```csharp -using Fengling.Member.Domain.Aggregates.UserAggregate; - -namespace Fengling.Member.Domain.DomainEvents; - -public record UserCreatedDomainEvent(User User) : IDomainEvent; - -public record UserEmailChangedDomainEvent(User User) : IDomainEvent; -``` \ No newline at end of file diff --git a/.github/instructions/endpoint.instructions.md b/.github/instructions/endpoint.instructions.md deleted file mode 100644 index 004d7cd..0000000 --- a/.github/instructions/endpoint.instructions.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Web/Endpoints/**/*.cs" ---- - -# Endpoint 开发指南 - -## 开发原则 - -### 必须 - -- **端点定义**: - - 继承对应的 `Endpoint` 基类。 - - 必须为每个 Endpoint 单独定义请求 DTO 和响应 DTO。 - - 请求 DTO、响应 DTO 与端点定义在同一文件中。 - - 不同的 Endpoint 放在不同文件中。 - - 使用 `ResponseData` 包装响应数据。 - - 使用主构造函数注入依赖的服务,如 `IMediator`。 -- **配置与实现**: - - 使用特性方式配置路由和权限:`[HttpPost("/api/...")]`、`[AllowAnonymous]` 等。 - - 在 `HandleAsync()` 方法中处理业务逻辑。 - - 使用构造函数注入 `IMediator` 发送命令或查询。 - - 使用 `Send.OkAsync()`、`Send.CreatedAsync()`、`Send.NoContentAsync()` 发送响应。 - - 使用 `.AsResponseData()` 扩展方法创建响应数据。 -- **强类型ID处理**: - - 在 DTO 中直接使用强类型 ID 类型,如 `UserId`、`OrderId`。 - - 依赖框架的隐式转换处理类型转换。 -- **引用**: - - 引用 `FastEndpoints` 和 `Microsoft.AspNetCore.Authorization`。 - -### 必须不要 - -- **配置方式**:使用属性特性而不是 `Configure()` 方法来配置端点。 -- **强类型ID**:避免使用 `.Value` 属性访问内部值。 - -## 文件命名规则 - -- 类文件应放置在 `src/Fengling.Member.Web/Endpoints/{Module}/` 目录下。 -- 端点文件名格式为 `{Action}{Entity}Endpoint.cs`。 -- 请求 DTO、响应 DTO 与端点定义在同一文件中。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Web/Endpoints/User/CreateUserEndpoint.cs` - -```csharp -using FastEndpoints; -using Fengling.Member.Web.Application.Commands; -using Fengling.Member.Domain.AggregatesModel.UserAggregate; -using Microsoft.AspNetCore.Authorization; - -namespace Fengling.Member.Web.Endpoints.User; - -public record CreateUserRequestDto(string Name, string Email); - -public record CreateUserResponseDto(UserId UserId); - -[Tags("Users")] -[HttpPost("/api/users")] -[AllowAnonymous] -public class CreateUserEndpoint(IMediator mediator) : Endpoint> -{ - public override async Task HandleAsync(CreateUserRequestDto req, CancellationToken ct) - { - var command = new CreateUserCommand(req.Name, req.Email); - var userId = await mediator.Send(command, ct); - - await Send.OkAsync(new CreateUserResponseDto(userId).AsResponseData(), cancellation: ct); - } -} -``` - -### 更多端点响应示例 - -#### 创建资源的端点 -```csharp -public override async Task HandleAsync(CreateUserRequestDto req, CancellationToken ct) -{ - var command = new CreateUserCommand(req.Name, req.Email); - var userId = await mediator.Send(command, ct); - - // ...existing code... - var response = new CreateUserResponseDto(userId); - await Send.CreatedAsync(response.AsResponseData(), ct); -} -``` - -#### 查询资源的端点 -```csharp -public override async Task HandleAsync(GetUserRequestDto req, CancellationToken ct) -{ - var query = new GetUserQuery(req.UserId); - var user = await mediator.Send(query, ct); - - await Send.OkAsync(user.AsResponseData(), ct); -} -``` - -#### 更新资源的端点 -```csharp -public override async Task HandleAsync(UpdateUserRequestDto req, CancellationToken ct) -{ - var command = new UpdateUserCommand(req.UserId, req.Name, req.Email); - await mediator.Send(command, ct); - - await Send.NoContentAsync(ct); -} -``` - -## 配置方式 - -端点使用属性模式配置,不使用 `Configure()` 方法: - -```csharp -[Tags("ModuleName")] -[HttpPost("/api/resource")] -[AllowAnonymous] -public class CreateResourceEndpoint(IMediator mediator) : Endpoint> -{ - // 实现 -} -``` diff --git a/.github/instructions/entity-configuration.instructions.md b/.github/instructions/entity-configuration.instructions.md deleted file mode 100644 index 645e753..0000000 --- a/.github/instructions/entity-configuration.instructions.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Infrastructure/EntityConfigurations/*.cs" ---- - -# 实体配置开发指南 - -## 开发原则 - -### 必须 - -- **配置定义**: - - 必须实现 `IEntityTypeConfiguration` 接口。 - - 每个实体一个配置文件。 -- **字段配置**: - - 必须配置主键,使用 `HasKey(x => x.Id)`。 - - 字符串属性必须设置最大长度。 - - 必填属性使用 `IsRequired()`。 - - 所有字段都不允许为 null,使用 `IsRequired()`。 - - 所有字段都必须提供注释说明。 - - 根据查询需求添加索引。 -- **强类型ID配置**: - - 对于强类型 ID,直接使用值生成器。 - - `IInt64StronglyTypedId` 使用 `UseSnowFlakeValueGenerator()`。 - - `IGuidStronglyTypedId` 使用 `UseGuidVersion7ValueGenerator()`。 - -### 必须不要 - -- **转换器**:不要使用 `HasConversion<{Id}.EfCoreValueConverter>()`,框架会自动处理强类型 ID 的转换。 -- **RowVersion**:`RowVersion` 无需配置。 - -## 文件命名规则 - -- 类文件应放置在 `src/Fengling.Member.Infrastructure/EntityConfigurations/` 目录下。 -- 文件名格式为 `{EntityName}EntityTypeConfiguration.cs`。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Infrastructure/EntityConfigurations/UserEntityTypeConfiguration.cs` - -```csharp -using Fengling.Member.Domain.AggregatesModel.UserAggregate; - -namespace Fengling.Member.Infrastructure.EntityConfigurations; - -public class UserEntityTypeConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("Users"); - - builder.HasKey(x => x.Id); - - builder.Property(x => x.Id) - .UseGuidVersion7ValueGenerator() - .HasComment("用户标识"); - - builder.Property(x => x.Name) - .IsRequired() - .HasMaxLength(50) - .HasComment("用户姓名"); - builder.Property(x => x.Email) - .IsRequired() - .HasMaxLength(100) - .HasComment("用户邮箱"); - - builder.HasIndex(x => x.Email) - .IsUnique(); - } -} -``` diff --git a/.github/instructions/integration-event-converter.instructions.md b/.github/instructions/integration-event-converter.instructions.md deleted file mode 100644 index 6daebc0..0000000 --- a/.github/instructions/integration-event-converter.instructions.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Web/Application/IntegrationEventConverters/*.cs" ---- - -# 集成事件转换器开发指南 - -## 开发原则 - -### 必须 - -- **转换器定义**: - - 必须实现 `IIntegrationEventConverter` 接口。 - - 转换器负责从领域事件创建集成事件。 - - 集成事件使用 `record` 类型定义。 -- **注册**:框架自动注册转换器。 - -### 必须不要 - -- **直接发布**:不要直接发布集成事件,应通过转换器。 - -## 文件命名规则 - -- 转换器应放置在 `src/Fengling.Member.Web/Application/IntegrationEventConverters/` 目录下。 -- 转换器文件名格式为 `{Entity}{Action}IntegrationEventConverter.cs`。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Web/Application/IntegrationEventConverters/UserCreatedIntegrationEventConverter.cs` - -```csharp -using Fengling.Member.Domain.DomainEvents; -using Fengling.Member.Web.Application.IntegrationEvents; - -namespace Fengling.Member.Web.Application.IntegrationEventConverters; - -public class UserCreatedIntegrationEventConverter - : IIntegrationEventConverter -{ - public UserCreatedIntegrationEvent Convert(UserCreatedDomainEvent domainEvent) - { - var user = domainEvent.User; - return new UserCreatedIntegrationEvent( - user.Id, - user.Name, - user.Email, - DateTime.UtcNow); - } -} -``` \ No newline at end of file diff --git a/.github/instructions/integration-event-handler.instructions.md b/.github/instructions/integration-event-handler.instructions.md deleted file mode 100644 index 2e9f973..0000000 --- a/.github/instructions/integration-event-handler.instructions.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Web/Application/IntegrationEventHandlers/*.cs" ---- - -# 集成事件处理器开发指南 - -## 开发原则 - -### 必须 - -- **处理器定义**: - - 必须实现 `IIntegrationEventHandler` 接口。 - - 实现方法:`public Task HandleAsync(TIntegrationEvent integrationEvent, CancellationToken cancellationToken)`。 - - 每个集成事件可以对应多个处理器,每个处理器对应特定的业务目的。 - - 使用主构造函数注入所需的服务。 -- **业务逻辑**: - - 处理来自其他服务的集成事件。 - - 主要用于跨服务的数据同步和业务协调。 - - 通过发送 Command 来操作聚合,而不是直接操作。 -- **注册**:框架自动注册事件处理器。 - -### 必须不要 - -- **直接操作**:不要直接操作聚合,应通过 Command。 - -## 文件命名规则 - -- 类文件应放置在 `src/Fengling.Member.Web/Application/IntegrationEventHandlers/` 目录下。 -- 文件名格式为 `{IntegrationEvent}HandlerFor{Action}.cs`,其中 `{Action}` 需要准确描述该 Handler 的目的。 -- 一个文件仅包含一个集成事件处理器。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Web/Application/IntegrationEventHandlers/UserCreatedIntegrationEventHandlerForSendNotifyEmail.cs` - -```csharp -using Fengling.Member.Web.Application.IntegrationEvents; -using Fengling.Member.Web.Application.Commands.Users; - -namespace Fengling.Member.Web.Application.IntegrationEventHandlers; - -public class UserCreatedIntegrationEventHandlerForSendNotifyEmail( - ILogger logger, - IMediator mediator) - : IIntegrationEventHandler -{ - public async Task HandleAsync(UserCreatedIntegrationEvent integrationEvent, CancellationToken cancellationToken) - { - logger.LogInformation("发送用户创建通知邮件:{UserId}", integrationEvent.UserId); - - // 通过Command发送通知邮件 - var command = new SendWelcomeEmailCommand( - integrationEvent.UserId, - integrationEvent.Email, - integrationEvent.Name); - - await mediator.Send(command, cancellationToken); - } -} -``` \ No newline at end of file diff --git a/.github/instructions/integration-event.instructions.md b/.github/instructions/integration-event.instructions.md deleted file mode 100644 index c38ff86..0000000 --- a/.github/instructions/integration-event.instructions.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Web/Application/IntegrationEvents/*.cs" ---- - -# 集成事件开发指南 - -## 开发原则 - -### 必须 - -- **事件定义**: - - 必须使用 `record` 类型。 - - 包含跨服务通信所需的关键数据。 - - 使用过去式动词描述已发生的事情。 - - 事件应该是不可变的。 -- **复杂类型**:如果需要复杂类型作为属性,则在同文件中定义,同样必须使用 `record` 类型。 - -### 必须不要 - -- **引用聚合**:不允许引用聚合。 -- **敏感信息**:避免包含敏感或过于详细的内部信息。 - -## 文件命名规则 - -- 类文件应放置在 `src/Fengling.Member.Web/Application/IntegrationEvents/` 目录下。 -- 文件名格式为 `{Entity}{Action}IntegrationEvent.cs`。 -- 每个集成事件一个文件。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Web/Application/IntegrationEvents/UserCreatedIntegrationEvent.cs` - -```csharp -using NetCorePal.Extensions.Domain; -using Fengling.Member.Domain.AggregatesModel.UserAggregate; - -namespace Fengling.Member.Web.Application.IntegrationEvents; - -public record UserCreatedIntegrationEvent( - UserId UserId, - string Name, - string Email, - DateTime CreatedTime); -``` diff --git a/.github/instructions/query.instructions.md b/.github/instructions/query.instructions.md deleted file mode 100644 index 012e761..0000000 --- a/.github/instructions/query.instructions.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Web/Application/Queries/**/*.cs" ---- - -# 查询开发指南 - -## 开发原则 - -### 必须 - -- **查询定义**: - - 查询实现 `IQuery` 接口。 - - 分页查询实现 `IPagedQuery` 接口 - - 必须为每个查询创建验证器,继承 `AbstractValidator`。 - - 查询处理器实现 `IQueryHandler` 接口。 - - 使用 `record` 类型定义查询和 DTO。 - - 框架默认会过滤掉软删除的数据(`Deleted(true)` 标记的数据)。 -- **数据访问**: - - 直接使用 `ApplicationDbContext` 进行数据访问。 - - 所有数据库操作都应使用异步版本。 - - 将 `CancellationToken` 传递给所有异步操作。 -- **性能优化**: - - 使用投影 (`Select`)、过滤 (`WhereIf`)、排序 (`OrderByIf`)、分页 (`ToPagedDataAsync`) 等优化性能。 - - 分页查询使用 `PagedData` 类型包装分页结果。 - - 确保默认排序,在动态排序时始终提供默认排序字段。 - -### 必须不要 - -- **仓储使用**:避免在查询中调用仓储方法(仓储仅用于命令处理器)。 -- **关联查询**: 不要跨聚合使用Join关联查询,应该通过多次查询再组合的方式实现。 -- **副作用**:查询不应修改任何数据状态。 -- **同步操作**:避免使用同步数据库操作。 - -## 文件命名规则 - -- 类文件应放置在 `src/Fengling.Member.Web/Application/Queries/{Module}s/` 目录下。 -- 查询文件名格式为 `{Action}{Entity}Query.cs`。 -- 查询、验证器、处理器和 DTO 定义在同一文件中。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Web/Application/Queries/Users/GetUserQuery.cs` - -```csharp -using Fengling.Member.Domain.AggregatesModel.UserAggregate; -using Fengling.Member.Infrastructure; -using Microsoft.EntityFrameworkCore; // 必需:用于EF Core扩展方法 - -namespace Fengling.Member.Web.Application.Queries.Users; - -public record GetUserQuery(UserId UserId) : IQuery; - -public class GetUserQueryValidator : AbstractValidator -{ - public GetUserQueryValidator() - { - RuleFor(x => x.UserId) - .NotEmpty() - .WithMessage("用户ID不能为空"); - } -} - -public class GetUserQueryHandler(ApplicationDbContext context) : IQueryHandler -{ - public async Task Handle(GetUserQuery request, CancellationToken cancellationToken) - { - var user = await context.Users - .Where(x => x.Id == request.UserId) - .Select(x => new UserDto(x.Id, x.Name, x.Email)) - .FirstOrDefaultAsync(cancellationToken) ?? - throw new KnownException($"未找到用户,UserId = {request.UserId}"); - - return user; - } -} - -public record UserDto(UserId Id, string Name, string Email); -``` - -### 分页查询示例 - -```csharp -using Fengling.Member.Domain.AggregatesModel.ProductAggregate; -using Fengling.Member.Infrastructure; -using Microsoft.EntityFrameworkCore; // 必需:用于EF Core扩展方法 - -namespace Fengling.Member.Web.Application.Queries.Products; - -public record PagedProductQuery(string? Name = null, CategoryId? CategoryId = null, decimal? MinPrice = null, decimal? MaxPrice = null, bool? IsActive = false, string? SortBy = null, int PageIndex = 1, int PageSize = 50, bool CountTotal = true) : IPagedQuery; - -public class PagedProductQueryValidator : AbstractValidator -{ - public PagedProductQueryValidator() - { - RuleFor(query => query.PageIndex) - .GreaterThanOrEqualTo(1) - .WithMessage("页码必须大于或等于1"); - RuleFor(query => query.PageSize) - .InclusiveBetween(1, 100) - .WithMessage("每页条数必须在1到100之间"); - } -} - -public class PagedProductQueryHandler(ApplicationDbContext context) : IQueryHandler> -{ - public async Task> Handle(PagedProductQuery request, CancellationToken cancellationToken) - { - return await context.Products - // 条件过滤 - .WhereIf(!string.IsNullOrWhiteSpace(request.Name), x => x.Name.Contains(request.Name!)) - .WhereIf(request.CategoryId.HasValue, x => x.CategoryId == request.CategoryId!.Value) - .WhereIf(request.MinPrice.HasValue, x => x.Price >= request.MinPrice!.Value) - .WhereIf(request.MaxPrice.HasValue, x => x.Price <= request.MaxPrice!.Value) - .WhereIf(request.IsActive.HasValue, x => x.IsActive == request.IsActive!.Value) - // 动态排序 - .OrderByIf(request.SortBy == "name", x => x.Name, request.Desc) - .ThenByIf(request.SortBy == "price", x => x.Price, request.Desc) - .ThenByIf(request.SortBy == "createTime", x => x.CreateTime, request.Desc) - .ThenByIf(string.IsNullOrEmpty(request.SortBy), x => x.Id) // 默认排序确保结果稳定 - // 数据投影 - .Select(p => new PagedProductListItemDto( - p.Id, - p.Name, - p.Price, - p.CategoryName, - p.IsActive, - p.CreateTime)) - // 分页处理 - .ToPagedDataAsync(request, cancellationToken: cancellationToken); - } -} -``` - -## 框架扩展方法 - -### WhereIf - 条件过滤 - -使用 `WhereIf` 方法可以根据条件动态添加 Where 子句,避免编写冗长的条件判断代码: - -```csharp -// 传统写法 -var query = context.Users.AsQueryable(); -if (!string.IsNullOrWhiteSpace(searchName)) -{ - query = query.Where(x => x.Name.Contains(searchName)); -} -if (isActive.HasValue) -{ - query = query.Where(x => x.IsActive == isActive.Value); -} - -// 使用 WhereIf 的简化写法 -var query = context.Users - .WhereIf(!string.IsNullOrWhiteSpace(searchName), x => x.Name.Contains(searchName!)) - .WhereIf(isActive.HasValue, x => x.IsActive == isActive!.Value); -``` - -### OrderByIf / ThenByIf - 条件排序 - -使用 `OrderByIf` 和 `ThenByIf` 方法可以根据条件动态添加排序: - -```csharp -// 复杂的动态排序示例 -var orderedQuery = context.Users - .OrderByIf(sortBy == "name", x => x.Name, desc) - .ThenByIf(sortBy == "email", x => x.Email, desc) - .ThenByIf(sortBy == "createTime", x => x.CreateTime, desc) - .ThenByIf(string.IsNullOrEmpty(sortBy), x => x.Id); // 默认排序 - -// 参数说明: -// - condition: 条件表达式,为 true 时才应用排序 -// - predicate: 排序字段表达式 -// - desc: 可选参数,是否降序排序,默认为 false(升序) -``` - -### ToPagedDataAsync - 分页数据 - -使用 `ToPagedDataAsync` 方法可以自动处理分页逻辑,返回 `PagedData` 类型: - -```csharp -// 基本分页用法 - 默认会查询总数 -var pagedResult = await query - .Select(u => new UserListItemDto(u.Id, u.Name, u.Email)) - .ToPagedDataAsync(pageIndex, pageSize, cancellationToken: cancellationToken); - -// 性能优化版本 - 不查询总数(适用于不需要显示总页数的场景) -var pagedResult = await query - .Select(u => new UserListItemDto(u.Id, u.Name, u.Email)) - .ToPagedDataAsync(pageIndex, pageSize, countTotal: false, cancellationToken); - -// 使用 IPageRequest 接口的版本 -var pagedResult = await query - .ToPagedDataAsync(pageRequest, cancellationToken); - -// PagedData 包含以下属性: -// - Items: IEnumerable - 当前页数据 -// - Total: int - 总记录数 -// - PageIndex: int - 当前页码 -// - PageSize: int - 每页大小 -``` - -**重要的using引用**: -```csharp -using Microsoft.EntityFrameworkCore; // 用于EF Core扩展方法 -// NetCorePal.Extensions.AspNetCore 已在GlobalUsings.cs中全局引用 -``` diff --git a/.github/instructions/repository.instructions.md b/.github/instructions/repository.instructions.md deleted file mode 100644 index f075cd4..0000000 --- a/.github/instructions/repository.instructions.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -applyTo: "src/Fengling.Member.Infrastructure/Repositories/*.cs" ---- - -# 仓储开发指南 - -## 开发原则 - -### 必须 - -- **仓储定义**: - - 每个聚合根对应一个仓储。 - - 接口必须继承 `IRepository`。 - - 实现必须继承 `RepositoryBase`。 - - 接口和实现定义在同一个文件中。 -- **注册**:仓储类会被自动注册到依赖注入容器中,无需手动注册。 -- **实现**: - - 使用 `DbContext` 属性访问当前的 `DbContext` 实例。 - - 在自定义仓储方法中,使用 `DbContext.EntitySetName` 访问具体的 DbSet。 - -### 必须不要 - -- **冗余方法**:默认基类已经实现了一组常用方法,如无必要,尽量不要定义新的仓储方法。 -- **重复引用**:无需重复添加 `Microsoft.EntityFrameworkCore` 引用(已在 `GlobalUsings.cs` 中定义)。 - -## 文件命名规则 - -- 仓储接口和实现应放置在 `src/Fengling.Member.Infrastructure/Repositories/` 目录下。 -- 文件名格式为 `{AggregateName}Repository.cs`。 - -## 代码示例 - -**文件**: `src/Fengling.Member.Infrastructure/Repositories/AdminUserRepository.cs` - -```csharp -using Fengling.Member.Domain.AggregatesModel.AdminUserAggregate; - -namespace Fengling.Member.Infrastructure.Repositories; - -// 接口和实现定义在同一文件中 -public interface IAdminUserRepository : IRepository -{ - /// - /// 根据用户名获取管理员 - /// - Task GetByUsernameAsync(string username, CancellationToken cancellationToken = default); - - /// - /// 检查用户名是否存在 - /// - Task UsernameExistsAsync(string username, CancellationToken cancellationToken = default); -} - -public class AdminUserRepository(ApplicationDbContext context) : - RepositoryBase(context), IAdminUserRepository -{ - public async Task GetByUsernameAsync(string username, CancellationToken cancellationToken = default) - { - return await DbContext.AdminUsers.FirstOrDefaultAsync(x => x.Username == username, cancellationToken); - } - - // ...existing code... - public async Task UsernameExistsAsync(string username, CancellationToken cancellationToken = default) - { - return await DbContext.AdminUsers.AnyAsync(x => x.Username == username, cancellationToken); - } -} -``` - -## 框架默认实现的方法 - -框架的 `IRepository` 和 `IRepository` 接口已实现以下方法,无需在自定义仓储中重复实现: - -### 基础操作 -- `IUnitOfWork UnitOfWork` - 获取工作单元对象 -- `TEntity Add(TEntity entity)` - 添加实体 -- `Task AddAsync(TEntity entity, CancellationToken cancellationToken = default)` - 异步添加实体 -- `void AddRange(IEnumerable entities)` - 批量添加实体 -- `Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default)` - 异步批量添加实体 -- `void Attach(TEntity entity)` - 附加实体(状态设为未更改) -- `void AttachRange(IEnumerable entities)` - 批量附加实体 -- `TEntity Update(TEntity entity)` - 更新实体 -- `Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)` - 异步更新实体 -- `bool Delete(Entity entity)` - 删除实体 -- `Task DeleteAsync(Entity entity)` - 异步删除实体 - -### 主键操作(仅 IRepository) -- `TEntity? Get(TKey id)` - 根据主键获取实体 -- `Task GetAsync(TKey id, CancellationToken cancellationToken = default)` - 异步根据主键获取实体 -- `int DeleteById(TKey id)` - 根据主键删除实体 -- `Task DeleteByIdAsync(TKey id, CancellationToken cancellationToken = default)` - 异步根据主键删除实体 \ No newline at end of file diff --git a/.github/instructions/unit-testing.instructions.md b/.github/instructions/unit-testing.instructions.md deleted file mode 100644 index 05d4a5c..0000000 --- a/.github/instructions/unit-testing.instructions.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -applyTo: "test/**/*.cs" ---- - -# 单元测试开发指南 - -## 开发原则 - -### 必须 - -- **测试模式**:使用 AAA 模式:Arrange、Act、Assert。 -- **测试范围**: - - 一个测试方法只测试一个场景。 - - 测试正常流程和异常流程。 - - 验证领域事件的发布。 - - 确保聚合根的业务规则始终满足(不变量)。 - - 验证状态变化的正确性。 - - 测试输入的边界值。 -- **命名规范**:测试方法命名清晰表达测试意图:`{Method}_{Scenario}_{ExpectedBehavior}`。 -- **数据驱动**:使用 `Theory` 和 `InlineData` 测试多个输入值。 -- **强类型ID**: - - 使用构造函数创建测试用的强类型 ID 实例:`new UserId(123)`。 - - 测试时直接比较强类型 ID,无需转换。 -- **时间处理**: - - 避免严格的时间比较 `>`,建议使用 `>=`。 - - 使用相对时间比较而非绝对时间比较。 -- **领域事件**: - - 使用 `GetDomainEvents()` 方法获取发布的事件。 - - 验证事件的类型和数量。 - -### 必须不要 - -- **时间比较**:不要使用严格的时间比较(如 `==` 或 `>`),因为执行时间会有微小差异。 - -## 文件命名规则 - -- 领域层测试:`test/Fengling.Member.Domain.Tests/{EntityName}Tests.cs`。 -- Web 层测试:`test/Fengling.Member.Web.Tests/{Feature}Tests.cs`。 -- 基础设施层测试:`test/Fengling.Member.Infrastructure.Tests/{Component}Tests.cs`。 - -## 代码示例 - -**基本聚合根测试** - -```csharp -public class UserTests -{ - [Fact] - public void User_Constructor_ShouldCreateValidUser() - { - // Arrange - var name = "张三"; - var email = "zhangsan@example.com"; - - // Act - var user = new User(name, email); - - // Assert - Assert.Equal(name, user.Name); - Assert.Equal(email, user.Email); - Assert.False(user.IsDeleted); - Assert.True(user.CreateTime <= DateTimeOffset.UtcNow); - - // 验证领域事件 - var domainEvents = user.GetDomainEvents(); - Assert.Single(domainEvents); - Assert.IsType(domainEvents.First()); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(null)] - public void User_Constructor_WithInvalidName_ShouldThrowException(string? name) - { - // Arrange - var email = "test@example.com"; - - // Act & Assert - Assert.Throws(() => new User(name!, email)); - } -} -``` - -**时间相关测试** - -```csharp -[Fact] -public void UpdateEmail_ShouldUpdateTimestamp() -{ - // Arrange - var user = new User("张三", "old@example.com"); - var originalUpdateTime = user.UpdateTime; - - // 确保时间差异 - Thread.Sleep(1); - - // Act - user.UpdateEmail("new@example.com"); - - // Assert - Assert.Equal("new@example.com", user.Email); - Assert.True(user.UpdateTime >= originalUpdateTime); - - // 验证领域事件 - var domainEvents = user.GetDomainEvents(); - Assert.Equal(2, domainEvents.Count); // 创建事件 + 更新事件 - Assert.IsType(domainEvents.Last()); -} -``` - -**强类型ID测试** - -```csharp -[Fact] -public void UserId_ShouldWorkCorrectly() -{ - // Arrange & Act - var userId1 = new UserId(123); - var userId2 = new UserId(123); - var userId3 = new UserId(456); - - // Assert - Assert.Equal(userId1, userId2); - Assert.NotEqual(userId1, userId3); - Assert.Equal("123", userId1.ToString()); -} -``` - -**业务规则测试** - -```csharp -[Fact] -public void ChangeEmail_OnDeletedUser_ShouldThrowException() -{ - // Arrange - var user = new User("张三", "test@example.com"); - user.Delete(); - - // Act & Assert - var exception = Assert.Throws(() => - user.UpdateEmail("new@example.com")); - Assert.Equal("无法修改已删除用户的邮箱", exception.Message); -} - -[Fact] -public void Delete_AlreadyDeletedUser_ShouldThrowException() -{ - // Arrange - var user = new User("张三", "test@example.com"); - user.Delete(); - - // Act & Assert - Assert.Throws(() => user.Delete()); -} -``` - -**领域事件详细测试** - -```csharp -[Fact] -public void User_BusinessOperations_ShouldPublishCorrectEvents() -{ - // Arrange - var user = new User("张三", "old@example.com"); - user.ClearDomainEvents(); // 清除构造函数事件 - - // Act - user.UpdateEmail("new@example.com"); - user.UpdateName("李四"); - - // Assert - var events = user.GetDomainEvents(); - Assert.Equal(2, events.Count); - - var emailEvent = events.OfType().Single(); - Assert.Equal(user, emailEvent.User); - - var nameEvent = events.OfType().Single(); - Assert.Equal(user, nameEvent.User); -} -``` - -### 测试数据工厂示例 - -使用Builder模式或Factory方法创建测试数据: - -```csharp -public static class TestDataFactory -{ - public static User CreateValidUser(string name = "测试用户", string email = "test@example.com") - { - return new User(name, email); - } - - public static UserId CreateUserId(long value = 123) - { - return new UserId(value); - } -} - -// 在测试中使用 -var user = TestDataFactory.CreateValidUser(); -var userId = TestDataFactory.CreateUserId(456); -``` diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..62a0353 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/scripts/EXAMPLES.md b/scripts/EXAMPLES.md deleted file mode 100644 index 73c509f..0000000 --- a/scripts/EXAMPLES.md +++ /dev/null @@ -1,151 +0,0 @@ -# Usage Examples - -This document provides practical examples for using the infrastructure initialization scripts. - -## Quick Start Examples - -### Default Setup (MySQL + Redis + RabbitMQ) -```bash -# Using Docker Compose (Recommended) -docker compose up -d - -# Using shell script (Linux/macOS) -./init-infrastructure.sh - -# Using PowerShell (Windows) -.\init-infrastructure.ps1 -``` - -### Different Database Options -```bash -# Use PostgreSQL instead of MySQL -docker compose --profile postgres up -d - -# Use SQL Server instead of MySQL -docker compose --profile sqlserver up -d - -# With PowerShell -.\init-infrastructure.ps1 -Postgres -.\init-infrastructure.ps1 -SqlServer -``` - -### Different Message Queue Options -```bash -# Use Kafka instead of RabbitMQ -docker compose --profile kafka up -d - -# With PowerShell -.\init-infrastructure.ps1 -Kafka -``` - -### Cleanup Examples -```bash -# Stop services, keep data -docker compose down -./clean-infrastructure.sh -.\clean-infrastructure.ps1 - -# Stop services and remove all data -docker compose down -v -./clean-infrastructure.sh --volumes -.\clean-infrastructure.ps1 -Volumes -``` - -## Development Workflow - -### Typical Development Session -```bash -# 1. Start infrastructure -cd scripts -docker compose up -d - -# 2. Develop your application -cd ../src/Fengling.Member.Web -dotnet run - -# 3. Run tests -cd ../../ -dotnet test - -# 4. Stop infrastructure (keep data) -cd scripts -docker compose down -``` - -### Clean Development Environment -```bash -# Clean slate - remove everything including data -cd scripts -docker compose down -v - -# Start fresh -docker compose up -d -``` - -## Troubleshooting - -### Check Service Status -```bash -# List running containers -docker ps - -# Check specific service logs -docker logs netcorepal-mysql -docker logs netcorepal-redis -docker logs netcorepal-rabbitmq - -# Check service health -docker compose ps -``` - -### Common Issues - -#### Port Already in Use -```bash -# Find what's using the port -netstat -tulpn | grep :3306 # Linux -netstat -ano | findstr :3306 # Windows - -# Stop conflicting services -sudo systemctl stop mysql # Linux -net stop mysql80 # Windows -``` - -#### Container Won't Start -```bash -# Remove problematic container and restart -docker rm -f netcorepal-mysql -docker compose up -d mysql -``` - -#### Data Corruption -```bash -# Remove data volumes and start fresh -docker compose down -v -docker compose up -d -``` - -## Connection Strings for Development - -Update your `appsettings.Development.json` with these connection strings: - -```json -{ - "ConnectionStrings": { - "Redis": "81.68.223.70:16379,password=sl52788542,defaultDatabase=0", - "MySql": "Server=localhost;Port=3306;Database=abctemplate;Uid=root;Pwd=123456;", - "SqlServer": "Server=localhost,1433;Database=abctemplate;User Id=sa;Password=Test123456!;TrustServerCertificate=true;", - "PostgreSQL": "Host=localhost;Port=15432;Database=abctemplate;Username=postgres;Password=123456;" - }, - "RabbitMQ": { - "HostName": "localhost", - "Port": 5672, - "UserName": "guest", - "Password": "guest", - "VirtualHost": "/" - }, - "Kafka": { - "BootstrapServers": "localhost:9092" - } -} -``` \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index eb9d22c..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Infrastructure Initialization Scripts - -This directory contains scripts to help developers quickly set up the infrastructure needed for development and debugging. - -## Available Scripts - -- `docker-compose.yml` - Complete infrastructure setup using Docker Compose -- `init-infrastructure.sh` - Shell script for Linux/macOS -- `init-infrastructure.ps1` - PowerShell script for Windows -- `clean-infrastructure.sh` - Cleanup script for Linux/macOS -- `clean-infrastructure.ps1` - Cleanup script for Windows - -## Quick Start - -### Using Docker Compose (Recommended) -```bash -# Start all infrastructure services -docker-compose up -d - -# Stop all services -docker-compose down - -# Stop and remove volumes (clean start) -docker-compose down -v -``` - -### Using Individual Scripts -```bash -# Linux/macOS -./init-infrastructure.sh - -# Windows PowerShell -.\init-infrastructure.ps1 -``` - -## Infrastructure Components - -The scripts will set up the following services: - -### Database Options -- **MySQL** (default): Port 3306, root password: 123456 -- **SQL Server**: Port 1433, SA password: Test123456! -- **PostgreSQL**: Port 5432, postgres password: 123456 - -### Cache & Message Queue -- **Redis**: Port 6379, no password -- **RabbitMQ**: Ports 5672 (AMQP), 15672 (Management UI), guest/guest -- **Kafka**: Port 9092 (when using Kafka option) - -### Management Interfaces -- RabbitMQ Management: http://localhost:15672 (guest/guest) -- Kafka UI (if included): http://localhost:8080 - -## Configuration - -The default configuration matches the test containers setup used in the project's integration tests. \ No newline at end of file