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)
This commit is contained in:
parent
9544bfe61f
commit
e29d731dd7
183
.github/copilot-instructions.md
vendored
183
.github/copilot-instructions.md
vendored
@ -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<T>` 而不是 `Validator<T>`
|
||||
- **领域事件处理器**: 实现 `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<OrderId> 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};
|
||||
```
|
||||
91
.github/instructions/aggregate.instructions.md
vendored
91
.github/instructions/aggregate.instructions.md
vendored
@ -1,91 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Domain/AggregatesModel/**/*.cs"
|
||||
---
|
||||
|
||||
# 聚合与强类型ID开发指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **聚合根定义**:
|
||||
- 聚合内必须有一个且只有一个聚合根。
|
||||
- 必须继承 `Entity<TId>` 并实现 `IAggregateRoot` 接口。
|
||||
- 必须有 `protected` 无参构造器供 EF Core 使用。
|
||||
- 所有属性使用 `private set`,并显式设置默认值。
|
||||
- 状态改变时发布领域事件,使用 `this.AddDomainEvent()`。
|
||||
- `Deleted` 属性表示软删除状态。
|
||||
- `RowVersion` 属性用于乐观并发控制。
|
||||
- **强类型ID定义**:
|
||||
- 必须使用 `IInt64StronglyTypedId` 或 `IGuidStronglyTypedId`,优先使用 `IGuidStronglyTypedId`。
|
||||
- 必须使用 `partial record` 声明,让框架生成具体实现。
|
||||
- 必须是 `public` 类型。
|
||||
- 必须与聚合/实体在同一个文件中定义。
|
||||
- 命名格式必须为 `{EntityName}Id`。
|
||||
- **子实体定义**:
|
||||
- 必须是 `public` 类。
|
||||
- 必须有一个无参构造器。
|
||||
- 必须有一个强类型ID,推荐使用 `IGuidStronglyTypedId`。
|
||||
- 必须继承自 `Entity<TId>`,并实现 `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<UserId>, 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
|
||||
}
|
||||
```
|
||||
89
.github/instructions/command.instructions.md
vendored
89
.github/instructions/command.instructions.md
vendored
@ -1,89 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Web/Application/Commands/**/*.cs"
|
||||
---
|
||||
|
||||
# 命令与命令处理器开发指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **命令定义**:
|
||||
- 使用 `record` 类型定义命令。
|
||||
- 无返回值命令实现 `ICommand` 接口。
|
||||
- 有返回值命令实现 `ICommand<TResponse>` 接口。
|
||||
- 必须为每个命令创建验证器,继承 `AbstractValidator<TCommand>`。
|
||||
- 命令处理器实现对应的 `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<UserId>;
|
||||
|
||||
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
|
||||
{
|
||||
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<CreateUserCommand, UserId>
|
||||
{
|
||||
public async Task<UserId> 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
40
.github/instructions/dbcontext.instructions.md
vendored
40
.github/instructions/dbcontext.instructions.md
vendored
@ -1,40 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Infrastructure/ApplicationDbContext.cs"
|
||||
---
|
||||
|
||||
# DbContext 添加聚合指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **命名空间**:在头部添加聚合根的命名空间。
|
||||
- **DbSet 定义**:
|
||||
- 添加新聚合时在 DbSet 区域添加对应属性。
|
||||
- 使用 `=> Set<T>()` 模式定义 DbSet。
|
||||
- **配置注册**:默认使用 `ApplyConfigurationsFromAssembly` 自动注册实体配置。
|
||||
|
||||
### 必须不要
|
||||
|
||||
- **额外配置**:无需手动注册实体配置,框架会自动扫描。
|
||||
|
||||
## 文件命名规则
|
||||
|
||||
- 文件在 `src/Fengling.Member.Infrastructure/ApplicationDbContext.cs`。
|
||||
|
||||
## 代码示例
|
||||
|
||||
**文件**: `src/Fengling.Member.Infrastructure/ApplicationDbContext.cs`
|
||||
|
||||
```csharp
|
||||
public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IMediator mediator)
|
||||
: AppDbContextBase(options, mediator)
|
||||
{
|
||||
// 现有的 DbSet
|
||||
public DbSet<Order> Orders => Set<Order>();
|
||||
public DbSet<DeliverRecord> DeliverRecords => Set<DeliverRecord>();
|
||||
|
||||
// 添加新聚合的 DbSet
|
||||
public DbSet<Customer> Customers => Set<Customer>();
|
||||
}
|
||||
```
|
||||
@ -1,54 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Web/Application/DomainEventHandlers/*.cs"
|
||||
---
|
||||
|
||||
# 领域事件处理器开发指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **处理器定义**:
|
||||
- 必须实现 `IDomainEventHandler<T>` 接口。
|
||||
- 实现方法:`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<OrderCreatedDomainEvent>
|
||||
{
|
||||
public async Task Handle(OrderCreatedDomainEvent domainEvent, CancellationToken cancellationToken)
|
||||
{
|
||||
// 通过发送命令操作聚合,而不是直接操作服务
|
||||
var command = new SetPaymentInfoCommand(domainEvent.OrderId, domainEvent.PaymentInfo);
|
||||
await mediator.Send(command, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -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;
|
||||
```
|
||||
121
.github/instructions/endpoint.instructions.md
vendored
121
.github/instructions/endpoint.instructions.md
vendored
@ -1,121 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Web/Endpoints/**/*.cs"
|
||||
---
|
||||
|
||||
# Endpoint 开发指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **端点定义**:
|
||||
- 继承对应的 `Endpoint` 基类。
|
||||
- 必须为每个 Endpoint 单独定义请求 DTO 和响应 DTO。
|
||||
- 请求 DTO、响应 DTO 与端点定义在同一文件中。
|
||||
- 不同的 Endpoint 放在不同文件中。
|
||||
- 使用 `ResponseData<T>` 包装响应数据。
|
||||
- 使用主构造函数注入依赖的服务,如 `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<CreateUserRequestDto, ResponseData<CreateUserResponseDto>>
|
||||
{
|
||||
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<CreateRequest, ResponseData<CreateResponse>>
|
||||
{
|
||||
// 实现
|
||||
}
|
||||
```
|
||||
@ -1,70 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Infrastructure/EntityConfigurations/*.cs"
|
||||
---
|
||||
|
||||
# 实体配置开发指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **配置定义**:
|
||||
- 必须实现 `IEntityTypeConfiguration<T>` 接口。
|
||||
- 每个实体一个配置文件。
|
||||
- **字段配置**:
|
||||
- 必须配置主键,使用 `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<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> 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();
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -1,49 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Web/Application/IntegrationEventConverters/*.cs"
|
||||
---
|
||||
|
||||
# 集成事件转换器开发指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **转换器定义**:
|
||||
- 必须实现 `IIntegrationEventConverter<TDomainEvent, TIntegrationEvent>` 接口。
|
||||
- 转换器负责从领域事件创建集成事件。
|
||||
- 集成事件使用 `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<UserCreatedDomainEvent, UserCreatedIntegrationEvent>
|
||||
{
|
||||
public UserCreatedIntegrationEvent Convert(UserCreatedDomainEvent domainEvent)
|
||||
{
|
||||
var user = domainEvent.User;
|
||||
return new UserCreatedIntegrationEvent(
|
||||
user.Id,
|
||||
user.Name,
|
||||
user.Email,
|
||||
DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -1,60 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Web/Application/IntegrationEventHandlers/*.cs"
|
||||
---
|
||||
|
||||
# 集成事件处理器开发指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **处理器定义**:
|
||||
- 必须实现 `IIntegrationEventHandler<T>` 接口。
|
||||
- 实现方法:`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<UserCreatedIntegrationEventHandlerForSendNotifyEmail> logger,
|
||||
IMediator mediator)
|
||||
: IIntegrationEventHandler<UserCreatedIntegrationEvent>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -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);
|
||||
```
|
||||
206
.github/instructions/query.instructions.md
vendored
206
.github/instructions/query.instructions.md
vendored
@ -1,206 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Web/Application/Queries/**/*.cs"
|
||||
---
|
||||
|
||||
# 查询开发指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **查询定义**:
|
||||
- 查询实现 `IQuery<TResponse>` 接口。
|
||||
- 分页查询实现 `IPagedQuery<TResponse>` 接口
|
||||
- 必须为每个查询创建验证器,继承 `AbstractValidator<TQuery>`。
|
||||
- 查询处理器实现 `IQueryHandler<TQuery, TResponse>` 接口。
|
||||
- 使用 `record` 类型定义查询和 DTO。
|
||||
- 框架默认会过滤掉软删除的数据(`Deleted(true)` 标记的数据)。
|
||||
- **数据访问**:
|
||||
- 直接使用 `ApplicationDbContext` 进行数据访问。
|
||||
- 所有数据库操作都应使用异步版本。
|
||||
- 将 `CancellationToken` 传递给所有异步操作。
|
||||
- **性能优化**:
|
||||
- 使用投影 (`Select`)、过滤 (`WhereIf`)、排序 (`OrderByIf`)、分页 (`ToPagedDataAsync`) 等优化性能。
|
||||
- 分页查询使用 `PagedData<T>` 类型包装分页结果。
|
||||
- 确保默认排序,在动态排序时始终提供默认排序字段。
|
||||
|
||||
### 必须不要
|
||||
|
||||
- **仓储使用**:避免在查询中调用仓储方法(仓储仅用于命令处理器)。
|
||||
- **关联查询**: 不要跨聚合使用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<UserDto>;
|
||||
|
||||
public class GetUserQueryValidator : AbstractValidator<GetUserQuery>
|
||||
{
|
||||
public GetUserQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId)
|
||||
.NotEmpty()
|
||||
.WithMessage("用户ID不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
public class GetUserQueryHandler(ApplicationDbContext context) : IQueryHandler<GetUserQuery, UserDto>
|
||||
{
|
||||
public async Task<UserDto> 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<PagedProductListItemDto>;
|
||||
|
||||
public class PagedProductQueryValidator : AbstractValidator<PagedProductQuery>
|
||||
{
|
||||
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<PagedProductQuery, PagedData<PagedProductListItemDto>>
|
||||
{
|
||||
public async Task<PagedData<PagedProductListItemDto>> 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<T>` 类型:
|
||||
|
||||
```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<T> 包含以下属性:
|
||||
// - Items: IEnumerable<T> - 当前页数据
|
||||
// - Total: int - 总记录数
|
||||
// - PageIndex: int - 当前页码
|
||||
// - PageSize: int - 每页大小
|
||||
```
|
||||
|
||||
**重要的using引用**:
|
||||
```csharp
|
||||
using Microsoft.EntityFrameworkCore; // 用于EF Core扩展方法
|
||||
// NetCorePal.Extensions.AspNetCore 已在GlobalUsings.cs中全局引用
|
||||
```
|
||||
91
.github/instructions/repository.instructions.md
vendored
91
.github/instructions/repository.instructions.md
vendored
@ -1,91 +0,0 @@
|
||||
---
|
||||
applyTo: "src/Fengling.Member.Infrastructure/Repositories/*.cs"
|
||||
---
|
||||
|
||||
# 仓储开发指南
|
||||
|
||||
## 开发原则
|
||||
|
||||
### 必须
|
||||
|
||||
- **仓储定义**:
|
||||
- 每个聚合根对应一个仓储。
|
||||
- 接口必须继承 `IRepository<TEntity, TKey>`。
|
||||
- 实现必须继承 `RepositoryBase<TEntity, TKey, TDbContext>`。
|
||||
- 接口和实现定义在同一个文件中。
|
||||
- **注册**:仓储类会被自动注册到依赖注入容器中,无需手动注册。
|
||||
- **实现**:
|
||||
- 使用 `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<AdminUser, AdminUserId>
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据用户名获取管理员
|
||||
/// </summary>
|
||||
Task<AdminUser?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 检查用户名是否存在
|
||||
/// </summary>
|
||||
Task<bool> UsernameExistsAsync(string username, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class AdminUserRepository(ApplicationDbContext context) :
|
||||
RepositoryBase<AdminUser, AdminUserId, ApplicationDbContext>(context), IAdminUserRepository
|
||||
{
|
||||
public async Task<AdminUser?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DbContext.AdminUsers.FirstOrDefaultAsync(x => x.Username == username, cancellationToken);
|
||||
}
|
||||
|
||||
// ...existing code...
|
||||
public async Task<bool> UsernameExistsAsync(string username, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DbContext.AdminUsers.AnyAsync(x => x.Username == username, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 框架默认实现的方法
|
||||
|
||||
框架的 `IRepository<TEntity>` 和 `IRepository<TEntity, TKey>` 接口已实现以下方法,无需在自定义仓储中重复实现:
|
||||
|
||||
### 基础操作
|
||||
- `IUnitOfWork UnitOfWork` - 获取工作单元对象
|
||||
- `TEntity Add(TEntity entity)` - 添加实体
|
||||
- `Task AddAsync(TEntity entity, CancellationToken cancellationToken = default)` - 异步添加实体
|
||||
- `void AddRange(IEnumerable<TEntity> entities)` - 批量添加实体
|
||||
- `Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default)` - 异步批量添加实体
|
||||
- `void Attach(TEntity entity)` - 附加实体(状态设为未更改)
|
||||
- `void AttachRange(IEnumerable<TEntity> entities)` - 批量附加实体
|
||||
- `TEntity Update(TEntity entity)` - 更新实体
|
||||
- `Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)` - 异步更新实体
|
||||
- `bool Delete(Entity entity)` - 删除实体
|
||||
- `Task DeleteAsync(Entity entity)` - 异步删除实体
|
||||
|
||||
### 主键操作(仅 IRepository<TEntity, TKey>)
|
||||
- `TEntity? Get(TKey id)` - 根据主键获取实体
|
||||
- `Task<TEntity?> GetAsync(TKey id, CancellationToken cancellationToken = default)` - 异步根据主键获取实体
|
||||
- `int DeleteById(TKey id)` - 根据主键删除实体
|
||||
- `Task<int> DeleteByIdAsync(TKey id, CancellationToken cancellationToken = default)` - 异步根据主键删除实体
|
||||
205
.github/instructions/unit-testing.instructions.md
vendored
205
.github/instructions/unit-testing.instructions.md
vendored
@ -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<UserCreatedDomainEvent>(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<KnownException>(() => 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<UserEmailUpdatedDomainEvent>(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<KnownException>(() =>
|
||||
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<KnownException>(() => 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<UserEmailUpdatedDomainEvent>().Single();
|
||||
Assert.Equal(user, emailEvent.User);
|
||||
|
||||
var nameEvent = events.OfType<UserNameUpdatedDomainEvent>().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);
|
||||
```
|
||||
8
NuGet.Config
Normal file
8
NuGet.Config
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="gitea" value="https://gitea.shtao1.cn/api/packages/fengling/nuget/index.json" />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -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.
|
||||
Loading…
Reference in New Issue
Block a user