feat(console): complete migration of User, Tenant, and Role management APIs

This commit is contained in:
Sam 2026-02-05 14:21:36 +08:00
commit 05631b6589
99 changed files with 9739 additions and 0 deletions

25
.dockerignore Normal file
View File

@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

183
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,183 @@
你的任务是按照 *.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};
```

View File

@ -0,0 +1,91 @@
---
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
}
```

View File

@ -0,0 +1,89 @@
---
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;
}
}
```

View File

@ -0,0 +1,40 @@
---
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>();
}
```

View File

@ -0,0 +1,54 @@
---
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);
}
}
```

View File

@ -0,0 +1,43 @@
---
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;
```

View File

@ -0,0 +1,121 @@
---
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>>
{
// 实现
}
```

View File

@ -0,0 +1,70 @@
---
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();
}
}
```

View File

@ -0,0 +1,49 @@
---
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);
}
}
```

View File

@ -0,0 +1,60 @@
---
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);
}
}
```

View File

@ -0,0 +1,44 @@
---
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);
```

View File

@ -0,0 +1,206 @@
---
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中全局引用
```

View File

@ -0,0 +1,91 @@
---
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)` - 异步根据主键删除实体

View File

@ -0,0 +1,205 @@
---
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);
```

398
.gitignore vendored Normal file
View File

@ -0,0 +1,398 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
# Exception: allow frontend scripts bin directory
!src/frontend/scripts/**/bin/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
# Only ignore NuGet packages folders at project root level, not frontend workspace packages
/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Nuget personal access tokens and Credentials
# nuget.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/csharp.code-snippets
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
.idea/
*.sln.iml
/.vs
# Internal packages build outputs (generated by unbuild --stub, contains absolute paths)
src/frontend/internal/**/dist/
src/frontend/packages/**/dist/
src/frontend/scripts/**/dist/

34
Directory.Build.props Normal file
View File

@ -0,0 +1,34 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)\eng\versions.props"/>
<PropertyGroup>
<Authors>Fengling.Member</Authors>
<Product>Fengling.Member</Product>
<owners>Fengling.Member</owners>
<PackagePrefix>Fengling.Member</PackagePrefix>
<PackageIconUrl></PackageIconUrl>
<PackageProjectUrl></PackageProjectUrl>
<PackageLicenseUrl></PackageLicenseUrl>
<RepositoryType>git</RepositoryType>
<RepositoryUrl></RepositoryUrl>
<GenerateAssemblyConfigurationAttribute>True</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>True</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>True</GenerateAssemblyProductAttribute>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;NU1507;S125;CS9107;</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="$(IsTestProject) != 'true'">
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>$(WarningsAsErrors);CS8625;CS8604;CS8602;CS8600;CS8618;CS8601;CS8603</WarningsAsErrors>
</PropertyGroup>
<ItemGroup Condition="$(IsTestProject) != 'true'">
<PackageReference Include="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

3
Directory.Build.targets Normal file
View File

@ -0,0 +1,3 @@
<Project>
<!-- Keep empty for now, version management moved to Directory.Packages.props -->
</Project>

157
Directory.Packages.props Normal file
View File

@ -0,0 +1,157 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<PropertyGroup>
<!-- Third-party package versions -->
<NetCorePalVersion>3.2.1</NetCorePalVersion>
<FastEndpointsVersion>7.1.1</FastEndpointsVersion>
<TestcontainersVersion>4.9.0</TestcontainersVersion>
<AspireVersion>13.1.0</AspireVersion>
<OpenTelemetryVersion>1.14.0</OpenTelemetryVersion>
<NetCorePalTestcontainerVersion>1.0.5</NetCorePalTestcontainerVersion>
<NetCorePalAspireVersion>1.1.2</NetCorePalAspireVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.Feishu" Version="9.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.Weixin" Version="9.0.0" />
<!-- Database providers - framework specific versions -->
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageVersion Include="DM.Microsoft.EntityFrameworkCore" Version="9.0.0.37033" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql.Json.Microsoft" Version="9.0.0" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="9.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageVersion Include="DotNetCore.EntityFrameworkCore.GaussDB" Version="9.0.0" />
<PackageVersion Include="DotNetCore.EntityFrameworkCore.KingbaseES" Version="9.0.0" />
<PackageVersion Include="MongoDB.EntityFrameworkCore" Version="9.0.3" />
<!-- ASP.NET Core and Microsoft packages - framework specific versions -->
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" />
<!-- CAP packages for .NET 9.0+ -->
<PackageVersion Include="DotNetCore.CAP.Dashboard" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.Kafka" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.AzureServiceBus" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.AmazonSQS" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.NATS" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.RedisStreams" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.Pulsar" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.OpenTelemetry" Version="8.4.1" />
<!-- FastEndpoints -->
<PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" />
<PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" />
<PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.3.0" />
<!-- Other packages -->
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.1" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.3.1" />
<PackageVersion Include="MediatR" Version="12.5.0" />
<PackageVersion Include="MediatR.Contracts" Version="12.5.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="Hangfire.AspNetCore" Version="1.8.17" />
<PackageVersion Include="Hangfire.Redis.StackExchange" Version="1.9.4" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageVersion Include="Refit.Newtonsoft.Json" Version="8.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.ClientInfo" Version="2.1.2" />
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.9.32" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<!-- Aspire packages -->
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.0-preview.1.25616.3" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.MySql" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.MongoDB" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Redis" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.RabbitMQ" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Kafka" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.NATS" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.StackExchange.Redis" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Pomelo.EntityFrameworkCore.MySql" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(AspireVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.MongoDB.Extensions" Version="13.0.0" />
<PackageVersion Include="Aspire.RabbitMQ.Client" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Confluent.Kafka" Version="$(AspireVersion)" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.1.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.13.0-beta.1" />
<PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.8" />
<!-- NetCorePal packages -->
<PackageVersion Include="NetCorePal.Context.AspNetCore" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Context.CAP" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Context.Shared" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.AspNetCore" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.CodeAnalysis" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedLocks.Redis" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.MySql" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.SqlServer" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.PostgreSQL" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.Sqlite" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.DMDB" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.MongoDB" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.GaussDB" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.DistributedTransactions.CAP.KingbaseES" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Domain.Abstractions" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Jwt.StackExchangeRedis" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.MicrosoftServiceDiscovery" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.MultiEnv" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.NewtonsoftJson" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Primitives" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Repository.EntityFrameworkCore" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Repository.EntityFrameworkCore.Snowflake" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.OpenTelemetry.Diagnostics" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.DMDB" Version="$(NetCorePalAspireVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" />
<!-- Testing packages -->
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.MySql" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.MongoDb" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.MsSql" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.RabbitMq" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.Kafka" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.Nats" Version="$(TestcontainersVersion)" />
<PackageVersion Include="Testcontainers.Redis" Version="$(TestcontainersVersion)" />
<PackageVersion Include="NetCorePal.Testcontainers.DMDB" Version="$(NetCorePalTestcontainerVersion)" />
<PackageVersion Include="NetCorePal.Testcontainers.KingbaseES" Version="$(NetCorePalTestcontainerVersion)" />
<PackageVersion Include="NetCorePal.Testcontainers.OpenGauss" Version="$(NetCorePalTestcontainerVersion)" />
<PackageVersion Include="xunit.v3" Version="3.2.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="FastEndpoints.Testing" Version="$(FastEndpointsVersion)" />
<!-- Code analysis -->
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,918 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=2CBD6971A7955044AD2624B84FB49E38/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=40C163D436D8ED48A6D01A0AFEFC5556/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=567DCF4B487C244A9F6BB46E4E9F3B84/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=7F2A1BE8D0078241A9AE7802038BAD3C/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=C4795E57DDEC1C4F97BBC8C7173EBBCA/@KeyIndexDefined">False</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Description/@EntryValue">post-processor</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Shortcut/@EntryValue">postproc</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Text/@EntryValue">sealed class $name$ : IPostProcessor&lt;$dto$Request, $dto$Response&gt;
{
public Task PostProcessAsync($dto$Request req, $dto$Response res, HttpContext ctx, IReadOnlyCollection&lt;ValidationFailure&gt; fails, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=dto/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=dto/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=dto/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=name/Expression/@EntryValue">constant("MyProcessor")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=05F0D1722D0C7745B8D9365C6C73D7A9/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Description/@EntryValue">test class</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Shortcut/@EntryValue">tstclass</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Text/@EntryValue">namespace Tests;
public class $name$Tests : TestClass&lt;$fixture$Fixture&gt;
{
public $name$Tests($fixture$Fixture f, ITestOutputHelper o) : base(f, o) { }
[Fact]
public async Task $test_name$()
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=fixture/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=fixture/Expression/@EntryValue">constant("App")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=fixture/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=test_005Fname/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=test_005Fname/Expression/@EntryValue">constant("Name_Of_The_Test")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Field/=test_005Fname/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0A7BFA2DC992D84C93FE334B8E39B17E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Description/@EntryValue">endpoint with request only</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Shortcut/@EntryValue">epreq</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Text/@EntryValue">sealed class $epName$Request
{
}
sealed class $epName$Endpoint : Endpoint&lt;$epName$Request&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync($epName$Request r, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=verb/Expression/@EntryValue">constant("Post")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0FB9C31AB3C94342BEDBDE36AED0492D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Description/@EntryValue">创建命令</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Shortcut/@EntryValue">ncpcmd</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=11503521883F874CBBBCDE8259105A71/Text/@EntryValue">public record $name$Command() : ICommand;
public class $name$CommandValidator : AbstractValidator&lt;$name$Command&gt;
{
public $name$CommandValidator()
{
// 添加验证规则示例:
// RuleFor(x =&gt; x.Property).NotEmpty();
}
}
public class $name$CommandHandler : ICommandHandler&lt;$name$Command&gt;
{
public async Task Handle(
$name$Command request,
CancellationToken cancellationToken)
{
// 实现业务逻辑
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Description/@EntryValue">创建命令(含返回值)</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Shortcut/@EntryValue">ncpcmdres</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=18E2B14D9EF3204A8C2E916E06EA2F2A/Text/@EntryValue">public record $name$Command() : ICommand&lt;$name$CommandResponse&gt;;
public record $name$CommandResponse();
public class $name$CommandValidator : AbstractValidator&lt;$name$Command&gt;
{
public $name$CommandValidator()
{
// 添加验证规则示例:
// RuleFor(x =&gt; x.Property).NotEmpty();
}
}
public class $name$CommandHandler : ICommandHandler&lt;$name$Command, $name$CommandResponse&gt;
{
public async Task&lt;$name$CommandResponse&gt; Handle(
$name$Command request,
CancellationToken cancellationToken)
{
// 实现业务逻辑
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Description/@EntryValue">endpoint request &amp; response dtos</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Shortcut/@EntryValue">epdto</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Text/@EntryValue">sealed class $name$Request
{
$END$
}
sealed class $name$Response
{
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1BC67AC84CCD7E4BAE97977346E7EA96/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Description/@EntryValue">创建聚合根</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Shortcut/@EntryValue">ncpar</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=1C6DBF7652D0F84A971A70E4BD4F009B/Text/@EntryValue">public partial record $name$Id : IInt64StronglyTypedId;
public class $name$ : Entity&lt;$name$Id&gt;, IAggregateRoot
{
protected $name$() { }
}
</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Description/@EntryValue">test fixture</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Shortcut/@EntryValue">tstfixture</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Text/@EntryValue">namespace Tests;
public class $name$Fixture : TestFixture&lt;Program&gt;
{
public $name$Fixture(IMessageSink s) : base(s) { }
protected override Task SetupAsync()
{
$END$
}
protected override void ConfigureServices(IServiceCollection s)
{
}
protected override Task TearDownAsync()
{
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Field/=name/Expression/@EntryValue">constant("App")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=20C68DE5FC26A941B3663E08B95DABC0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Applicability/=File/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/CustomProperties/=Extension/@EntryIndexedValue">cs</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/CustomProperties/=FileName/@EntryIndexedValue">Endpoint</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/CustomProperties/=ValidateFileName/@EntryIndexedValue">False</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Description/@EntryValue">FastEndpoints Feature File Set</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Text/@EntryValue">namespace $name_space$;
sealed class Request
{
}
sealed class Validator : Validator&lt;Request&gt;
{
public Validator()
{
}
}
sealed class Response
{
public string Message =&gt; "This endpoint hasn't been implemented yet!";
}
sealed class Endpoint : Endpoint&lt;Request, Response, Mapper&gt;
{
public override void Configure()
{
Post("$route$");
}
public override async Task HandleAsync(Request r, CancellationToken c)
{
await SendAsync(new Response());$END$
}
}
sealed class Mapper : Mapper&lt;Request, Response, object&gt;
{
}
static class Data
{
}</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/UITag/@EntryValue">Class/Interface</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=name_005Fspace/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=name_005Fspace/Expression/@EntryValue">fileDefaultNamespace()</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=name_005Fspace/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Field/=route/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Scope/=E8F0594528C33E45BBFEC6CFE851095D/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=29F1FF6B85CF6B43810FB210D8429C05/Scope/=E8F0594528C33E45BBFEC6CFE851095D/Type/@EntryValue">InCSharpProjectFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Description/@EntryValue">event handler</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Shortcut/@EntryValue">evnt</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Text/@EntryValue">sealed class $name$ : IEvent
{
}
sealed class $name$Handler : IEventHandler&lt;$name$&gt;
{
public Task HandleAsync($name$ e, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Field/=name/Expression/@EntryValue">constant("MyEvent")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=32B4A14C9D8E8E41B84D726F3BF3B83B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Description/@EntryValue">创建仓储</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Shortcut/@EntryValue">ncprepo</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=3990E0976664CC47941C4C99D9633785/Text/@EntryValue">public interface I$name$Repository : IRepository&lt;$name$, $name$Id&gt;;
public class $name$Repository(ApplicationDbContext context)
: RepositoryBase&lt;$name$, $name$Id, ApplicationDbContext&gt;(context),
I$name$Repository
{
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Description/@EntryValue">endpoint data</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Shortcut/@EntryValue">epdat</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Text/@EntryValue">static class $name$Data
{
$END$
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=4A753F31FB59794B92CC342B3D916AF0/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Description/@EntryValue">command handler with result</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Shortcut/@EntryValue">cmdres</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Text/@EntryValue">sealed class $name$ : ICommand&lt;$name$Result&gt;
{
}
sealed class $name$Result
{
}
sealed class $name$Handler : ICommandHandler&lt;$name$, $name$Result&gt;
{
public Task&lt;$name$Result&gt; ExecuteAsync($name$ cmd, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Field/=name/Expression/@EntryValue">constant("MyCommand")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5608073DECE29447BBC916D9A45CB431/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Description/@EntryValue">command handler</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Shortcut/@EntryValue">cmd</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Text/@EntryValue">sealed class $name$ : ICommand
{
}
sealed class $name$Handler : ICommandHandler&lt;$name$&gt;
{
public Task ExecuteAsync($name$ cmd, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Field/=name/Expression/@EntryValue">constant("MyCommand")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=5CB8B97F878E2A429499C0BAFFAACA08/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Description/@EntryValue">endpoint validator</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Shortcut/@EntryValue">epval</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Text/@EntryValue">sealed class $name$Validator : Validator&lt;$name$Request&gt;
{
public $name$Validator()
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=6FE119A547576C409320655117DD5739/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Description/@EntryValue">global pre-processor</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Shortcut/@EntryValue">preproc_g</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Text/@EntryValue">sealed class $name$ : IGlobalPreProcessor
{
public Task PreProcessAsync(object r, HttpContext ctx, List&lt;ValidationFailure&gt; fails, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Field/=name/Expression/@EntryValue">constant("MyProcessor")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=708606B749489E419384C91A6AA44142/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Description/@EntryValue">endpoint with response only</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Shortcut/@EntryValue">epres</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Text/@EntryValue">sealed class $epName$Response
{
}
sealed class $epName$Endpoint : EndpointWithoutRequest&lt;$epName$Response&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=verb/Expression/@EntryValue">constant("Get")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=7447D2E4580AF646ABA86511009839CD/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Description/@EntryValue">创建集成事件与事件处理器</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Field/=name/Expression/@EntryValue">constant("MyCreated")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Shortcut/@EntryValue">ncpie</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=90130789CFD7A44DBCAAE38E926129FB/Text/@EntryValue">public record $name$IntegrationEvent();
public class $name$IntegrationEventHandler(IMediator mediator) : IIntegrationEventHandler&lt;$name$IntegrationEvent&gt;
{
public Task HandleAsync($name$IntegrationEvent eventData, CancellationToken cancellationToken = default)
{
// var cmd = new $name$Command(eventData.Id);
// return mediator.Send(cmd, cancellationToken);
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Description/@EntryValue">创建领域事件处理器</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Field/=name/Expression/@EntryValue">constant("MyCreated")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Shortcut/@EntryValue">ncpdeh</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=AD34247A5DABAF44A7194A6B98ECE566/Text/@EntryValue">public class $name$DomainEventHandler(IMediator mediator)
: IDomainEventHandler&lt;$name$DomainEvent&gt;
{
public async Task Handle($name$DomainEvent notification,
CancellationToken cancellationToken)
{
// 实现业务逻辑
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Description/@EntryValue">endpoint vertical slice - NCP</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=descriptionText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=descriptionText/Expression/@EntryValue">constant("Description text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=descriptionText/Order/@EntryValue">4</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=summaryText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=summaryText/Expression/@EntryValue">constant("Summary text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=summaryText/Order/@EntryValue">3</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=verb/Expression/@EntryValue">constant("Post")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Shortcut/@EntryValue">epp</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2DD7B7A51002B49B63B79F74E955FBF/Text/@EntryValue">sealed class $epName$Endpoint(IMediator mediator) : Endpoint&lt;$epName$Request, ResponseData&lt;$epName$Response&gt;&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync($epName$Request r, CancellationToken c)
{
var cmd = new $epName$Command(r.Property1, r.Property2);
var result = await mediator.Send(cmd, c);
var res = new $epName$Response();
await SendOkAsync(res.AsResponseData(), c);
$END$
}
}
sealed record $epName$Request();
sealed record $epName$Response();
sealed class $epName$Validator : Validator&lt;$epName$Request&gt;
{
public $epName$Validator()
{
// RuleFor(x =&gt; x.Property).NotEmpty();
}
}
sealed class $epName$Summary : Summary&lt;$epName$Endpoint, $epName$Request&gt;
{
public $epName$Summary()
{
Summary = "$summaryText$";
Description = "$descriptionText$";
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Description/@EntryValue">pre-processor</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Shortcut/@EntryValue">preproc</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Text/@EntryValue">sealed class $name$ : IPreProcessor&lt;$req$Request&gt;
{
public Task PreProcessAsync($req$Request r, HttpContext ctx, List&lt;ValidationFailure&gt; fails, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=name/Expression/@EntryValue">constant("MyProcessor")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=req/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=req/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Field/=req/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=B2F01B6412BAED4FBEDBDF8C42CAE748/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Description/@EntryValue">创建集成事件转换器</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Field/=name/Expression/@EntryValue">constant("MyCreated")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Shortcut/@EntryValue">ncpiec</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=BF0EE1C126A9094D821F52F7BD08449B/Text/@EntryValue">public class $name$IntegrationEventConverter
: IIntegrationEventConverter&lt;$name$DomainEvent, $name$IntegrationEvent&gt;
{
public $name$IntegrationEvent Convert($name$DomainEvent domainEvent)
{
// return new $name$IntegrationEvent(domainEvent.Id);
throw new NotImplementedException();
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Description/@EntryValue">endpoint mapper</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Shortcut/@EntryValue">epmap</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Text/@EntryValue">sealed class $epName$Mapper : Mapper&lt;$epName$Request, $epName$Response, $entity$&gt;
{
public override $entity$ ToEntity($epName$Request r) =&gt; new()
{
$END$
};
public override $epName$Response FromEntity($entity$ e) =&gt; new()
{
};
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=entity/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=entity/Expression/@EntryValue">constant("YourEntity")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=entity/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=C4140DCF16198B44BA1CA74DD48BA21D/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Description/@EntryValue">endpoint vertical slice</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=descriptionText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=descriptionText/Expression/@EntryValue">constant("Description text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=descriptionText/Order/@EntryValue">5</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=summaryText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=summaryText/Expression/@EntryValue">constant("Summary text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=summaryText/Order/@EntryValue">4</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Shortcut/@EntryValue">epfull</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Text/@EntryValue">sealed class $epName$Endpoint : Endpoint&lt;$epName$Request, $epName$Response, $epName$Mapper&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync($epName$Request r, CancellationToken c)
{
$END$
}
}
sealed class $epName$Request
{
}
sealed class $epName$Response
{
}
sealed class $epName$Validator : Validator&lt;$epName$Request&gt;
{
public $epName$Validator()
{
}
}
sealed class $epName$Mapper: Mapper&lt;$epName$Request, $epName$Response, $entity$&gt;
{
public override $entity$ ToEntity($epName$Request r) =&gt; new()
{
};
public override $epName$Response FromEntity($entity$ e) =&gt; new()
{
};
}
sealed class $epName$Summary : Summary&lt;$epName$Endpoint, $epName$Request&gt;
{
public $epName$Summary()
{
Summary = "$summaryText$";
Description = "$descriptionText$";
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=entity/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=entity/Expression/@EntryValue">constant("YourEntity")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=entity/Order/@EntryValue">3</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=verb/Expression/@EntryValue">constant("Post")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CE7A652209B10B4CB66F65C1D485A3E1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Description/@EntryValue">global post-processor</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Shortcut/@EntryValue">postproc_g</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Text/@EntryValue">sealed class $name$ : IGlobalPostProcessor
{
public Task PostProcessAsync(object req, object? res, HttpContext ctx, IReadOnlyCollection&lt;ValidationFailure&gt; fails, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Field/=name/Expression/@EntryValue">constant("MyProcessor")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E0AE343F732B3A4D82626D43412F1AEA/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Description/@EntryValue">test method</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Shortcut/@EntryValue">tstmethod</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Text/@EntryValue"> [Fact]
public async Task $test_name$()
{
$END$
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Field/=test_005Fname/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Field/=test_005Fname/Expression/@EntryValue">constant("Name_Of_The_Test")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Field/=test_005Fname/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E8C3ABB4F8689445AB1C755840C18308/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Description/@EntryValue">创建领域事件</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Field/=name/Expression/@EntryValue">constant("MyCreated")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Reformat/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Shortcut/@EntryValue">ncpde</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=EF36D18412E40944922BA7E05699EC20/Text/@EntryValue">public record $name$DomainEvent() : IDomainEvent;</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Description/@EntryValue">endpoint summary</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Shortcut/@EntryValue">epsum</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Text/@EntryValue">sealed class $name$Summary : Summary&lt;$name$Endpoint, $name$Request&gt;
{
public $name$Summary()
{
Summary = "$summaryText$";
Description = "$descriptionText$";
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=descriptionText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=descriptionText/Expression/@EntryValue">constant("Description text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=descriptionText/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=name/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=name/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=name/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=summaryText/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=summaryText/Expression/@EntryValue">constant("Summary text goes here...")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Field/=summaryText/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F98DF402F177C048910D14E90EA93054/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Description/@EntryValue">endpoint without request</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Shortcut/@EntryValue">epnoreq</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Text/@EntryValue">sealed class $My$Endpoint : EndpointWithoutRequest
{
public override void Configure()
{
$Get$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=Get/@KeyIndexDefined">True</s:Boolean>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=Get/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=My/@KeyIndexDefined">True</s:Boolean>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=My/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FCF9C07A45226F41A7460D9D04E6E7C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Description/@EntryValue">endpoint with request &amp; response</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Shortcut/@EntryValue">epreqres</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Text/@EntryValue">sealed class $epName$Request
{
}
sealed class $epName$Response
{
}
sealed class $epName$Endpoint : Endpoint&lt;$epName$Request, $epName$Response&gt;
{
public override void Configure()
{
$verb$("$route$");
AllowAnonymous();
}
public override async Task HandleAsync($epName$Request r, CancellationToken c)
{
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=epName/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=epName/Expression/@EntryValue">constant("My")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=epName/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=route/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=route/Expression/@EntryValue">constant("route-pattern")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=route/Order/@EntryValue">2</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=verb/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=verb/Expression/@EntryValue">constant("Post")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Field/=verb/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=FDE43092B97B5F45996AF67E4BAB0B34/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Description/@EntryValue">创建实体配置类</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Reformat/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Shortcut/@EntryValue">ncpconfig</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Text/@EntryValue">public class $Entity$Configuration : IEntityTypeConfiguration&lt;$Entity$&gt;
{
public void Configure(EntityTypeBuilder&lt;$Entity$&gt; builder)
{
builder.ToTable("$table$");
builder.HasKey(t =&gt; t.Id);
builder.Property(t =&gt; t.Id)
/*.UseSnowFlakeValueGenerator()*/ // 如果使用 SnowFlake ID 生成器,请取消注释
/*.UseGuidVersion7ValueGenerator()*/ // 如果使用 Guid Version 7 ID 生成器,请取消注释
;
// Configure other properties if needed
$END$
}
}</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=Entity/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=Entity/Expression/@EntryValue">constant("Entity")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=Entity/Order/@EntryValue">0</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=table/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=table/Expression/@EntryValue">constant("table")</s:String>
<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Field/=table/Order/@EntryValue">1</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=NCPCONFIG1234567890ABCDEF/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String></wpf:ResourceDictionary>

230
README.md Normal file
View File

@ -0,0 +1,230 @@
# Fengling.Member
## 环境准备
### 使用 Aspire推荐
如果您的项目启用了 Aspire 支持(使用 `--UseAspire` 参数创建),只需要 Docker 环境即可,无需手动配置各种基础设施服务。
```bash
# 仅需确保 Docker 环境运行
docker version
# 直接运行 AppHost 项目Aspire 会自动管理所有依赖服务
cd src/Fengling.Member.AppHost
dotnet run
```
Aspire 会自动为您:
- 启动和管理数据库容器MySQL、SQL Server、PostgreSQL、MongoDB 等)
- 启动和管理消息队列容器RabbitMQ、Kafka、NATS 等)
- 启动和管理 Redis 容器
- 提供统一的 Aspire Dashboard 界面查看所有服务状态
- 自动配置服务间的连接字符串和依赖关系
访问 Aspire Dashboard通常在 http://localhost:15888可以查看和管理所有服务。
### 推荐方式:使用初始化脚本(不使用 Aspire 时)
如果您没有启用 Aspire项目提供了完整的基础设施初始化脚本支持快速搭建开发环境
#### 使用 Docker Compose推荐
```bash
# 进入脚本目录
cd scripts
# 启动默认基础设施 (MySQL + Redis + RabbitMQ)
docker-compose up -d
# 使用 SQL Server 替代 MySQL
docker-compose --profile sqlserver up -d
# 使用 PostgreSQL 替代 MySQL
docker-compose --profile postgres up -d
# 使用 Kafka 替代 RabbitMQ
docker-compose --profile kafka up -d
# 停止所有服务
docker-compose down
# 停止并删除数据卷(完全清理)
docker-compose down -v
```
#### 使用初始化脚本
```bash
# Linux/macOS
cd scripts
./init-infrastructure.sh
# Windows PowerShell
cd scripts
.\init-infrastructure.ps1
# 清理环境
./clean-infrastructure.sh # Linux/macOS
.\clean-infrastructure.ps1 # Windows
```
### 手动方式:单独运行 Docker 容器
如果需要手动控制每个容器,可以使用以下命令:
```bash
# Redis
docker run --restart unless-stopped --name netcorepal-redis -p 6379:6379 -v netcorepal_redis_data:/data -d redis:7.2-alpine redis-server --appendonly yes --databases 1024
# MySQL
docker run --restart unless-stopped --name netcorepal-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_CHARACTER_SET_SERVER=utf8mb4 -e MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci -e TZ=Asia/Shanghai -v netcorepal_mysql_data:/var/lib/mysql -d mysql:8.0
# RabbitMQ
docker run --restart unless-stopped --name netcorepal-rabbitmq -p 5672:5672 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest -v netcorepal_rabbitmq_data:/var/lib/rabbitmq -d rabbitmq:3.12-management-alpine
```
### 服务访问信息
启动后,可以通过以下地址访问各个服务:
- **Redis**: `localhost:6379`
- **MySQL**: `localhost:3306` (root/123456)
- **RabbitMQ AMQP**: `localhost:5672` (guest/guest)
- **RabbitMQ 管理界面**: http://localhost:15672 (guest/guest)
- **SQL Server**: `localhost:1433` (sa/Test123456!)
- **PostgreSQL**: `localhost:5432` (postgres/123456)
- **Kafka**: `localhost:9092`
- **Kafka UI**: http://localhost:8080
## IDE 代码片段配置
本模板提供了丰富的代码片段,帮助您快速生成常用的代码结构。
### Visual Studio 配置
运行以下 PowerShell 命令自动安装代码片段:
```powershell
cd vs-snippets
.\Install-VSSnippets.ps1
```
或者手动安装:
1. 打开 Visual Studio
2. 转到 `工具` > `代码片段管理器`
3. 导入 `vs-snippets/NetCorePalTemplates.snippet` 文件
### VS Code 配置
VS Code 的代码片段已预配置在 `.vscode/csharp.code-snippets` 文件中,打开项目时自动生效。
### JetBrains Rider 配置
Rider 用户可以直接使用 `Fengling.Member.sln.DotSettings` 文件中的 Live Templates 配置。
### 可用的代码片段
#### NetCorePal (ncp) 快捷键
| 快捷键 | 描述 | 生成内容 |
|--------|------|----------|
| `ncpcmd` | NetCorePal 命令 | ICommand 实现(含验证器和处理器) |
| `ncpcmdres` | 命令(含返回值) | ICommand&lt;Response&gt; 实现 |
| `ncpar` | 聚合根 | Entity&lt;Id&gt; 和 IAggregateRoot |
| `ncprepo` | NetCorePal 仓储 | IRepository 接口和实现 |
| `ncpie` | 集成事件 | IntegrationEvent 和处理器 |
| `ncpdeh` | 域事件处理器 | IDomainEventHandler 实现 |
| `ncpiec` | 集成事件转换器 | IIntegrationEventConverter |
| `ncpde` | 域事件 | IDomainEvent 记录 |
#### Endpoint (ep) 快捷键
| 快捷键 | 描述 | 生成内容 |
|--------|------|----------|
| `epp` | FastEndpoint(NCP风格) | 完整的垂直切片实现 |
| `epreq` | 仅请求端点 | Endpoint&lt;Request&gt; |
| `epres` | 仅响应端点 | EndpointWithoutRequest&lt;Response&gt; |
| `epdto` | 端点 DTOs | Request 和 Response 类 |
| `epval` | 端点验证器 | Validator&lt;Request&gt; |
| `epmap` | 端点映射器 | Mapper&lt;Request, Response, Entity&gt; |
| `epfull` | 完整端点切片 | 带映射器的完整实现 |
| `epsum` | 端点摘要 | Summary&lt;Endpoint, Request&gt; |
| `epnoreq` | 无请求端点 | EndpointWithoutRequest |
| `epreqres` | 请求响应端点 | Endpoint&lt;Request, Response&gt; |
| `epdat` | 端点数据 | 静态数据类 |
更多详细配置请参考:[vs-snippets/README.md](vs-snippets/README.md)
## 依赖对框架与组件
+ [NetCorePal Cloud Framework](https://github.com/netcorepal/netcorepal-cloud-framework)
+ [ASP.NET Core](https://github.com/dotnet/aspnetcore)
+ [EFCore](https://github.com/dotnet/efcore)
+ [CAP](https://github.com/dotnetcore/CAP)
+ [MediatR](https://github.com/jbogard/MediatR)
+ [FluentValidation](https://docs.fluentvalidation.net/en/latest)
+ [Swashbuckle.AspNetCore.Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)
## 数据库迁移
```shell
# 安装工具 SEE https://learn.microsoft.com/zh-cn/ef/core/cli/dotnet#installing-the-tools
dotnet tool install --global dotnet-ef --version 9.0.0
# 强制更新数据库
dotnet ef database update -p src/Fengling.Member.Infrastructure
# 创建迁移 SEEhttps://learn.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli
dotnet ef migrations add InitialCreate -p src/Fengling.Member.Infrastructure
```
## 代码分析可视化
框架提供了强大的代码流分析和可视化功能帮助开发者直观地理解DDD架构中的组件关系和数据流向。
### 🎯 核心特性
+ **自动代码分析**:通过源生成器自动分析代码结构,识别控制器、命令、聚合根、事件等组件
+ **多种图表类型**:支持架构流程图、命令链路图、事件流程图、类图等多种可视化图表
+ **交互式HTML可视化**生成完整的交互式HTML页面内置导航和图表预览功能
+ **一键在线编辑**:集成"View in Mermaid Live"按钮,支持一键跳转到在线编辑器
### 🚀 快速开始
安装命令行工具来生成独立的HTML文件
```bash
# 安装全局工具
dotnet tool install -g NetCorePal.Extensions.CodeAnalysis.Tools
# 进入项目目录并生成可视化文件
cd src/Fengling.Member.Web
netcorepal-codeanalysis generate --output architecture.html
```
### ✨ 主要功能
+ **交互式HTML页面**
+ 左侧树形导航,支持不同图表类型切换
+ 内置Mermaid.js实时渲染
+ 响应式设计,适配不同设备
+ 专业的现代化界面
+ **一键在线编辑**
+ 每个图表右上角的"View in Mermaid Live"按钮
+ 智能压缩算法优化URL长度
+ 自动跳转到[Mermaid Live Editor](https://mermaid.live/)
+ 支持在线编辑、导出图片、生成分享链接
### 📖 详细文档
完整的使用说明和示例请参考:
+ [代码流分析文档](https://netcorepal.github.io/netcorepal-cloud-framework/zh/code-analysis/code-flow-analysis/)
+ [代码分析工具文档](https://netcorepal.github.io/netcorepal-cloud-framework/zh/code-analysis/code-analysis-tools/)
## 关于监控
这里使用了`prometheus-net`作为与基础设施prometheus集成的监控方案默认通过地址 `/metrics` 输出监控指标。
更多信息请参见:[https://github.com/prometheus-net/prometheus-net](https://github.com/prometheus-net/prometheus-net)

6
eng/versions.props Normal file
View File

@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
</PropertyGroup>
</Project>

151
scripts/EXAMPLES.md Normal file
View File

@ -0,0 +1,151 @@
# 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": "localhost:6379,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=5432;Database=abctemplate;Username=postgres;Password=123456;"
},
"RabbitMQ": {
"HostName": "localhost",
"Port": 5672,
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/"
},
"Kafka": {
"BootstrapServers": "localhost:9092"
}
}
```

56
scripts/README.md Normal file
View File

@ -0,0 +1,56 @@
# 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.

View File

@ -0,0 +1,195 @@
# NetCorePal Template - Infrastructure Cleanup Script (PowerShell)
# This script stops and removes all infrastructure containers
param(
[switch]$Volumes,
[switch]$Help
)
$ErrorActionPreference = "Stop"
# Color functions for output
function Write-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Blue
}
function Write-Success {
param([string]$Message)
Write-Host "[SUCCESS] $Message" -ForegroundColor Green
}
function Write-Warning {
param([string]$Message)
Write-Host "[WARNING] $Message" -ForegroundColor Yellow
}
function Write-Error {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
}
function Show-Help {
Write-Host "NetCorePal Template - Infrastructure Cleanup" -ForegroundColor Green
Write-Host "===========================================" -ForegroundColor Green
Write-Host ""
Write-Host "Usage: .\clean-infrastructure.ps1 [OPTIONS]"
Write-Host ""
Write-Host "Clean up NetCorePal Template infrastructure containers"
Write-Host ""
Write-Host "Options:"
Write-Host " -Help Show this help message"
Write-Host " -Volumes Also remove data volumes (WARNING: This will delete all data!)"
Write-Host ""
Write-Host "Examples:"
Write-Host " .\clean-infrastructure.ps1 # Stop and remove containers, keep data"
Write-Host " .\clean-infrastructure.ps1 -Volumes # Stop and remove containers and all data volumes"
Write-Host ""
}
function Remove-Container {
param([string]$ContainerName)
try {
$exists = docker ps -a --format "table {{.Names}}" | Select-String "^$ContainerName$"
if ($exists) {
Write-Info "Stopping and removing $ContainerName..."
# Stop the container
try {
docker stop $ContainerName 2>$null | Out-Null
Write-Info "$ContainerName stopped"
}
catch {
Write-Warning "Could not stop $ContainerName (may already be stopped)"
}
# Remove the container
try {
docker rm $ContainerName 2>$null | Out-Null
Write-Success "$ContainerName removed"
}
catch {
Write-Warning "Could not remove $ContainerName"
}
}
else {
Write-Info "$ContainerName not found, skipping..."
}
}
catch {
Write-Warning "Error processing $ContainerName : $_"
}
}
function Remove-Volumes {
param([bool]$RemoveVolumes)
if ($RemoveVolumes) {
Write-Info "Removing data volumes..."
$volumes = @(
"netcorepal_redis_data",
"netcorepal_mysql_data",
"netcorepal_sqlserver_data",
"netcorepal_postgres_data",
"netcorepal_rabbitmq_data",
"netcorepal_zookeeper_data",
"netcorepal_zookeeper_logs",
"netcorepal_kafka_data"
)
foreach ($volume in $volumes) {
try {
$exists = docker volume ls --format "table {{.Name}}" | Select-String "^$volume$"
if ($exists) {
docker volume rm $volume 2>$null | Out-Null
Write-Success "Volume $volume removed"
}
}
catch {
Write-Warning "Could not remove volume $volume"
}
}
}
else {
Write-Info "Preserving data volumes (use -Volumes to remove them)"
}
}
function Remove-Network {
try {
$exists = docker network ls --format "table {{.Name}}" | Select-String "^netcorepal-network$"
if ($exists) {
Write-Info "Removing network netcorepal-network..."
try {
docker network rm netcorepal-network 2>$null | Out-Null
Write-Success "Network removed"
}
catch {
Write-Warning "Could not remove network (may still be in use)"
}
}
}
catch {
Write-Warning "Error checking network: $_"
}
}
function Start-Cleanup {
Write-Host ""
Write-Info "Starting infrastructure cleanup..."
Write-Host ""
# List of containers to clean up
$containers = @(
"netcorepal-redis",
"netcorepal-mysql",
"netcorepal-sqlserver",
"netcorepal-postgres",
"netcorepal-rabbitmq",
"netcorepal-kafka",
"netcorepal-kafka-ui",
"netcorepal-zookeeper"
)
# Clean up containers
foreach ($container in $containers) {
Remove-Container -ContainerName $container
}
# Clean up volumes if requested
Remove-Volumes -RemoveVolumes $Volumes
# Clean up network
Remove-Network
Write-Host ""
Write-Success "🎉 Infrastructure cleanup completed!"
Write-Host ""
if ($Volumes) {
Write-Warning "⚠️ All data has been removed. You'll need to reinitialize your databases."
}
else {
Write-Info "💾 Data volumes preserved. Data will be available when you restart the infrastructure."
}
Write-Host ""
Write-Info "Use '.\init-infrastructure.ps1' to restart the infrastructure"
}
# Main execution
Write-Host "🧹 NetCorePal Template - Infrastructure Cleanup" -ForegroundColor Green
Write-Host "===============================================" -ForegroundColor Green
if ($Help) {
Show-Help
exit 0
}
try {
Start-Cleanup
}
catch {
Write-Error "An error occurred during cleanup: $_"
exit 1
}

View File

@ -0,0 +1,177 @@
#!/bin/bash
# NetCorePal Template - Infrastructure Cleanup Script
# This script stops and removes all infrastructure containers
set -e
echo "🧹 NetCorePal Template - Infrastructure Cleanup"
echo "==============================================="
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to stop and remove container
cleanup_container() {
local container_name=$1
if docker ps -a --format 'table {{.Names}}' | grep -q "^$container_name$"; then
print_status "Stopping and removing $container_name..."
# Stop the container
if docker stop $container_name > /dev/null 2>&1; then
print_status "$container_name stopped"
else
print_warning "Could not stop $container_name (may already be stopped)"
fi
# Remove the container
if docker rm $container_name > /dev/null 2>&1; then
print_success "$container_name removed"
else
print_warning "Could not remove $container_name"
fi
else
print_status "$container_name not found, skipping..."
fi
}
# Function to remove volumes
cleanup_volumes() {
local remove_volumes=$1
if [ "$remove_volumes" = "true" ]; then
print_status "Removing data volumes..."
local volumes=(
"netcorepal_redis_data"
"netcorepal_mysql_data"
"netcorepal_sqlserver_data"
"netcorepal_postgres_data"
"netcorepal_rabbitmq_data"
"netcorepal_zookeeper_data"
"netcorepal_zookeeper_logs"
"netcorepal_kafka_data"
)
for volume in "${volumes[@]}"; do
if docker volume ls --format 'table {{.Name}}' | grep -q "^$volume$"; then
if docker volume rm "$volume" > /dev/null 2>&1; then
print_success "Volume $volume removed"
else
print_warning "Could not remove volume $volume"
fi
fi
done
else
print_status "Preserving data volumes (use --volumes to remove them)"
fi
}
# Function to remove network
cleanup_network() {
if docker network ls --format 'table {{.Name}}' | grep -q "^netcorepal-network$"; then
print_status "Removing network netcorepal-network..."
if docker network rm netcorepal-network > /dev/null 2>&1; then
print_success "Network removed"
else
print_warning "Could not remove network (may still be in use)"
fi
fi
}
# Main cleanup function
main() {
local remove_volumes=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--volumes|-v)
remove_volumes=true
shift
;;
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo
echo "Clean up NetCorePal Template infrastructure containers"
echo
echo "Options:"
echo " -h, --help Show this help message"
echo " -v, --volumes Also remove data volumes (WARNING: This will delete all data!)"
echo
echo "Examples:"
echo " $0 # Stop and remove containers, keep data"
echo " $0 --volumes # Stop and remove containers and all data volumes"
exit 0
;;
*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
echo
print_status "Starting infrastructure cleanup..."
echo
# List of containers to clean up
local containers=(
"netcorepal-redis"
"netcorepal-mysql"
"netcorepal-sqlserver"
"netcorepal-postgres"
"netcorepal-rabbitmq"
"netcorepal-kafka"
"netcorepal-kafka-ui"
"netcorepal-zookeeper"
)
# Clean up containers
for container in "${containers[@]}"; do
cleanup_container "$container"
done
# Clean up volumes if requested
cleanup_volumes "$remove_volumes"
# Clean up network
cleanup_network
echo
print_success "🎉 Infrastructure cleanup completed!"
echo
if [ "$remove_volumes" = "true" ]; then
print_warning "⚠️ All data has been removed. You'll need to reinitialize your databases."
else
print_status "💾 Data volumes preserved. Data will be available when you restart the infrastructure."
fi
echo
print_status "Use './init-infrastructure.sh' to restart the infrastructure"
}
# Execute main function with all arguments
main "$@"

167
scripts/docker-compose.yml Normal file
View File

@ -0,0 +1,167 @@
services:
# Redis - Always included for caching and sessions
redis:
image: redis:7.2-alpine
container_name: netcorepal-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes --databases 1024
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# MySQL Database (default option)
mysql:
image: mysql:8.0
container_name: netcorepal-mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_CHARACTER_SET_SERVER: utf8mb4
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
TZ: Asia/Shanghai
volumes:
- mysql_data:/var/lib/mysql
- ./mysql-init:/docker-entrypoint-initdb.d:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p123456"]
interval: 10s
timeout: 5s
retries: 5
# SQL Server (alternative database option)
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: netcorepal-sqlserver
ports:
- "1433:1433"
environment:
ACCEPT_EULA: Y
MSSQL_SA_PASSWORD: Test123456!
TZ: Asia/Shanghai
volumes:
- sqlserver_data:/var/opt/mssql
restart: unless-stopped
profiles:
- sqlserver
healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Test123456! -Q 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 5
# PostgreSQL (alternative database option)
postgres:
image: postgres:15-alpine
container_name: netcorepal-postgres
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123456
POSTGRES_DB: postgres
TZ: Asia/Shanghai
volumes:
- postgres_data:/var/lib/postgresql/data
- ./postgres-init:/docker-entrypoint-initdb.d:ro
restart: unless-stopped
profiles:
- postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 3
# RabbitMQ (default message queue option)
rabbitmq:
image: rabbitmq:3.12-management-alpine
container_name: netcorepal-rabbitmq
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
volumes:
- rabbitmq_data:/var/lib/rabbitmq
restart: unless-stopped
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 3
# Kafka (alternative message queue option)
zookeeper:
image: confluentinc/cp-zookeeper:7.4.0
container_name: netcorepal-zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
volumes:
- zookeeper_data:/var/lib/zookeeper/data
- zookeeper_logs:/var/lib/zookeeper/log
restart: unless-stopped
profiles:
- kafka
kafka:
image: confluentinc/cp-kafka:7.4.0
container_name: netcorepal-kafka
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
volumes:
- kafka_data:/var/lib/kafka/data
restart: unless-stopped
profiles:
- kafka
healthcheck:
test: ["CMD-SHELL", "kafka-broker-api-versions --bootstrap-server localhost:9092"]
interval: 10s
timeout: 5s
retries: 5
# Kafka UI (optional management interface)
kafka-ui:
image: provectuslabs/kafka-ui:latest
container_name: netcorepal-kafka-ui
depends_on:
- kafka
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
restart: unless-stopped
profiles:
- kafka
volumes:
redis_data:
mysql_data:
sqlserver_data:
postgres_data:
rabbitmq_data:
zookeeper_data:
zookeeper_logs:
kafka_data:
networks:
default:
name: netcorepal-network

View File

@ -0,0 +1,258 @@
# NetCorePal Template - Infrastructure Initialization Script (PowerShell)
# This script initializes the required infrastructure for development
param(
[switch]$SqlServer,
[switch]$Postgres,
[switch]$Kafka,
[switch]$Help
)
$ErrorActionPreference = "Stop"
# Color functions for output
function Write-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Blue
}
function Write-Success {
param([string]$Message)
Write-Host "[SUCCESS] $Message" -ForegroundColor Green
}
function Write-Warning {
param([string]$Message)
Write-Host "[WARNING] $Message" -ForegroundColor Yellow
}
function Write-Error {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
}
function Show-Help {
Write-Host "NetCorePal Template - Infrastructure Initialization" -ForegroundColor Green
Write-Host "=================================================" -ForegroundColor Green
Write-Host ""
Write-Host "Usage: .\init-infrastructure.ps1 [OPTIONS]"
Write-Host ""
Write-Host "Initialize infrastructure containers for NetCorePal Template development"
Write-Host ""
Write-Host "Options:"
Write-Host " -Help Show this help message"
Write-Host " -SqlServer Use SQL Server database instead of MySQL"
Write-Host " -Postgres Use PostgreSQL database instead of MySQL"
Write-Host " -Kafka Use Kafka instead of RabbitMQ"
Write-Host ""
Write-Host "Examples:"
Write-Host " .\init-infrastructure.ps1 # Start with MySQL and RabbitMQ (default)"
Write-Host " .\init-infrastructure.ps1 -Postgres # Start with PostgreSQL and RabbitMQ"
Write-Host " .\init-infrastructure.ps1 -Kafka # Start with MySQL and Kafka"
Write-Host ""
}
function Test-Docker {
Write-Info "Checking Docker installation..."
try {
$null = Get-Command docker -ErrorAction Stop
}
catch {
Write-Error "Docker is not installed. Please install Docker Desktop first."
Write-Host "Download from: https://www.docker.com/products/docker-desktop/" -ForegroundColor Cyan
exit 1
}
try {
$null = docker info 2>$null
}
catch {
Write-Error "Docker is not running. Please start Docker Desktop first."
exit 1
}
Write-Success "Docker is installed and running"
}
function Start-Container {
param(
[string]$Name,
[string]$Image,
[string]$Ports,
[string]$Environment,
[string]$Volumes,
[string]$AdditionalArgs
)
Write-Info "Starting $Name container..."
# Stop and remove existing container if it exists
$existingContainer = docker ps -a --format "table {{.Names}}" | Select-String "^$Name$"
if ($existingContainer) {
Write-Warning "Stopping existing $Name container..."
docker stop $Name 2>$null | Out-Null
docker rm $Name 2>$null | Out-Null
}
# Build the docker run command
$cmd = "docker run --restart unless-stopped --name $Name"
if ($Ports) { $cmd += " $Ports" }
if ($Environment) { $cmd += " $Environment" }
if ($Volumes) { $cmd += " $Volumes" }
if ($AdditionalArgs) { $cmd += " $AdditionalArgs" }
$cmd += " -d $Image"
try {
Invoke-Expression $cmd | Out-Null
Write-Success "$Name container started successfully"
return $true
}
catch {
Write-Error "Failed to start $Name container: $_"
return $false
}
}
function Wait-ForContainer {
param(
[string]$ContainerName,
[int]$MaxAttempts = 30
)
Write-Info "Waiting for $ContainerName to be healthy..."
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
$running = docker ps --filter "name=$ContainerName" --filter "status=running" | Select-String $ContainerName
if ($running) {
Write-Success "$ContainerName is running"
return $true
}
Write-Host "." -NoNewline
Start-Sleep -Seconds 2
}
Write-Host "" # New line after dots
Write-Error "$ContainerName failed to start properly"
return $false
}
function Start-Infrastructure {
Write-Host ""
Write-Info "Starting infrastructure setup..."
Write-Host ""
# Check prerequisites
Test-Docker
# Start Redis
$success = Start-Container -Name "netcorepal-redis" -Image "redis:7.2-alpine" `
-Ports "-p 6379:6379" `
-Volumes "-v netcorepal_redis_data:/data" `
-AdditionalArgs "redis-server --appendonly yes --databases 1024"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-redis" -MaxAttempts 15
}
# Start Database
if ($Postgres) {
Write-Info "Setting up PostgreSQL database..."
$success = Start-Container -Name "netcorepal-postgres" -Image "postgres:15-alpine" `
-Ports "-p 5432:5432" `
-Environment "-e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=123456 -e POSTGRES_DB=postgres -e TZ=Asia/Shanghai" `
-Volumes "-v netcorepal_postgres_data:/var/lib/postgresql/data"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-postgres" -MaxAttempts 30
}
}
elseif ($SqlServer) {
Write-Info "Setting up SQL Server database..."
$success = Start-Container -Name "netcorepal-sqlserver" -Image "mcr.microsoft.com/mssql/server:2022-latest" `
-Ports "-p 1433:1433" `
-Environment "-e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=Test123456! -e TZ=Asia/Shanghai" `
-Volumes "-v netcorepal_sqlserver_data:/var/opt/mssql"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-sqlserver" -MaxAttempts 30
}
}
else {
Write-Info "Setting up MySQL database..."
$success = Start-Container -Name "netcorepal-mysql" -Image "mysql:8.0" `
-Ports "-p 3306:3306" `
-Environment "-e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_CHARACTER_SET_SERVER=utf8mb4 -e MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci -e TZ=Asia/Shanghai" `
-Volumes "-v netcorepal_mysql_data:/var/lib/mysql"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-mysql" -MaxAttempts 30
}
}
# Start Message Queue
if ($Kafka) {
Write-Info "Setting up Kafka message queue..."
Write-Warning "Kafka setup requires Zookeeper. For full Kafka setup, please use Docker Compose:"
Write-Host "docker-compose --profile kafka up -d" -ForegroundColor Cyan
}
else {
Write-Info "Setting up RabbitMQ message queue..."
$success = Start-Container -Name "netcorepal-rabbitmq" -Image "rabbitmq:3.12-management-alpine" `
-Ports "-p 5672:5672 -p 15672:15672" `
-Environment "-e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest" `
-Volumes "-v netcorepal_rabbitmq_data:/var/lib/rabbitmq"
if ($success) {
Wait-ForContainer -ContainerName "netcorepal-rabbitmq" -MaxAttempts 20
}
}
Write-Host ""
Write-Success "🎉 Infrastructure setup completed successfully!"
Write-Host ""
Write-Host "📋 Service Summary:" -ForegroundColor Cyan
Write-Host "==================="
Write-Host "✅ Redis: localhost:6379"
if ($Postgres) {
Write-Host "✅ PostgreSQL: localhost:5432 (postgres/123456)"
}
elseif ($SqlServer) {
Write-Host "✅ SQL Server: localhost:1433 (sa/Test123456!)"
}
else {
Write-Host "✅ MySQL: localhost:3306 (root/123456)"
}
if (-not $Kafka) {
Write-Host "✅ RabbitMQ: localhost:5672 (guest/guest)"
Write-Host "📊 RabbitMQ Management UI: http://localhost:15672"
}
Write-Host ""
Write-Host "💡 Tips:" -ForegroundColor Yellow
Write-Host "• Use 'docker ps' to see running containers"
Write-Host "• Use 'docker logs <container_name>' to check logs"
Write-Host "• Use '.\clean-infrastructure.ps1' to stop and remove all containers"
Write-Host ""
Write-Info "Ready for development! 🚀"
}
# Main execution
Write-Host "🚀 NetCorePal Template - Infrastructure Setup" -ForegroundColor Green
Write-Host "==============================================" -ForegroundColor Green
if ($Help) {
Show-Help
exit 0
}
try {
Start-Infrastructure
}
catch {
Write-Error "An error occurred during setup: $_"
exit 1
}

View File

@ -0,0 +1,200 @@
#!/bin/bash
# NetCorePal Template - Infrastructure Initialization Script
# This script initializes the required infrastructure for development
set -e
echo "🚀 NetCorePal Template - Infrastructure Setup"
echo "=============================================="
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if Docker is installed and running
check_docker() {
print_status "Checking Docker installation..."
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed. Please install Docker first."
echo "Download from: https://www.docker.com/products/docker-desktop/"
exit 1
fi
if ! docker info &> /dev/null; then
print_error "Docker is not running. Please start Docker first."
exit 1
fi
print_success "Docker is installed and running"
}
# Function to run a Docker container with retry logic
run_container() {
local name=$1
local image=$2
local ports=$3
local environment=$4
local volumes=$5
local additional_args=$6
print_status "Starting $name container..."
# Stop and remove existing container if it exists
if docker ps -a --format 'table {{.Names}}' | grep -q "^$name$"; then
print_warning "Stopping existing $name container..."
docker stop $name > /dev/null 2>&1 || true
docker rm $name > /dev/null 2>&1 || true
fi
# Run the container
local cmd="docker run --restart unless-stopped --name $name $ports $environment $volumes $additional_args -d $image"
if eval $cmd > /dev/null; then
print_success "$name container started successfully"
return 0
else
print_error "Failed to start $name container"
return 1
fi
}
# Function to wait for container to be healthy
wait_for_container() {
local container_name=$1
local max_attempts=${2:-30}
local attempt=1
print_status "Waiting for $container_name to be healthy..."
while [ $attempt -le $max_attempts ]; do
if docker ps --filter "name=$container_name" --filter "status=running" | grep -q $container_name; then
print_success "$container_name is running"
return 0
fi
echo -n "."
sleep 2
((attempt++))
done
print_error "$container_name failed to start properly"
return 1
}
# Main execution
main() {
echo
print_status "Starting infrastructure setup..."
echo
# Check prerequisites
check_docker
# Start Redis
run_container "netcorepal-redis" "redis:7.2-alpine" \
"-p 6379:6379" \
"" \
"-v netcorepal_redis_data:/data" \
"redis-server --appendonly yes --databases 1024"
wait_for_container "netcorepal-redis" 15
# Start MySQL (default database)
print_status "Setting up MySQL database..."
run_container "netcorepal-mysql" "mysql:8.0" \
"-p 3306:3306" \
"-e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_CHARACTER_SET_SERVER=utf8mb4 -e MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci -e TZ=Asia/Shanghai" \
"-v netcorepal_mysql_data:/var/lib/mysql" \
""
wait_for_container "netcorepal-mysql" 30
# Start RabbitMQ (default message queue)
print_status "Setting up RabbitMQ message queue..."
run_container "netcorepal-rabbitmq" "rabbitmq:3.12-management-alpine" \
"-p 5672:5672 -p 15672:15672" \
"-e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest" \
"-v netcorepal_rabbitmq_data:/var/lib/rabbitmq" \
""
wait_for_container "netcorepal-rabbitmq" 20
echo
print_success "🎉 Infrastructure setup completed successfully!"
echo
echo "📋 Service Summary:"
echo "==================="
echo "✅ Redis: localhost:6379"
echo "✅ MySQL: localhost:3306 (root/123456)"
echo "✅ RabbitMQ: localhost:5672 (guest/guest)"
echo "📊 RabbitMQ Management UI: http://localhost:15672"
echo
echo "💡 Tips:"
echo "• Use 'docker ps' to see running containers"
echo "• Use 'docker logs <container_name>' to check logs"
echo "• Use './clean-infrastructure.sh' to stop and remove all containers"
echo
print_status "Ready for development! 🚀"
}
# Parse command line arguments
case "${1:-}" in
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo
echo "Initialize infrastructure containers for NetCorePal Template development"
echo
echo "Options:"
echo " -h, --help Show this help message"
echo " --mysql Use MySQL database (default)"
echo " --sqlserver Use SQL Server database"
echo " --postgres Use PostgreSQL database"
echo " --kafka Use Kafka instead of RabbitMQ"
echo
echo "Examples:"
echo " $0 # Start with MySQL and RabbitMQ (default)"
echo " $0 --postgres # Start with PostgreSQL and RabbitMQ"
echo " $0 --kafka # Start with MySQL and Kafka"
exit 0
;;
--sqlserver)
print_status "SQL Server option will be implemented in Docker Compose version"
print_status "For now, use: docker-compose --profile sqlserver up -d"
exit 0
;;
--postgres)
print_status "PostgreSQL option will be implemented in Docker Compose version"
print_status "For now, use: docker-compose --profile postgres up -d"
exit 0
;;
--kafka)
print_status "Kafka option will be implemented in Docker Compose version"
print_status "For now, use: docker-compose --profile kafka up -d"
exit 0
;;
*)
main
;;
esac

View File

@ -0,0 +1,18 @@
-- MySQL Initialization Script for NetCorePal Template
-- This script creates the necessary database and user for development
-- Create development database if it doesn't exist
CREATE DATABASE IF NOT EXISTS `abctemplate` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Create a development user (optional - you can use root for development)
-- CREATE USER IF NOT EXISTS 'devuser'@'%' IDENTIFIED BY 'devpass123';
-- GRANT ALL PRIVILEGES ON `abctemplate`.* TO 'devuser'@'%';
-- Ensure root can connect from any host (for development only)
-- ALTER USER 'root'@'%' IDENTIFIED BY '123456';
-- GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
-- Display completion message
SELECT 'MySQL initialization completed successfully' AS message;

View File

@ -0,0 +1,24 @@
-- PostgreSQL Initialization Script for NetCorePal Template
-- This script creates the necessary database and user for development
-- Create development database if it doesn't exist
SELECT 'CREATE DATABASE abctemplate'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'abctemplate')\gexec
-- Create a development user (optional - you can use postgres for development)
-- DO
-- $do$
-- BEGIN
-- IF NOT EXISTS (
-- SELECT FROM pg_catalog.pg_roles
-- WHERE rolname = 'devuser') THEN
-- CREATE ROLE devuser LOGIN PASSWORD 'devpass123';
-- END IF;
-- END
-- $do$;
-- Grant privileges to development user
-- GRANT ALL PRIVILEGES ON DATABASE abctemplate TO devuser;
-- Display completion message
SELECT 'PostgreSQL initialization completed successfully' AS message;

View File

@ -0,0 +1,68 @@
using MediatR;
using FluentValidation;
using Fengling.Member.Domain.Aggregates.Users;
using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member;
public class AddMemberTagCommand : IRequest<AddMemberTagResponse>
{
public long MemberId { get; set; }
public string TagId { get; set; } = string.Empty;
public string? TagName { get; set; }
}
public class AddMemberTagResponse
{
public long MemberId { get; set; }
public string TagId { get; set; } = string.Empty;
public string? TagName { get; set; }
public DateTime AddedAt { get; set; }
}
public class AddMemberTagCommandValidator : AbstractValidator<AddMemberTagCommand>
{
public AddMemberTagCommandValidator()
{
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.TagId).NotEmpty().MaximumLength(50);
RuleFor(x => x.TagName).MaximumLength(100);
}
}
public class AddMemberTagCommandHandler : IRequestHandler<AddMemberTagCommand, AddMemberTagResponse>
{
private readonly IMemberRepository _memberRepository;
private readonly ILogger<AddMemberTagCommandHandler> _logger;
public AddMemberTagCommandHandler(
IMemberRepository memberRepository,
ILogger<AddMemberTagCommandHandler> logger)
{
_memberRepository = memberRepository;
_logger = logger;
}
public async Task<AddMemberTagResponse> Handle(AddMemberTagCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Adding tag {TagId} to member {MemberId}", request.TagId, request.MemberId);
var member = await _memberRepository.GetAsync(request.MemberId, cancellationToken);
if (member == null)
{
throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
}
member.AddTag(request.TagId, request.TagName);
_logger.LogInformation("Tag {TagId} added to member {MemberId}", request.TagId, request.MemberId);
return new AddMemberTagResponse
{
MemberId = member.Id,
TagId = request.TagId,
TagName = request.TagName,
AddedAt = DateTime.UtcNow
};
}
}

View File

@ -0,0 +1,68 @@
using MediatR;
using FluentValidation;
using Fengling.Member.Domain.Aggregates.Users;
using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member;
public class BindAlipayCommand : IRequest<BindAlipayResponse>
{
public long MemberId { get; set; }
public string AlipayOpenId { get; set; } = string.Empty;
public string? AlipayUserId { get; set; }
}
public class BindAlipayResponse
{
public long MemberId { get; set; }
public string AlipayOpenId { get; set; } = string.Empty;
public string? AlipayUserId { get; set; }
public DateTime BoundAt { get; set; }
}
public class BindAlipayCommandValidator : AbstractValidator<BindAlipayCommand>
{
public BindAlipayCommandValidator()
{
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.AlipayOpenId).NotEmpty().MaximumLength(128);
RuleFor(x => x.AlipayUserId).MaximumLength(128);
}
}
public class BindAlipayCommandHandler : IRequestHandler<BindAlipayCommand, BindAlipayResponse>
{
private readonly IMemberRepository _memberRepository;
private readonly ILogger<BindAlipayCommandHandler> _logger;
public BindAlipayCommandHandler(
IMemberRepository memberRepository,
ILogger<BindAlipayCommandHandler> logger)
{
_memberRepository = memberRepository;
_logger = logger;
}
public async Task<BindAlipayResponse> Handle(BindAlipayCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Binding Alipay for member {MemberId}", request.MemberId);
var member = await _memberRepository.GetAsync(request.MemberId, cancellationToken);
if (member == null)
{
throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
}
member.BindOAuth(OAuthProvider.Alipay, request.AlipayOpenId, request.AlipayUserId);
_logger.LogInformation("Alipay bound successfully for member {MemberId}", request.MemberId);
return new BindAlipayResponse
{
MemberId = member.Id,
AlipayOpenId = request.AlipayOpenId,
AlipayUserId = request.AlipayUserId,
BoundAt = DateTime.UtcNow
};
}
}

View File

@ -0,0 +1,74 @@
using MediatR;
using FluentValidation;
using Fengling.Member.Domain.Aggregates.Users;
using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member;
public class BindOAuthCommand : IRequest<BindOAuthResponse>
{
public OAuthProvider Provider { get; set; }
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
}
public class BindOAuthResponse
{
public long MemberId { get; set; }
public OAuthProvider Provider { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
public DateTime BoundAt { get; set; }
}
public class BindOAuthCommandValidator : AbstractValidator<BindOAuthCommand>
{
public BindOAuthCommandValidator()
{
RuleFor(x => x.Provider).IsInEnum();
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.OpenId).NotEmpty().MaximumLength(128);
RuleFor(x => x.UnionId).MaximumLength(128);
}
}
public class BindOAuthCommandHandler : IRequestHandler<BindOAuthCommand, BindOAuthResponse>
{
private readonly IMemberRepository _memberRepository;
private readonly ILogger<BindOAuthCommandHandler> _logger;
public BindOAuthCommandHandler(
IMemberRepository memberRepository,
ILogger<BindOAuthCommandHandler> logger)
{
_memberRepository = memberRepository;
_logger = logger;
}
public async Task<BindOAuthResponse> Handle(BindOAuthCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Binding {Provider} for member {MemberId}",
request.Provider.GetProviderName(), request.MemberId);
var member = await _memberRepository.GetAsync(request.MemberId, cancellationToken);
if (member == null)
{
throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
}
member.BindOAuth(request.Provider, request.OpenId, request.UnionId);
_logger.LogInformation("{Provider} bound successfully for member {MemberId}",
request.Provider.GetProviderName(), request.MemberId);
return new BindOAuthResponse
{
MemberId = member.Id,
Provider = request.Provider,
OpenId = request.OpenId,
UnionId = request.UnionId,
BoundAt = DateTime.UtcNow
};
}
}

View File

@ -0,0 +1,68 @@
using MediatR;
using FluentValidation;
using Fengling.Member.Domain.Aggregates.Users;
using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member;
public class BindWechatCommand : IRequest<BindWechatResponse>
{
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
}
public class BindWechatResponse
{
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
public DateTime BoundAt { get; set; }
}
public class BindWechatCommandValidator : AbstractValidator<BindWechatCommand>
{
public BindWechatCommandValidator()
{
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.OpenId).NotEmpty().MaximumLength(64);
RuleFor(x => x.UnionId).MaximumLength(64);
}
}
public class BindWechatCommandHandler : IRequestHandler<BindWechatCommand, BindWechatResponse>
{
private readonly IMemberRepository _memberRepository;
private readonly ILogger<BindWechatCommandHandler> _logger;
public BindWechatCommandHandler(
IMemberRepository memberRepository,
ILogger<BindWechatCommandHandler> logger)
{
_memberRepository = memberRepository;
_logger = logger;
}
public async Task<BindWechatResponse> Handle(BindWechatCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Binding wechat for member {MemberId}", request.MemberId);
var member = await _memberRepository.GetAsync(request.MemberId, cancellationToken);
if (member == null)
{
throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
}
member.BindWechat(request.OpenId, request.UnionId);
_logger.LogInformation("Wechat bound successfully for member {MemberId}", request.MemberId);
return new BindWechatResponse
{
MemberId = member.Id,
OpenId = request.OpenId,
UnionId = request.UnionId,
BoundAt = DateTime.UtcNow
};
}
}

View File

@ -0,0 +1,109 @@
using MediatR;
using FluentValidation;
using Fengling.Member.Domain.Aggregates.Users;
using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member;
public class RegisterMemberCommand : IRequest<RegisterMemberResponse>
{
public long TenantId { get; set; }
public string? PhoneNumber { get; set; }
public string? OpenId { get; set; }
public string? UnionId { get; set; }
public string? Source { get; set; }
}
public class RegisterMemberResponse
{
public long MemberId { get; set; }
public long TenantId { get; set; }
public string? PhoneNumber { get; set; }
public string? OpenId { get; set; }
public MemberStatus Status { get; set; } = MemberStatus.Active;
public DateTime RegisteredAt { get; set; }
}
public class RegisterMemberCommandHandler : IRequestHandler<RegisterMemberCommand, RegisterMemberResponse>
{
private readonly IMemberRepository _memberRepository;
private readonly ILogger<RegisterMemberCommandHandler> _logger;
public RegisterMemberCommandHandler(
IMemberRepository memberRepository,
ILogger<RegisterMemberCommandHandler> logger)
{
_memberRepository = memberRepository;
_logger = logger;
}
public async Task<RegisterMemberResponse> Handle(RegisterMemberCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Registering new member for tenant {TenantId}", request.TenantId);
if (!string.IsNullOrEmpty(request.PhoneNumber))
{
var existingMember = await _memberRepository.GetByPhoneNumberAsync(request.TenantId, request.PhoneNumber, cancellationToken);
if (existingMember != null)
{
_logger.LogWarning("Member with phone {PhoneNumber} already exists in tenant {TenantId}",
request.PhoneNumber, request.TenantId);
if (!string.IsNullOrEmpty(request.OpenId) && string.IsNullOrEmpty(existingMember.OpenId))
{
existingMember.BindWechat(request.OpenId, request.UnionId);
}
return new RegisterMemberResponse
{
MemberId = existingMember.Id,
TenantId = existingMember.TenantId,
PhoneNumber = existingMember.PhoneNumber,
OpenId = existingMember.OpenId,
Status = existingMember.Status,
RegisteredAt = existingMember.CreatedAt
};
}
}
if (!string.IsNullOrEmpty(request.OpenId))
{
var existingByOpenId = await _memberRepository.GetByOpenIdAsync(request.OpenId, cancellationToken);
if (existingByOpenId != null)
{
_logger.LogWarning("Member with OpenId {OpenId} already exists", request.OpenId);
return new RegisterMemberResponse
{
MemberId = existingByOpenId.Id,
TenantId = existingByOpenId.TenantId,
PhoneNumber = existingByOpenId.PhoneNumber,
OpenId = existingByOpenId.OpenId,
Status = existingByOpenId.Status,
RegisteredAt = existingByOpenId.CreatedAt
};
}
}
var member = MemberEntity.Create(request.TenantId, request.PhoneNumber);
if (!string.IsNullOrEmpty(request.OpenId))
{
member.BindWechat(request.OpenId, request.UnionId);
}
await _memberRepository.AddAsync(member, cancellationToken);
_logger.LogInformation("Member registered successfully with ID {MemberId}", member.Id);
return new RegisterMemberResponse
{
MemberId = member.Id,
TenantId = member.TenantId,
PhoneNumber = member.PhoneNumber,
OpenId = member.OpenId,
Status = member.Status,
RegisteredAt = member.CreatedAt
};
}
}

View File

@ -0,0 +1,76 @@
using MediatR;
using FluentValidation;
using Fengling.Member.Domain.Aggregates.PointsModel;
using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Points;
public class AddPointsCommand : IRequest<AddPointsResponse>
{
public long MemberId { get; set; }
public int Points { get; set; }
public string TransactionType { get; set; } = string.Empty;
public string SourceId { get; set; } = string.Empty;
public string? Remark { get; set; }
}
public class AddPointsResponse
{
public long AccountId { get; set; }
public long MemberId { get; set; }
public int AddedPoints { get; set; }
public int TotalPoints { get; set; }
public DateTime TransactionAt { get; set; }
}
public class AddPointsCommandValidator : AbstractValidator<AddPointsCommand>
{
public AddPointsCommandValidator()
{
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.Points).GreaterThan(0);
RuleFor(x => x.TransactionType).NotEmpty().MaximumLength(50);
RuleFor(x => x.SourceId).MaximumLength(100);
RuleFor(x => x.Remark).MaximumLength(500);
}
}
public class AddPointsCommandHandler : IRequestHandler<AddPointsCommand, AddPointsResponse>
{
private readonly IPointsAccountRepository _pointsAccountRepository;
private readonly ILogger<AddPointsCommandHandler> _logger;
public AddPointsCommandHandler(
IPointsAccountRepository pointsAccountRepository,
ILogger<AddPointsCommandHandler> logger)
{
_pointsAccountRepository = pointsAccountRepository;
_logger = logger;
}
public async Task<AddPointsResponse> Handle(AddPointsCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Adding {Points} points to member {MemberId}", request.Points, request.MemberId);
var account = await _pointsAccountRepository.GetByMemberIdAsync(request.MemberId, cancellationToken);
if (account == null)
{
account = PointsAccount.Create(request.MemberId, 0);
await _pointsAccountRepository.AddAsync(account, cancellationToken);
}
account.AddPoints(request.Points, request.TransactionType, request.SourceId, request.Remark);
_logger.LogInformation("{Points} points added to member {MemberId}. New balance: {TotalPoints}",
request.Points, request.MemberId, account.TotalPoints);
return new AddPointsResponse
{
AccountId = account.Id,
MemberId = account.MemberId,
AddedPoints = request.Points,
TotalPoints = account.TotalPoints,
TransactionAt = DateTime.UtcNow
};
}
}

View File

@ -0,0 +1,101 @@
using MediatR;
using FluentValidation;
using Fengling.Member.Domain.Aggregates.PointsModel;
using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Points;
public class DeductPointsCommand : IRequest<DeductPointsResponse>
{
public long MemberId { get; set; }
public int Points { get; set; }
public string TransactionType { get; set; } = string.Empty;
public string SourceId { get; set; } = string.Empty;
public string? Remark { get; set; }
}
public class DeductPointsResponse
{
public long AccountId { get; set; }
public long MemberId { get; set; }
public int DeductedPoints { get; set; }
public int TotalPoints { get; set; }
public bool Success { get; set; }
public DateTime TransactionAt { get; set; }
}
public class DeductPointsCommandValidator : AbstractValidator<DeductPointsCommand>
{
public DeductPointsCommandValidator()
{
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.Points).GreaterThan(0);
RuleFor(x => x.TransactionType).NotEmpty().MaximumLength(50);
RuleFor(x => x.SourceId).MaximumLength(100);
RuleFor(x => x.Remark).MaximumLength(500);
}
}
public class DeductPointsCommandHandler : IRequestHandler<DeductPointsCommand, DeductPointsResponse>
{
private readonly IPointsAccountRepository _pointsAccountRepository;
private readonly ILogger<DeductPointsCommandHandler> _logger;
public DeductPointsCommandHandler(
IPointsAccountRepository pointsAccountRepository,
ILogger<DeductPointsCommandHandler> logger)
{
_pointsAccountRepository = pointsAccountRepository;
_logger = logger;
}
public async Task<DeductPointsResponse> Handle(DeductPointsCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Deducting {Points} points from member {MemberId}", request.Points, request.MemberId);
var account = await _pointsAccountRepository.GetByMemberIdAsync(request.MemberId, cancellationToken);
if (account == null)
{
_logger.LogWarning("Member {MemberId} has no points account", request.MemberId);
return new DeductPointsResponse
{
MemberId = request.MemberId,
DeductedPoints = 0,
TotalPoints = 0,
Success = false,
TransactionAt = DateTime.UtcNow
};
}
var success = account.DeductPoints(request.Points, request.TransactionType, request.SourceId, request.Remark);
if (!success)
{
_logger.LogWarning("Insufficient points for member {MemberId}. Available: {Available}, Requested: {Requested}",
request.MemberId, account.AvailablePoints, request.Points);
return new DeductPointsResponse
{
AccountId = account.Id,
MemberId = account.MemberId,
DeductedPoints = 0,
TotalPoints = account.TotalPoints,
Success = false,
TransactionAt = DateTime.UtcNow
};
}
_logger.LogInformation("{Points} points deducted from member {MemberId}. New balance: {TotalPoints}",
request.Points, request.MemberId, account.TotalPoints);
return new DeductPointsResponse
{
AccountId = account.Id,
MemberId = account.MemberId,
DeductedPoints = request.Points,
TotalPoints = account.TotalPoints,
Success = true,
TransactionAt = DateTime.UtcNow
};
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" />
<PackageReference Include="MediatR" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fengling.Member.Domain\Fengling.Member.Domain.csproj" />
<ProjectReference Include="..\Fengling.Member.Infrastructure\Fengling.Member.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
global using NetCorePal.Extensions.Domain;
global using MediatR;
global using FluentValidation;
global using Microsoft.Extensions.Logging;

View File

@ -0,0 +1,135 @@
using Fengling.Member.Domain.Events.Points;
namespace Fengling.Member.Domain.Aggregates.PointsModel;
public class PointsAccount : Entity<long>, IAggregateRoot
{
public long MemberId { get; private set; }
public long TenantId { get; private set; }
public int TotalPoints { get; private set; } = 0;
public int FrozenPoints { get; private set; } = 0;
public int AvailablePoints => TotalPoints - FrozenPoints;
public int Version { get; private set; } = 1;
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; private set; }
private readonly List<PointsTransaction> _transactions = new();
public IReadOnlyCollection<PointsTransaction> Transactions => _transactions.AsReadOnly();
private PointsAccount()
{
}
public static PointsAccount Create(long memberId, long tenantId)
{
if (memberId <= 0)
throw new ArgumentException("会员ID必须大于0", nameof(memberId));
if (tenantId <= 0)
throw new ArgumentException("租户ID必须大于0", nameof(tenantId));
var account = new PointsAccount
{
MemberId = memberId,
TenantId = tenantId,
TotalPoints = 0,
FrozenPoints = 0,
CreatedAt = DateTime.UtcNow
};
return account;
}
public void AddPoints(int points, string transactionType, string sourceId, string? remark = null)
{
if (points <= 0)
throw new ArgumentException("积分必须大于0", nameof(points));
var transaction = PointsTransaction.Create(
Id,
MemberId,
points,
transactionType,
sourceId,
PointsTransactionType.Earn,
remark
);
_transactions.Add(transaction);
TotalPoints += points;
UpdatedAt = DateTime.UtcNow;
Version++;
AddDomainEvent(new PointsChangedEvent(Id, MemberId, points, TotalPoints, transactionType));
}
public bool DeductPoints(int points, string transactionType, string sourceId, string? remark = null)
{
if (points <= 0)
throw new ArgumentException("积分必须大于0", nameof(points));
if (AvailablePoints < points)
return false;
var transaction = PointsTransaction.Create(
Id,
MemberId,
points,
transactionType,
sourceId,
PointsTransactionType.Deduct,
remark
);
_transactions.Add(transaction);
TotalPoints -= points;
UpdatedAt = DateTime.UtcNow;
Version++;
AddDomainEvent(new PointsChangedEvent(Id, MemberId, -points, TotalPoints, transactionType));
return true;
}
public bool FreezePoints(int points)
{
if (points <= 0)
throw new ArgumentException("冻结积分必须大于0", nameof(points));
if (AvailablePoints < points)
return false;
FrozenPoints += points;
UpdatedAt = DateTime.UtcNow;
Version++;
return true;
}
public bool UnfreezePoints(int points)
{
if (points <= 0)
throw new ArgumentException("解冻积分必须大于0", nameof(points));
if (FrozenPoints < points)
return false;
FrozenPoints -= points;
UpdatedAt = DateTime.UtcNow;
Version++;
return true;
}
public bool UseFrozenPoints(int points)
{
if (points <= 0)
throw new ArgumentException("使用积分必须大于0", nameof(points));
if (FrozenPoints < points)
return false;
FrozenPoints -= points;
TotalPoints -= points;
UpdatedAt = DateTime.UtcNow;
Version++;
return true;
}
}

View File

@ -0,0 +1,45 @@
namespace Fengling.Member.Domain.Aggregates.PointsModel;
public class PointsTransaction : Entity<long>
{
public long PointsAccountId { get; private set; }
public long MemberId { get; private set; }
public int Points { get; private set; }
public string TransactionType { get; private set; } = string.Empty;
public string SourceId { get; private set; } = string.Empty;
public PointsTransactionType TransactionTypeCategory { get; private set; }
public string? Remark { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
private PointsTransaction()
{
}
public static PointsTransaction Create(
long pointsAccountId,
long memberId,
int points,
string transactionType,
string sourceId,
PointsTransactionType typeCategory,
string? remark = null)
{
return new PointsTransaction
{
PointsAccountId = pointsAccountId,
MemberId = memberId,
Points = points,
TransactionType = transactionType,
SourceId = sourceId,
TransactionTypeCategory = typeCategory,
Remark = remark,
CreatedAt = DateTime.UtcNow
};
}
}
public enum PointsTransactionType
{
Earn = 1,
Deduct = 2
}

View File

@ -0,0 +1,145 @@
using System.Text.RegularExpressions;
using Fengling.Member.Domain.Events.Member;
namespace Fengling.Member.Domain.Aggregates.Users;
public class MemberEntity : Entity<long>, IAggregateRoot
{
public long TenantId { get; private set; }
public string? PhoneNumber { get; private set; }
public string? OpenId { get; private set; }
public string? UnionId { get; private set; }
public MemberStatus Status { get; private set; } = MemberStatus.Active;
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; private set; }
public int Version { get; private set; } = 1;
private readonly List<MemberTag> _tags = new();
public IReadOnlyCollection<MemberTag> Tags => _tags.AsReadOnly();
private readonly List<OAuthAuthorization> _oauthAuthorizations = new();
public IReadOnlyCollection<OAuthAuthorization> OAuthAuthorizations => _oauthAuthorizations.AsReadOnly();
private MemberEntity()
{
}
public static MemberEntity Create(long tenantId, string? phoneNumber = null)
{
if (tenantId <= 0)
throw new ArgumentException("租户ID必须大于0", nameof(tenantId));
var member = new MemberEntity
{
TenantId = tenantId,
PhoneNumber = phoneNumber,
Status = MemberStatus.Active,
CreatedAt = DateTime.UtcNow
};
member.AddDomainEvent(new MemberRegisteredEvent(member.Id, member.TenantId, phoneNumber, member.CreatedAt));
return member;
}
public void BindPhoneNumber(string phoneNumber)
{
if (string.IsNullOrWhiteSpace(phoneNumber))
throw new ArgumentException("手机号不能为空", nameof(phoneNumber));
if (!Regex.IsMatch(phoneNumber, @"^1[3-9]\d{9}$"))
throw new ArgumentException("手机号格式不正确", nameof(phoneNumber));
PhoneNumber = phoneNumber;
UpdatedAt = DateTime.UtcNow;
}
public void BindOAuth(OAuthProvider provider, string openId, string? unionId = null)
{
if (string.IsNullOrWhiteSpace(openId))
throw new ArgumentException("OpenId不能为空", nameof(openId));
UpdatePrimaryOAuthId(provider, openId, unionId);
UpdatedAt = DateTime.UtcNow;
var authorization = _oauthAuthorizations.FirstOrDefault(x => x.Provider == provider && x.OpenId == openId);
if (authorization == null)
{
authorization = OAuthAuthorization.Create(Id, provider, openId, unionId);
_oauthAuthorizations.Add(authorization);
}
else
{
authorization.UpdateUnionId(unionId);
}
}
public void BindWechat(string openId, string? unionId = null)
{
BindOAuth(OAuthProvider.Wechat, openId, unionId);
}
private void UpdatePrimaryOAuthId(OAuthProvider provider, string openId, string? unionId)
{
if (provider == OAuthProvider.Wechat)
{
OpenId = openId;
UnionId = unionId ?? UnionId;
}
}
public OAuthAuthorization? GetOAuthAuthorization(OAuthProvider provider)
{
return _oauthAuthorizations.FirstOrDefault(x => x.Provider == provider);
}
public bool HasOAuthBound(OAuthProvider provider, string openId)
{
return _oauthAuthorizations.Any(x => x.Provider == provider && x.OpenId == openId);
}
public void RecordOAuthLogin(OAuthProvider provider, string openId)
{
var authorization = _oauthAuthorizations.FirstOrDefault(x => x.Provider == provider && x.OpenId == openId);
authorization?.RecordLogin();
}
public void AddTag(string tagId, string? tagName = null)
{
var existingTag = _tags.FirstOrDefault(t => t.TagId == tagId);
if (existingTag != null)
return;
_tags.Add(MemberTag.Create(Id, tagId, tagName));
}
public void RemoveTag(string tagId)
{
var tag = _tags.FirstOrDefault(t => t.TagId == tagId);
if (tag != null)
_tags.Remove(tag);
}
public void Freeze()
{
if (Status == MemberStatus.Frozen)
return;
Status = MemberStatus.Frozen;
UpdatedAt = DateTime.UtcNow;
}
public void Unfreeze()
{
Status = MemberStatus.Active;
UpdatedAt = DateTime.UtcNow;
}
public void Deactivate()
{
if (Status == MemberStatus.Inactive)
return;
Status = MemberStatus.Inactive;
UpdatedAt = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,37 @@
namespace Fengling.Member.Domain.Aggregates.Users;
public record MemberStatus
{
public static MemberStatus Active { get; } = new("Active", "正常");
public static MemberStatus Inactive { get; } = new("Inactive", "已停用");
public static MemberStatus Frozen { get; } = new("Frozen", "已冻结");
public string Value { get; }
public string Description { get; }
private MemberStatus(string value, string description)
{
Value = value;
Description = description;
}
public static MemberStatus FromValue(string value)
{
return value switch
{
"Active" => Active,
"Inactive" => Inactive,
"Frozen" => Frozen,
_ => throw new ArgumentException($"未知的会员状态: {value}", nameof(value))
};
}
public static IEnumerable<MemberStatus> GetAll()
{
yield return Active;
yield return Inactive;
yield return Frozen;
}
public override string ToString() => Value;
}

View File

@ -0,0 +1,28 @@
namespace Fengling.Member.Domain.Aggregates.Users;
public class MemberTag : Entity<long>
{
public long MemberId { get; private set; }
public string TagId { get; private set; } = string.Empty;
public string? TagName { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
private MemberTag()
{
}
public static MemberTag Create(long memberId, string tagId, string? tagName = null)
{
return new MemberTag
{
MemberId = memberId,
TagId = tagId,
TagName = tagName
};
}
public void UpdateTagName(string? tagName)
{
TagName = tagName;
}
}

View File

@ -0,0 +1,50 @@
namespace Fengling.Member.Domain.Aggregates.Users;
public class OAuthAuthorization : Entity<long>
{
public long MemberId { get; private set; }
public OAuthProvider Provider { get; private set; }
public string OpenId { get; private set; } = string.Empty;
public string? UnionId { get; private set; }
public string? AccessToken { get; private set; }
public string? RefreshToken { get; private set; }
public DateTime? TokenExpiredAt { get; private set; }
public DateTime AuthorizedAt { get; private set; } = DateTime.UtcNow;
public DateTime? LastLoginAt { get; private set; }
private OAuthAuthorization()
{
}
public static OAuthAuthorization Create(long memberId, OAuthProvider provider, string openId, string? unionId = null)
{
return new OAuthAuthorization
{
MemberId = memberId,
Provider = provider,
OpenId = openId,
UnionId = unionId,
AuthorizedAt = DateTime.UtcNow
};
}
public void UpdateUnionId(string? unionId)
{
if (unionId != null && UnionId != unionId)
{
UnionId = unionId;
}
}
public void RecordLogin()
{
LastLoginAt = DateTime.UtcNow;
}
public void UpdateToken(string accessToken, string? refreshToken, DateTime? expiredAt)
{
AccessToken = accessToken;
RefreshToken = refreshToken;
TokenExpiredAt = expiredAt;
}
}

View File

@ -0,0 +1,26 @@
namespace Fengling.Member.Domain.Aggregates.Users;
public enum OAuthProvider
{
Wechat = 1,
Alipay = 2,
QQ = 3,
Weibo = 4,
Douyin = 5
}
public static class OAuthProviderExtensions
{
public static string GetProviderName(this OAuthProvider provider)
{
return provider switch
{
OAuthProvider.Wechat => "wechat",
OAuthProvider.Alipay => "alipay",
OAuthProvider.QQ => "qq",
OAuthProvider.Weibo => "weibo",
OAuthProvider.Douyin => "douyin",
_ => throw new ArgumentException($"未知的OAuth提供商: {provider}")
};
}
}

View File

@ -0,0 +1,38 @@
namespace Fengling.Member.Domain.Aggregates.Users;
public class WechatAuthorization : Entity<long>
{
public long MemberId { get; private set; }
public string OpenId { get; private set; } = string.Empty;
public string? UnionId { get; private set; }
public DateTime AuthorizedAt { get; private set; } = DateTime.UtcNow;
public DateTime? LastLoginAt { get; private set; }
private WechatAuthorization()
{
}
public static WechatAuthorization Create(long memberId, string openId, string? unionId = null)
{
return new WechatAuthorization
{
MemberId = memberId,
OpenId = openId,
UnionId = unionId,
AuthorizedAt = DateTime.UtcNow
};
}
public void UpdateUnionId(string? unionId)
{
if (unionId != null && UnionId != unionId)
{
UnionId = unionId;
}
}
public void RecordLogin()
{
LastLoginAt = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,8 @@
namespace Fengling.Member.Domain.Events.Member;
public record MemberRegisteredEvent(
long MemberId,
long TenantId,
string? PhoneNumber,
DateTime RegisteredAt
) : IDomainEvent;

View File

@ -0,0 +1,9 @@
namespace Fengling.Member.Domain.Events.Points;
public record PointsChangedEvent(
long AccountId,
long MemberId,
int ChangedPoints,
int NewBalance,
string TransactionType
) : IDomainEvent;

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NetCorePal.Extensions.CodeAnalysis" />
<PackageReference Include="NetCorePal.Extensions.Domain.Abstractions" />
<PackageReference Include="NetCorePal.Extensions.Primitives" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,2 @@
global using NetCorePal.Extensions.Domain;
global using NetCorePal.Extensions.Primitives;

View File

@ -0,0 +1,11 @@
using Fengling.Member.Domain.Aggregates.Users;
namespace Fengling.Member.Domain.Repositories;
public interface IOAuthAuthorizationRepository
{
Task<OAuthAuthorization?> GetByProviderAndOpenIdAsync(OAuthProvider provider, string openId, CancellationToken cancellationToken = default);
Task<OAuthAuthorization?> GetByMemberIdAndProviderAsync(long memberId, OAuthProvider provider, CancellationToken cancellationToken = default);
Task<IEnumerable<OAuthAuthorization>> GetByMemberIdAsync(long memberId, CancellationToken cancellationToken = default);
Task<bool> ExistsByProviderAndOpenIdAsync(OAuthProvider provider, string openId, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,32 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using NetCorePal.Extensions.DistributedTransactions.CAP.Persistence;
namespace Fengling.Member.Infrastructure;
public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IMediator mediator)
: AppDbContextBase(options, mediator)
, IPostgreSqlCapDataStorage
{
public DbSet<Fengling.Member.Domain.Aggregates.Users.MemberEntity> Members => Set<Fengling.Member.Domain.Aggregates.Users.MemberEntity>();
public DbSet<Fengling.Member.Domain.Aggregates.Users.MemberTag> MemberTags => Set<Fengling.Member.Domain.Aggregates.Users.MemberTag>();
public DbSet<Fengling.Member.Domain.Aggregates.Users.WechatAuthorization> WechatAuthorizations => Set<Fengling.Member.Domain.Aggregates.Users.WechatAuthorization>();
public DbSet<Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount> PointsAccounts => Set<Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (modelBuilder is null)
{
throw new ArgumentNullException(nameof(modelBuilder));
}
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
ConfigureStronglyTypedIdValueConverter(configurationBuilder);
base.ConfigureConventions(configurationBuilder);
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.DependencyInjection;
namespace Fengling.Member.Infrastructure;
public class DesignTimeApplicationDbContextFactory: IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
IServiceCollection services = new ServiceCollection();
services.AddMediatR(c =>
c.RegisterServicesFromAssemblies(typeof(DesignTimeApplicationDbContextFactory).Assembly));
services.AddDbContext<ApplicationDbContext>(options =>
{
// change connectionstring if you want to run command “dotnet ef database update”
options.UseNpgsql("Host=any;Database=any;Username=any;Password=any",
b =>
{
b.MigrationsAssembly(typeof(DesignTimeApplicationDbContextFactory).Assembly.FullName);
});
});
var provider = services.BuildServiceProvider();
var dbContext = provider.CreateScope().ServiceProvider.GetRequiredService<ApplicationDbContext>();
return dbContext;
}
}

View File

@ -0,0 +1,80 @@
using Fengling.Member.Domain.Aggregates.Users;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Fengling.Member.Infrastructure.EntityConfigurations;
public class MemberEntityTypeConfiguration : IEntityTypeConfiguration<MemberEntity>
{
private static readonly ValueConverter<MemberStatus, string> _statusConverter = new(
v => v.Value,
v => MemberStatus.FromValue(v));
public void Configure(EntityTypeBuilder<MemberEntity> builder)
{
builder.ToTable("fls_member");
builder.HasKey(m => m.Id);
builder.Property(m => m.Id)
.HasColumnName("id")
.UseIdentityColumn();
builder.Property(m => m.TenantId)
.HasColumnName("tenant_id")
.IsRequired();
builder.Property(m => m.PhoneNumber)
.HasColumnName("phone_number")
.HasMaxLength(20);
builder.Property(m => m.OpenId)
.HasColumnName("open_id")
.HasMaxLength(64);
builder.Property(m => m.UnionId)
.HasColumnName("union_id")
.HasMaxLength(64);
builder.Property(m => m.Status)
.HasColumnName("status")
.HasMaxLength(20)
.HasConversion(_statusConverter);
builder.Property(m => m.CreatedAt)
.HasColumnName("created_at")
.IsRequired();
builder.Property(m => m.UpdatedAt)
.HasColumnName("updated_at");
builder.Property(m => m.Version)
.HasColumnName("version")
.IsRequired()
.HasDefaultValue(1)
.IsConcurrencyToken();
builder.HasIndex(m => new { m.TenantId, m.PhoneNumber })
.HasDatabaseName("idx_member_tenant_phone");
builder.HasIndex(m => m.OpenId)
.HasDatabaseName("idx_member_openid");
builder.HasIndex(m => m.UnionId)
.HasDatabaseName("idx_member_unionid");
builder.HasIndex(m => m.TenantId)
.HasDatabaseName("idx_member_tenantid");
builder.HasMany(m => m.Tags)
.WithOne()
.HasForeignKey(t => t.MemberId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(m => m.OAuthAuthorizations)
.WithOne()
.HasForeignKey(w => w.MemberId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@ -0,0 +1,43 @@
using Fengling.Member.Domain.Aggregates.Users;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Fengling.Member.Infrastructure.EntityConfigurations;
public class MemberTagEntityTypeConfiguration : IEntityTypeConfiguration<MemberTag>
{
public void Configure(EntityTypeBuilder<MemberTag> builder)
{
builder.ToTable("fls_member_tag");
builder.HasKey(t => t.Id);
builder.Property(t => t.Id)
.HasColumnName("id")
.UseIdentityColumn();
builder.Property(t => t.MemberId)
.HasColumnName("member_id")
.IsRequired();
builder.Property(t => t.TagId)
.HasColumnName("tag_id")
.HasMaxLength(50)
.IsRequired();
builder.Property(t => t.TagName)
.HasColumnName("tag_name")
.HasMaxLength(100);
builder.Property(t => t.CreatedAt)
.HasColumnName("created_at")
.IsRequired();
builder.HasIndex(t => new { t.MemberId, t.TagId })
.HasDatabaseName("idx_membertag_member_tag")
.IsUnique();
builder.HasIndex(t => t.TagId)
.HasDatabaseName("idx_membertag_tagid");
}
}

View File

@ -0,0 +1,57 @@
using Fengling.Member.Domain.Aggregates.PointsModel;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Fengling.Member.Infrastructure.EntityConfigurations;
public class PointsAccountEntityTypeConfiguration : IEntityTypeConfiguration<PointsAccount>
{
public void Configure(EntityTypeBuilder<PointsAccount> builder)
{
builder.ToTable("mka_integraldetails");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id)
.HasColumnName("id")
.UseIdentityColumn();
builder.Property(p => p.MemberId)
.HasColumnName("user_id")
.IsRequired();
builder.Property(p => p.TenantId)
.HasColumnName("tenant_id")
.IsRequired();
builder.Property(p => p.TotalPoints)
.HasColumnName("points")
.IsRequired()
.HasDefaultValue(0);
builder.Property(p => p.FrozenPoints)
.HasColumnName("frozen_points")
.IsRequired()
.HasDefaultValue(0);
builder.Property(p => p.Version)
.HasColumnName("version")
.IsRequired()
.HasDefaultValue(1)
.IsConcurrencyToken();
builder.Property(p => p.CreatedAt)
.HasColumnName("created_at")
.IsRequired();
builder.Property(p => p.UpdatedAt)
.HasColumnName("updated_at");
builder.HasIndex(p => p.MemberId)
.HasDatabaseName("idx_points_account_memberid")
.IsUnique();
builder.HasIndex(p => new { p.MemberId, p.TenantId })
.HasDatabaseName("idx_points_account_member_tenant");
}
}

View File

@ -0,0 +1,49 @@
using Fengling.Member.Domain.Aggregates.Users;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Fengling.Member.Infrastructure.EntityConfigurations;
public class WechatAuthorizationEntityTypeConfiguration : IEntityTypeConfiguration<WechatAuthorization>
{
public void Configure(EntityTypeBuilder<WechatAuthorization> builder)
{
builder.ToTable("fls_wechat_authorization");
builder.HasKey(w => w.Id);
builder.Property(w => w.Id)
.HasColumnName("id")
.UseIdentityColumn();
builder.Property(w => w.MemberId)
.HasColumnName("member_id")
.IsRequired();
builder.Property(w => w.OpenId)
.HasColumnName("open_id")
.HasMaxLength(64)
.IsRequired();
builder.Property(w => w.UnionId)
.HasColumnName("union_id")
.HasMaxLength(64);
builder.Property(w => w.AuthorizedAt)
.HasColumnName("authorized_at")
.IsRequired();
builder.Property(w => w.LastLoginAt)
.HasColumnName("last_login_at");
builder.HasIndex(w => w.OpenId)
.HasDatabaseName("idx_wechat_auth_openid")
.IsUnique();
builder.HasIndex(w => w.UnionId)
.HasDatabaseName("idx_wechat_auth_unionid");
builder.HasIndex(w => w.MemberId)
.HasDatabaseName("idx_wechat_auth_memberid");
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL"/>
<PackageReference Include="NetCorePal.Extensions.DistributedTransactions.CAP.PostgreSQL"/>
<PackageReference Include="NetCorePal.Extensions.Repository.EntityFrameworkCore"/>
<PackageReference Include="NetCorePal.Extensions.Repository.EntityFrameworkCore.Snowflake"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fengling.Member.Domain\Fengling.Member.Domain.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Update="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
global using NetCorePal.Extensions.Domain;
global using NetCorePal.Extensions.Primitives;
global using NetCorePal.Extensions.Repository;
global using NetCorePal.Extensions.Repository.EntityFrameworkCore;
global using MediatR;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.EntityFrameworkCore.Metadata.Builders;

View File

@ -0,0 +1,140 @@
// <auto-generated />
using System;
using Fengling.Member.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Member.Infrastructure.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260122054728_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TIMESTAMP");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,101 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Member.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CAPLock",
columns: table => new
{
Key = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Instance = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
LastLockTime = table.Column<DateTime>(type: "TIMESTAMP", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_CAPLock", x => x.Key);
});
migrationBuilder.CreateTable(
name: "CAPPublishedMessage",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Version = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Content = table.Column<string>(type: "TEXT", nullable: true),
Retries = table.Column<int>(type: "integer", nullable: true),
Added = table.Column<DateTime>(type: "TIMESTAMP", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "TIMESTAMP", nullable: true),
StatusName = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CAPPublishedMessage", x => x.Id);
});
migrationBuilder.CreateTable(
name: "CAPReceivedMessage",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Version = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
Name = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
Group = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Content = table.Column<string>(type: "TEXT", nullable: true),
Retries = table.Column<int>(type: "integer", nullable: true),
Added = table.Column<DateTime>(type: "TIMESTAMP", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "TIMESTAMP", nullable: true),
StatusName = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CAPReceivedMessage", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ExpiresAt_StatusName",
table: "CAPPublishedMessage",
columns: new[] { "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex(
name: "IX_Version_ExpiresAt_StatusName",
table: "CAPPublishedMessage",
columns: new[] { "Version", "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex(
name: "IX_ExpiresAt_StatusName1",
table: "CAPReceivedMessage",
columns: new[] { "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex(
name: "IX_Version_ExpiresAt_StatusName1",
table: "CAPReceivedMessage",
columns: new[] { "Version", "ExpiresAt", "StatusName" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CAPLock");
migrationBuilder.DropTable(
name: "CAPPublishedMessage");
migrationBuilder.DropTable(
name: "CAPReceivedMessage");
}
}
}

View File

@ -0,0 +1,429 @@
// <auto-generated />
using System;
using Fengling.Member.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Member.Infrastructure.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260205051658_AddMemberAndPointsEntities")]
partial class AddMemberAndPointsEntities
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<int>("FrozenPoints")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("frozen_points");
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("user_id");
b.Property<long>("TenantId")
.HasColumnType("bigint")
.HasColumnName("tenant_id");
b.Property<int>("TotalPoints")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("points");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<int>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1)
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("MemberId")
.IsUnique()
.HasDatabaseName("idx_points_account_memberid");
b.HasIndex("MemberId", "TenantId")
.HasDatabaseName("idx_points_account_member_tenant");
b.ToTable("mka_integraldetails", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long>("MemberId")
.HasColumnType("bigint");
b.Property<int>("Points")
.HasColumnType("integer");
b.Property<long>("PointsAccountId")
.HasColumnType("bigint");
b.Property<string>("Remark")
.HasColumnType("text");
b.Property<string>("SourceId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TransactionType")
.IsRequired()
.HasColumnType("text");
b.Property<int>("TransactionTypeCategory")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("PointsAccountId");
b.ToTable("PointsTransaction");
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<string>("OpenId")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("open_id");
b.Property<string>("PhoneNumber")
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("phone_number");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("status");
b.Property<long>("TenantId")
.HasColumnType("bigint")
.HasColumnName("tenant_id");
b.Property<string>("UnionId")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("union_id");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<int>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1)
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("OpenId")
.HasDatabaseName("idx_member_openid");
b.HasIndex("TenantId")
.HasDatabaseName("idx_member_tenantid");
b.HasIndex("UnionId")
.HasDatabaseName("idx_member_unionid");
b.HasIndex("TenantId", "PhoneNumber")
.HasDatabaseName("idx_member_tenant_phone");
b.ToTable("fls_member", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("member_id");
b.Property<string>("TagId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("tag_id");
b.Property<string>("TagName")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("tag_name");
b.HasKey("Id");
b.HasIndex("TagId")
.HasDatabaseName("idx_membertag_tagid");
b.HasIndex("MemberId", "TagId")
.IsUnique()
.HasDatabaseName("idx_membertag_member_tag");
b.ToTable("fls_member_tag", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.WechatAuthorization", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("AuthorizedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("authorized_at");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_login_at");
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("member_id");
b.Property<string>("OpenId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("open_id");
b.Property<string>("UnionId")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("union_id");
b.HasKey("Id");
b.HasIndex("MemberId")
.HasDatabaseName("idx_wechat_auth_memberid");
b.HasIndex("OpenId")
.IsUnique()
.HasDatabaseName("idx_wechat_auth_openid");
b.HasIndex("UnionId")
.HasDatabaseName("idx_wechat_auth_unionid");
b.ToTable("fls_wechat_authorization", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TIMESTAMP");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", null)
.WithMany("Transactions")
.HasForeignKey("PointsAccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
.WithMany("Tags")
.HasForeignKey("MemberId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.WechatAuthorization", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
.WithMany("WechatAuthorizations")
.HasForeignKey("MemberId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
{
b.Navigation("Transactions");
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
{
b.Navigation("Tags");
b.Navigation("WechatAuthorizations");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,208 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Member.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddMemberAndPointsEntities : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "fls_member",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
tenant_id = table.Column<long>(type: "bigint", nullable: false),
phone_number = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
open_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
union_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
version = table.Column<int>(type: "integer", nullable: false, defaultValue: 1)
},
constraints: table =>
{
table.PrimaryKey("PK_fls_member", x => x.id);
});
migrationBuilder.CreateTable(
name: "mka_integraldetails",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
user_id = table.Column<long>(type: "bigint", nullable: false),
tenant_id = table.Column<long>(type: "bigint", nullable: false),
points = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
frozen_points = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
version = table.Column<int>(type: "integer", nullable: false, defaultValue: 1),
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_mka_integraldetails", x => x.id);
});
migrationBuilder.CreateTable(
name: "fls_member_tag",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
member_id = table.Column<long>(type: "bigint", nullable: false),
tag_id = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
tag_name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_fls_member_tag", x => x.id);
table.ForeignKey(
name: "FK_fls_member_tag_fls_member_member_id",
column: x => x.member_id,
principalTable: "fls_member",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "fls_wechat_authorization",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
member_id = table.Column<long>(type: "bigint", nullable: false),
open_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
union_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
authorized_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
last_login_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_fls_wechat_authorization", x => x.id);
table.ForeignKey(
name: "FK_fls_wechat_authorization_fls_member_member_id",
column: x => x.member_id,
principalTable: "fls_member",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PointsTransaction",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
PointsAccountId = table.Column<long>(type: "bigint", nullable: false),
MemberId = table.Column<long>(type: "bigint", nullable: false),
Points = table.Column<int>(type: "integer", nullable: false),
TransactionType = table.Column<string>(type: "text", nullable: false),
SourceId = table.Column<string>(type: "text", nullable: false),
TransactionTypeCategory = table.Column<int>(type: "integer", nullable: false),
Remark = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PointsTransaction", x => x.Id);
table.ForeignKey(
name: "FK_PointsTransaction_mka_integraldetails_PointsAccountId",
column: x => x.PointsAccountId,
principalTable: "mka_integraldetails",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "idx_member_openid",
table: "fls_member",
column: "open_id");
migrationBuilder.CreateIndex(
name: "idx_member_tenant_phone",
table: "fls_member",
columns: new[] { "tenant_id", "phone_number" });
migrationBuilder.CreateIndex(
name: "idx_member_tenantid",
table: "fls_member",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "idx_member_unionid",
table: "fls_member",
column: "union_id");
migrationBuilder.CreateIndex(
name: "idx_membertag_member_tag",
table: "fls_member_tag",
columns: new[] { "member_id", "tag_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "idx_membertag_tagid",
table: "fls_member_tag",
column: "tag_id");
migrationBuilder.CreateIndex(
name: "idx_wechat_auth_memberid",
table: "fls_wechat_authorization",
column: "member_id");
migrationBuilder.CreateIndex(
name: "idx_wechat_auth_openid",
table: "fls_wechat_authorization",
column: "open_id",
unique: true);
migrationBuilder.CreateIndex(
name: "idx_wechat_auth_unionid",
table: "fls_wechat_authorization",
column: "union_id");
migrationBuilder.CreateIndex(
name: "idx_points_account_member_tenant",
table: "mka_integraldetails",
columns: new[] { "user_id", "tenant_id" });
migrationBuilder.CreateIndex(
name: "idx_points_account_memberid",
table: "mka_integraldetails",
column: "user_id",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PointsTransaction_PointsAccountId",
table: "PointsTransaction",
column: "PointsAccountId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "fls_member_tag");
migrationBuilder.DropTable(
name: "fls_wechat_authorization");
migrationBuilder.DropTable(
name: "PointsTransaction");
migrationBuilder.DropTable(
name: "fls_member");
migrationBuilder.DropTable(
name: "mka_integraldetails");
}
}
}

View File

@ -0,0 +1,426 @@
// <auto-generated />
using System;
using Fengling.Member.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Member.Infrastructure.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<int>("FrozenPoints")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("frozen_points");
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("user_id");
b.Property<long>("TenantId")
.HasColumnType("bigint")
.HasColumnName("tenant_id");
b.Property<int>("TotalPoints")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("points");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<int>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1)
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("MemberId")
.IsUnique()
.HasDatabaseName("idx_points_account_memberid");
b.HasIndex("MemberId", "TenantId")
.HasDatabaseName("idx_points_account_member_tenant");
b.ToTable("mka_integraldetails", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long>("MemberId")
.HasColumnType("bigint");
b.Property<int>("Points")
.HasColumnType("integer");
b.Property<long>("PointsAccountId")
.HasColumnType("bigint");
b.Property<string>("Remark")
.HasColumnType("text");
b.Property<string>("SourceId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TransactionType")
.IsRequired()
.HasColumnType("text");
b.Property<int>("TransactionTypeCategory")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("PointsAccountId");
b.ToTable("PointsTransaction");
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<string>("OpenId")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("open_id");
b.Property<string>("PhoneNumber")
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("phone_number");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("status");
b.Property<long>("TenantId")
.HasColumnType("bigint")
.HasColumnName("tenant_id");
b.Property<string>("UnionId")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("union_id");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<int>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1)
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("OpenId")
.HasDatabaseName("idx_member_openid");
b.HasIndex("TenantId")
.HasDatabaseName("idx_member_tenantid");
b.HasIndex("UnionId")
.HasDatabaseName("idx_member_unionid");
b.HasIndex("TenantId", "PhoneNumber")
.HasDatabaseName("idx_member_tenant_phone");
b.ToTable("fls_member", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("member_id");
b.Property<string>("TagId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("tag_id");
b.Property<string>("TagName")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("tag_name");
b.HasKey("Id");
b.HasIndex("TagId")
.HasDatabaseName("idx_membertag_tagid");
b.HasIndex("MemberId", "TagId")
.IsUnique()
.HasDatabaseName("idx_membertag_member_tag");
b.ToTable("fls_member_tag", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.WechatAuthorization", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("AuthorizedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("authorized_at");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_login_at");
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("member_id");
b.Property<string>("OpenId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("open_id");
b.Property<string>("UnionId")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("union_id");
b.HasKey("Id");
b.HasIndex("MemberId")
.HasDatabaseName("idx_wechat_auth_memberid");
b.HasIndex("OpenId")
.IsUnique()
.HasDatabaseName("idx_wechat_auth_openid");
b.HasIndex("UnionId")
.HasDatabaseName("idx_wechat_auth_unionid");
b.ToTable("fls_wechat_authorization", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TIMESTAMP");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", null)
.WithMany("Transactions")
.HasForeignKey("PointsAccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
.WithMany("Tags")
.HasForeignKey("MemberId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.WechatAuthorization", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
.WithMany("WechatAuthorizations")
.HasForeignKey("MemberId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
{
b.Navigation("Transactions");
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
{
b.Navigation("Tags");
b.Navigation("WechatAuthorizations");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,52 @@
using Fengling.Member.Domain.Aggregates.Users;
namespace Fengling.Member.Infrastructure.Repositories;
public interface IMemberRepository : IRepository<Fengling.Member.Domain.Aggregates.Users.MemberEntity, long>
{
Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default);
Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByOpenIdAsync(string openId, CancellationToken cancellationToken = default);
Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByUnionIdAsync(string unionId, CancellationToken cancellationToken = default);
Task<IEnumerable<Fengling.Member.Domain.Aggregates.Users.MemberEntity>> GetByTenantIdAsync(long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default);
Task<bool> ExistsByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default);
Task<bool> ExistsByOpenIdAsync(string openId, CancellationToken cancellationToken = default);
}
public class MemberRepository(ApplicationDbContext context) :
RepositoryBase<Fengling.Member.Domain.Aggregates.Users.MemberEntity, long, ApplicationDbContext>(context), IMemberRepository
{
public async Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default)
{
return await DbContext.Members.FirstOrDefaultAsync(m => m.TenantId == tenantId && m.PhoneNumber == phoneNumber, cancellationToken);
}
public async Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByOpenIdAsync(string openId, CancellationToken cancellationToken = default)
{
return await DbContext.Members.FirstOrDefaultAsync(m => m.OpenId == openId, cancellationToken);
}
public async Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByUnionIdAsync(string unionId, CancellationToken cancellationToken = default)
{
return await DbContext.Members.FirstOrDefaultAsync(m => m.UnionId == unionId, cancellationToken);
}
public async Task<IEnumerable<Fengling.Member.Domain.Aggregates.Users.MemberEntity>> GetByTenantIdAsync(long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default)
{
return await DbContext.Members
.Where(m => m.TenantId == tenantId)
.OrderByDescending(m => m.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
}
public async Task<bool> ExistsByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default)
{
return await DbContext.Members.AnyAsync(m => m.TenantId == tenantId && m.PhoneNumber == phoneNumber, cancellationToken);
}
public async Task<bool> ExistsByOpenIdAsync(string openId, CancellationToken cancellationToken = default)
{
return await DbContext.Members.AnyAsync(m => m.OpenId == openId, cancellationToken);
}
}

View File

@ -0,0 +1,51 @@
using Fengling.Member.Domain.Aggregates.PointsModel;
namespace Fengling.Member.Infrastructure.Repositories;
public interface IPointsAccountRepository : IRepository<PointsAccount, long>
{
Task<PointsAccount?> GetByMemberIdAsync(long memberId, CancellationToken cancellationToken = default);
Task<PointsAccount?> GetByMemberIdAndTenantIdAsync(long memberId, long tenantId, CancellationToken cancellationToken = default);
Task<PointsAccount?> GetByMemberIdWithLockAsync(long memberId, CancellationToken cancellationToken = default);
Task<IEnumerable<PointsAccount>> GetByTenantIdAsync(long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default);
Task<bool> ExistsByMemberIdAsync(long memberId, CancellationToken cancellationToken = default);
}
public class PointsAccountRepository(ApplicationDbContext context) :
RepositoryBase<PointsAccount, long, ApplicationDbContext>(context), IPointsAccountRepository
{
public async Task<PointsAccount?> GetByMemberIdAsync(long memberId, CancellationToken cancellationToken = default)
{
return await DbContext.PointsAccounts.FirstOrDefaultAsync(p => p.MemberId == memberId, cancellationToken);
}
public async Task<PointsAccount?> GetByMemberIdAndTenantIdAsync(long memberId, long tenantId, CancellationToken cancellationToken = default)
{
return await DbContext.PointsAccounts.FirstOrDefaultAsync(p => p.MemberId == memberId && p.TenantId == tenantId, cancellationToken);
}
public async Task<PointsAccount?> GetByMemberIdWithLockAsync(long memberId, CancellationToken cancellationToken = default)
{
var account = await DbContext.PointsAccounts.FirstOrDefaultAsync(p => p.MemberId == memberId, cancellationToken);
if (account != null)
{
await DbContext.Entry(account).Reference(p => p.Transactions).LoadAsync(cancellationToken);
}
return account;
}
public async Task<IEnumerable<PointsAccount>> GetByTenantIdAsync(long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default)
{
return await DbContext.PointsAccounts
.Where(p => p.TenantId == tenantId)
.OrderByDescending(p => p.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
}
public async Task<bool> ExistsByMemberIdAsync(long memberId, CancellationToken cancellationToken = default)
{
return await DbContext.PointsAccounts.AnyAsync(p => p.MemberId == memberId, cancellationToken);
}
}

View File

@ -0,0 +1,14 @@
namespace Fengling.Member.Web.Application.Hubs;
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
}
public class ChatHub : Microsoft.AspNetCore.SignalR.Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}
}

View File

@ -0,0 +1,11 @@
using Refit;
namespace Fengling.Member.Web.Clients;
public interface IUserServiceClient
{
[Get("/users/{userId}")]
Task<UserDto> GetUserAsync(long userId);
}
public record UserDto(string Name, string Email, string Phone);

View File

@ -0,0 +1,21 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore "src/Fengling.Member.Web/Fengling.Member.Web.csproj"
WORKDIR "/src/src/Fengling.Member.Web"
RUN dotnet build "Fengling.Member.Web.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Fengling.Member.Web.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Fengling.Member.Web.dll"]

View File

@ -0,0 +1,25 @@
using FastEndpoints;
using FastEndpoints.Swagger;
using Microsoft.AspNetCore.Authorization;
using NetCorePal.Extensions.Dto;
namespace Fengling.Member.Web.Endpoints;
/// <summary>
/// Hello
/// </summary>
public class HelloEndpoint : EndpointWithoutRequest<ResponseData<string>>
{
public override void Configure()
{
Tags("Hello");
Description(b => b.AutoTagOverride("Hello"));
Get("/api/hello");
AllowAnonymous();
}
public override Task HandleAsync(CancellationToken ct)
{
return Send.OkAsync("hello".AsResponseData(), cancellation: ct);
}
}

View File

@ -0,0 +1,62 @@
using FastEndpoints;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Fengling.Member.Application.Commands.Member;
namespace Fengling.Member.Web.Endpoints.v1;
public class BindAlipayEndpoint : Endpoint<BindAlipayRequest, BindAlipayResponse>
{
private readonly IMediator _mediator;
public BindAlipayEndpoint(IMediator mediator)
{
_mediator = mediator;
}
public override void Configure()
{
Post("/api/v1/members/{MemberId}/alipay");
Summary(s =>
{
s.Summary = "绑定支付宝";
s.Description = "为会员绑定支付宝账号";
});
}
public override async Task HandleAsync(BindAlipayRequest req, CancellationToken ct)
{
var command = new BindAlipayCommand
{
MemberId = req.MemberId,
AlipayOpenId = req.AlipayOpenId,
AlipayUserId = req.AlipayUserId
};
var result = await _mediator.Send(command, ct);
Response = new BindAlipayResponse
{
MemberId = result.MemberId,
AlipayOpenId = result.AlipayOpenId,
AlipayUserId = result.AlipayUserId,
BoundAt = result.BoundAt
};
}
}
public class BindAlipayRequest
{
[FromRoute]
public long MemberId { get; set; }
public string AlipayOpenId { get; set; } = string.Empty;
public string? AlipayUserId { get; set; }
}
public class BindAlipayResponse
{
public long MemberId { get; set; }
public string AlipayOpenId { get; set; } = string.Empty;
public string? AlipayUserId { get; set; }
public DateTime BoundAt { get; set; }
}

View File

@ -0,0 +1,69 @@
using FastEndpoints;
using MediatR;
using Fengling.Member.Application.Commands.Member;
namespace Fengling.Member.Web.Endpoints.v1;
public class RegisterMemberEndpoint : Endpoint<RegisterMemberRequest, RegisterMemberResponse>
{
private readonly IMediator _mediator;
public RegisterMemberEndpoint(IMediator mediator)
{
_mediator = mediator;
}
public override void Configure()
{
Post("/api/v1/members");
AllowAnonymous();
Summary(s =>
{
s.Summary = "注册会员";
s.Description = "根据手机号或微信OpenID注册新会员";
});
}
public override async Task HandleAsync(RegisterMemberRequest req, CancellationToken ct)
{
var command = new RegisterMemberCommand
{
TenantId = req.TenantId,
PhoneNumber = req.PhoneNumber,
OpenId = req.OpenId,
UnionId = req.UnionId,
Source = req.Source
};
var result = await _mediator.Send(command, ct);
Response = new RegisterMemberResponse
{
MemberId = result.MemberId,
TenantId = result.TenantId,
PhoneNumber = result.PhoneNumber,
OpenId = result.OpenId,
Status = result.Status.ToString(),
RegisteredAt = result.RegisteredAt
};
}
}
public class RegisterMemberRequest
{
public long TenantId { get; set; }
public string? PhoneNumber { get; set; }
public string? OpenId { get; set; }
public string? UnionId { get; set; }
public string? Source { get; set; }
}
public class RegisterMemberResponse
{
public long MemberId { get; set; }
public long TenantId { get; set; }
public string? PhoneNumber { get; set; }
public string? OpenId { get; set; }
public string Status { get; set; } = string.Empty;
public DateTime RegisteredAt { get; set; }
}

View File

@ -0,0 +1,67 @@
using FastEndpoints;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Fengling.Member.Application.Commands.Member;
namespace Fengling.Member.Web.Endpoints.v1;
public class BindOAuthEndpoint : Endpoint<BindOAuthRequest, BindOAuthResponse>
{
private readonly IMediator _mediator;
public BindOAuthEndpoint(IMediator mediator)
{
_mediator = mediator;
}
public override void Configure()
{
Post("/api/v1/members/{MemberId}/oauth");
Summary(s =>
{
s.Summary = "绑定第三方账号";
s.Description = "为会员绑定微信、支付宝等第三方账号";
});
}
public override async Task HandleAsync(BindOAuthRequest req, CancellationToken ct)
{
var command = new BindOAuthCommand
{
MemberId = req.MemberId,
Provider = req.Provider,
OpenId = req.OpenId,
UnionId = req.UnionId
};
var result = await _mediator.Send(command, ct);
Response = new BindOAuthResponse
{
MemberId = result.MemberId,
Provider = result.Provider,
OpenId = result.OpenId,
UnionId = result.UnionId,
BoundAt = result.BoundAt
};
}
}
public class BindOAuthRequest
{
[FromRoute]
public long MemberId { get; set; }
[FromRoute]
public Domain.Aggregates.Users.OAuthProvider Provider { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
}
public class BindOAuthResponse
{
public long MemberId { get; set; }
public Domain.Aggregates.Users.OAuthProvider Provider { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
public DateTime BoundAt { get; set; }
}

View File

@ -0,0 +1,68 @@
using FastEndpoints;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Fengling.Member.Application.Commands.Points;
namespace Fengling.Member.Web.Endpoints.v1;
public class AddPointsEndpoint : Endpoint<AddPointsRequest, AddPointsResponse>
{
private readonly IMediator _mediator;
public AddPointsEndpoint(IMediator mediator)
{
_mediator = mediator;
}
public override void Configure()
{
Post("/api/v1/members/{MemberId}/points");
Summary(s =>
{
s.Summary = "增加积分";
s.Description = "为会员增加积分";
});
}
public override async Task HandleAsync(AddPointsRequest req, CancellationToken ct)
{
var command = new AddPointsCommand
{
MemberId = req.MemberId,
Points = req.Points,
TransactionType = req.TransactionType,
SourceId = req.SourceId,
Remark = req.Remark
};
var result = await _mediator.Send(command, ct);
Response = new AddPointsResponse
{
AccountId = result.AccountId,
MemberId = result.MemberId,
AddedPoints = result.AddedPoints,
TotalPoints = result.TotalPoints,
TransactionAt = result.TransactionAt
};
}
}
public class AddPointsRequest
{
[FromRoute]
public long MemberId { get; set; }
public int Points { get; set; }
public string TransactionType { get; set; } = string.Empty;
public string SourceId { get; set; } = string.Empty;
public string? Remark { get; set; }
}
public class AddPointsResponse
{
public long AccountId { get; set; }
public long MemberId { get; set; }
public int AddedPoints { get; set; }
public int TotalPoints { get; set; }
public DateTime TransactionAt { get; set; }
}

View File

@ -0,0 +1,62 @@
using FastEndpoints;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Fengling.Member.Application.Commands.Member;
namespace Fengling.Member.Web.Endpoints.v1;
public class BindWechatEndpoint : Endpoint<BindWechatRequest, BindWechatResponse>
{
private readonly IMediator _mediator;
public BindWechatEndpoint(IMediator mediator)
{
_mediator = mediator;
}
public override void Configure()
{
Post("/api/v1/members/{MemberId}/wechat");
Summary(s =>
{
s.Summary = "绑定微信";
s.Description = "为会员绑定微信OpenID";
});
}
public override async Task HandleAsync(BindWechatRequest req, CancellationToken ct)
{
var command = new BindWechatCommand
{
MemberId = req.MemberId,
OpenId = req.OpenId,
UnionId = req.UnionId
};
var result = await _mediator.Send(command, ct);
Response = new BindWechatResponse
{
MemberId = result.MemberId,
OpenId = result.OpenId,
UnionId = result.UnionId,
BoundAt = result.BoundAt
};
}
}
public class BindWechatRequest
{
[FromRoute]
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
}
public class BindWechatResponse
{
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
public DateTime BoundAt { get; set; }
}

View File

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.StackExchangeRedis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
namespace Fengling.Member.Web.Extensions;
/// <summary>
/// Extension methods for configuring StackExchange.Redis-based data protection.
/// </summary>
public static class StackExchangeRedisDataProtectionBuilderExtensions
{
/// <summary>
/// Configures data protection to persist keys to StackExchange.Redis.
/// This method resolves IConnectionMultiplexer from DI, making it work with both
/// Aspire (where AddRedisClient registers the multiplexer) and non-Aspire scenarios.
/// </summary>
/// <param name="builder">The data protection builder.</param>
/// <param name="key">The Redis key where data protection keys will be stored.</param>
/// <returns>The data protection builder for chaining.</returns>
public static IDataProtectionBuilder PersistKeysToStackExchangeRedis(
this IDataProtectionBuilder builder,
RedisKey key)
{
builder.Services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(services =>
{
var connectionMultiplexer = services.GetRequiredService<IConnectionMultiplexer>();
return new ConfigureOptions<KeyManagementOptions>(options =>
{
options.XmlRepository = new RedisXmlRepository(() => connectionMultiplexer.GetDatabase(), key);
});
});
return builder;
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Fengling.Member.Web.Extensions;
public static class SwaggerGenOptionsExtionsions
{
public static SwaggerGenOptions AddEntityIdSchemaMap(this SwaggerGenOptions swaggerGenOptions)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()
.Where(p => p.FullName != null && p.FullName.Contains("Fengling.Member")))
{
foreach (var type in assembly.GetTypes())
{
if (type.IsClass && Array.Exists(type.GetInterfaces(), p => p == typeof(IEntityId)))
{
swaggerGenOptions.MapType(type,
() => new OpenApiSchema { Type = typeof(string).Name.ToLower() });
}
}
}
return swaggerGenOptions;
}
}

View File

@ -0,0 +1,69 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Feishu" />
<PackageReference Include="AspNet.Security.OAuth.Weixin" />
<PackageReference Include="DotNetCore.CAP.Dashboard" />
<PackageReference Include="DotNetCore.CAP.RabbitMQ" />
<PackageReference Include="FastEndpoints" />
<PackageReference Include="FastEndpoints.Swagger" />
<PackageReference Include="FastEndpoints.Swagger.Swashbuckle" />
<PackageReference Include="FluentValidation.AspNetCore" />
<PackageReference Include="Hangfire.AspNetCore" />
<PackageReference Include="Hangfire.Redis.StackExchange" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
<PackageReference Include="NetCorePal.Context.AspNetCore" />
<PackageReference Include="NetCorePal.Context.CAP" />
<PackageReference Include="NetCorePal.Context.Shared" />
<PackageReference Include="NetCorePal.Extensions.AspNetCore" />
<PackageReference Include="NetCorePal.Extensions.CodeAnalysis" />
<PackageReference Include="NetCorePal.Extensions.DistributedLocks.Redis" />
<PackageReference Include="NetCorePal.Extensions.MultiEnv" />
<PackageReference Include="NetCorePal.Extensions.MicrosoftServiceDiscovery" />
<PackageReference Include="NetCorePal.Extensions.Primitives" />
<PackageReference Include="NetCorePal.Extensions.Jwt.StackExchangeRedis" />
<PackageReference Include="prometheus-net.AspNetCore" />
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" />
<PackageReference Include="Refit.HttpClientFactory" />
<PackageReference Include="Refit.Newtonsoft.Json" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.ClientInfo" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" />
<PackageReference Include="StackExchange.Redis" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fengling.Member.Domain\Fengling.Member.Domain.csproj" />
<ProjectReference Include="..\Fengling.Member.Infrastructure\Fengling.Member.Infrastructure.csproj" />
<ProjectReference Include="..\Fengling.Member.Application\Fengling.Member.Application.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
global using NetCorePal.Extensions.AspNetCore;
global using NetCorePal.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection;
global using Fengling.Member.Infrastructure;
global using FluentValidation;
global using NetCorePal.Extensions.Primitives;
global using MediatR;
global using NetCorePal.Extensions.Domain;
global using NetCorePal.Extensions.Dto;
global using NetCorePal.Extensions.DistributedTransactions;

View File

@ -0,0 +1,245 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Prometheus;
using System.Reflection;
using System.Text.Json;
using Microsoft.AspNetCore.DataProtection;
using StackExchange.Redis;
using FluentValidation.AspNetCore;
using Fengling.Member.Web.Clients;
using Fengling.Member.Web.Extensions;
using Fengling.Member.Web.Utils;
using FastEndpoints;
using Serilog;
using Serilog.Formatting.Json;
using Hangfire;
using Hangfire.Redis.StackExchange;
using Microsoft.AspNetCore.Http.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Refit;
using NetCorePal.Extensions.CodeAnalysis;
Log.Logger = new LoggerConfiguration()
.Enrich.WithClientIp()
.WriteTo.Console(new JsonFormatter())
.CreateLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
#region SignalR
builder.Services.AddHealthChecks();
builder.Services.AddMvc()
.AddNewtonsoftJson(options => { options.SerializerSettings.AddNetCorePalJsonConverters(); });
builder.Services.AddSignalR();
#endregion
#region Prometheus监控
builder.Services.AddHealthChecks().ForwardToPrometheus();
builder.Services.AddHttpClient(Options.DefaultName)
.UseHttpClientMetrics();
#endregion
// Add services to the container.
#region
var redis = await ConnectionMultiplexer.ConnectAsync(builder.Configuration.GetConnectionString("Redis")!);
builder.Services.AddSingleton<IConnectionMultiplexer>(_ => redis);
// DataProtection - use custom extension that resolves IConnectionMultiplexer from DI
builder.Services.AddDataProtection()
.PersistKeysToStackExchangeRedis("DataProtection-Keys");
// 配置JWT认证
builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration"));
var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>() ?? new AppConfiguration { JwtIssuer = "netcorepal", JwtAudience = "netcorepal" };
builder.Services.AddAuthentication().AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters.ValidAudience = appConfig.JwtAudience;
options.TokenValidationParameters.ValidateAudience = true;
options.TokenValidationParameters.ValidIssuer = appConfig.JwtIssuer;
options.TokenValidationParameters.ValidateIssuer = true;
});
builder.Services.AddNetCorePalJwt().AddRedisStore();
#endregion
#region Controller
builder.Services.AddControllers().AddNetCorePalSystemTextJson();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c => c.AddEntityIdSchemaMap()); //强类型id swagger schema 映射
#endregion
#region FastEndpoints
builder.Services.AddFastEndpoints(o => o.IncludeAbstractValidators = true);
builder.Services.Configure<JsonOptions>(o =>
o.SerializerOptions.AddNetCorePalJsonConverters());
#endregion
#region
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
builder.Services.AddKnownExceptionErrorModelInterceptor();
#endregion
#region
builder.Services.AddRepositories(typeof(ApplicationDbContext).Assembly);
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(builder.Configuration.GetConnectionString("PostgreSQL"));
// 仅在开发环境启用敏感数据日志,防止生产环境泄露敏感信息
if (builder.Environment.IsDevelopment())
{
options.EnableSensitiveDataLogging();
}
options.EnableDetailedErrors();
});
builder.Services.AddUnitOfWork<ApplicationDbContext>();
builder.Services.AddRedisLocks();
builder.Services.AddContext().AddEnvContext().AddCapContextProcessor();
builder.Services.AddNetCorePalServiceDiscoveryClient();
builder.Services.AddIntegrationEvents(typeof(Program))
.UseCap<ApplicationDbContext>(b =>
{
b.RegisterServicesFromAssemblies(typeof(Program));
b.AddContextIntegrationFilters();
});
builder.Services.AddCap(x =>
{
x.UseNetCorePalStorage<ApplicationDbContext>();
x.JsonSerializerOptions.AddNetCorePalJsonConverters();
x.ConsumerThreadCount = Environment.ProcessorCount;
x.UseRabbitMQ(p => builder.Configuration.GetSection("RabbitMQ").Bind(p));
x.UseDashboard(); //CAP Dashboard path /cap
});
#endregion
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly())
.AddCommandLockBehavior()
.AddKnownExceptionValidationBehavior()
.AddUnitOfWorkBehaviors());
#region
builder.Services.AddMultiEnv(envOption => envOption.ServiceName = "Abc.Template")
.UseMicrosoftServiceDiscovery();
builder.Services.AddConfigurationServiceEndpointProvider();
#endregion
#region
var jsonSerializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
jsonSerializerSettings.AddNetCorePalJsonConverters();
var ser = new NewtonsoftJsonContentSerializer(jsonSerializerSettings);
var settings = new RefitSettings(ser);
builder.Services.AddRefitClient<IUserServiceClient>(settings)
.ConfigureHttpClient(client =>
client.BaseAddress = new Uri(builder.Configuration.GetValue<string>("https+http://user:8080")!))
.AddMultiEnvMicrosoftServiceDiscovery() //多环境服务发现支持
.AddStandardResilienceHandler(); //添加标准的重试策略
#endregion
#region Jobs
builder.Services.AddHangfire(x => { x.UseRedisStorage(builder.Configuration.GetConnectionString("Redis")); });
builder.Services.AddHangfireServer(); //hangfire dashboard path /hangfire
#endregion
var app = builder.Build();
// 在非生产环境中执行数据库迁移包括开发、测试、Staging等环境
if (!app.Environment.IsProduction())
{
using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.MigrateAsync();
}
app.UseKnownExceptionHandler();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseStaticFiles();
//app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication(); // Authentication 必须在 Authorization 之前
app.UseAuthorization();
app.MapControllers();
app.UseFastEndpoints();
#region SignalR
app.MapHub<Fengling.Member.Web.Application.Hubs.ChatHub>("/chat");
#endregion
app.UseHttpMetrics();
app.MapHealthChecks("/health");
app.MapMetrics(); // 通过 /metrics 访问指标
// Code analysis endpoint
app.MapGet("/code-analysis", () =>
{
var assemblies = new List<Assembly> { typeof(Program).Assembly, typeof(ApplicationDbContext).Assembly };
var html = VisualizationHtmlBuilder.GenerateVisualizationHtml(
CodeFlowAnalysisHelper.GetResultFromAssemblies(assemblies.ToArray())
);
return Results.Content(html, "text/html; charset=utf-8");
});
app.UseHangfireDashboard();
await app.RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
await Log.CloseAndFlushAsync();
}
#pragma warning disable S1118
public partial class Program
#pragma warning restore S1118
{
}

View File

@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5511",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7435;http://localhost:5511",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,18 @@
namespace Fengling.Member.Web.Utils;
public class AppConfiguration
{
public string Secret { get; set; } = string.Empty;
public int TokenExpiryInMinutes { get; set; }
/// <summary>
/// JWT Issuer签发者
/// </summary>
public string JwtIssuer { get; set; } = "netcorepal";
/// <summary>
/// JWT Audience受众
/// </summary>
public string JwtAudience { get; set; } = "netcorepal";
}

View File

@ -0,0 +1,31 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"PostgreSQL": "Host=localhost;Database=dev;Username=postgres;Password=123456",
"Redis": "localhost:6379"
},
"RabbitMQ": {
"HostName": "localhost",
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/",
"Port": 5672
},
"Services": {
"user": {
"https": [
"https://user:8443"
]
},
"user-v2": {
"https": [
"https://user-v2:8443"
]
}
}
}

View File

@ -0,0 +1,32 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"PostgreSQL": "Host=localhost;Database=dev;Username=postgres;Password=123456",
"Redis": "localhost:6379"
},
"RabbitMQ": {
"HostName": "localhost",
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/",
"Port": 5672
},
"Services": {
"user": {
"https": [
"https://user:8443"
]
},
"user-v2": {
"https": [
"https://user-v2:8443"
]
}
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Fengling.Member.Domain\Fengling.Member.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,2 @@
global using Xunit;
global using NetCorePal.Extensions.Primitives;

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Fengling.Member.Infrastructure\Fengling.Member.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1 @@
global using Xunit;

View File

@ -0,0 +1,8 @@
//Ordering Tests In Collections see:https://fast-endpoints.com/docs/integration-unit-testing#ordering-tests-in-collections
// [assembly: EnableAdvancedTesting]
// can capture standard output and standard error
// [assembly: CaptureConsole]
// will capture output from Debug and Trace
// [assembly: CaptureTrace]

View File

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<None Remove="appsettings.Development.json" />
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Moq" />
<PackageReference Include="Testcontainers" />
<PackageReference Include="Testcontainers.PostgreSql" />
<PackageReference Include="Testcontainers.RabbitMq" />
<PackageReference Include="Testcontainers.Redis" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.CodeAnalysis" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FastEndpoints.Testing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Fengling.Member.Web\Fengling.Member.Web.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,54 @@
using Testcontainers.PostgreSql;
using Testcontainers.RabbitMq;
using Testcontainers.Redis;
using Microsoft.AspNetCore.Hosting;
using Fengling.Member.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace Fengling.Member.Web.Tests.Fixtures;
public class WebAppFixture : AppFixture<Program>
{
private RedisContainer _redisContainer = null!;
private RabbitMqContainer _rabbitMqContainer = null!;
private PostgreSqlContainer _databaseContainer = null!;
protected override async ValueTask PreSetupAsync()
{
_redisContainer = new RedisBuilder()
.WithCommand("--databases", "1024").Build();
_rabbitMqContainer = new RabbitMqBuilder()
.WithUsername("guest").WithPassword("guest").Build();
_databaseContainer = new PostgreSqlBuilder()
.WithUsername("postgres").WithPassword("123456")
.WithEnvironment("TZ", "Asia/Shanghai")
.WithDatabase("postgres").Build();
var tasks = new List<Task> { _redisContainer.StartAsync() };
tasks.Add(_rabbitMqContainer.StartAsync());
tasks.Add(_databaseContainer.StartAsync());
await Task.WhenAll(tasks);
await CreateVisualHostAsync("/");
}
protected override void ConfigureApp(IWebHostBuilder a)
{
a.UseSetting("ConnectionStrings:Redis",
_redisContainer.GetConnectionString());
a.UseSetting("ConnectionStrings:PostgreSQL",
_databaseContainer.GetConnectionString());
a.UseSetting("RabbitMQ:Port", _rabbitMqContainer.GetMappedPublicPort(5672).ToString());
a.UseSetting("RabbitMQ:UserName", "guest");
a.UseSetting("RabbitMQ:Password", "guest");
a.UseSetting("RabbitMQ:VirtualHost", "/");
a.UseSetting("RabbitMQ:HostName", _rabbitMqContainer.Hostname);
a.UseEnvironment("Development");
}
private async Task CreateVisualHostAsync(string visualHost)
{
await _rabbitMqContainer.ExecAsync(["rabbitmqctl", "add_vhost", visualHost]);
await _rabbitMqContainer.ExecAsync(["rabbitmqctl", "set_permissions", "-p", visualHost, "guest", ".*", ".*", ".*"
]);
}
}

View File

@ -0,0 +1,7 @@
namespace Fengling.Member.Web.Tests.Fixtures;
[CollectionDefinition(Name)]
public class WebAppTestCollection : TestCollection<WebAppFixture>
{
public const string Name = nameof(WebAppTestCollection);
}

View File

@ -0,0 +1,9 @@
global using Xunit;
global using Fengling.Member.Web.Tests.Fixtures;
global using FastEndpoints.Testing;
global using FastEndpoints;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection.Extensions;
global using NetCorePal.Extensions.NewtonsoftJson;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.Logging;

View File

@ -0,0 +1,5 @@
{
"parallelizeAssembly": true,
"parallelizeTestCollections": false,
"diagnosticMessages": false
}

View File

@ -0,0 +1,83 @@
# NetCorePal Template - Visual Studio Code Snippets Installer
# Auto install Visual Studio code snippets
param(
[string]$VisualStudioVersion = "2022",
[switch]$ShowPathOnly
)
$ErrorActionPreference = "Stop"
Write-Host "NetCorePal Template - Visual Studio Code Snippets Installer" -ForegroundColor Green
Write-Host "=================================================" -ForegroundColor Green
# Get current script directory
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$SnippetFile = Join-Path $ScriptDir "NetCorePalTemplates.snippet"
# Check if snippet file exists
if (-not (Test-Path $SnippetFile)) {
Write-Error "Snippet file not found: $SnippetFile"
exit 1
}
# Build Visual Studio snippets directory path
$VSSnippetsPath = "$env:USERPROFILE\Documents\Visual Studio $VisualStudioVersion\Code Snippets\Visual C#\My Code Snippets"
Write-Host "Target directory: $VSSnippetsPath" -ForegroundColor Yellow
# If only showing path, don't execute installation
if ($ShowPathOnly) {
Write-Host ""
Write-Host "Manual installation steps:" -ForegroundColor Cyan
Write-Host "1. Ensure target directory exists: $VSSnippetsPath" -ForegroundColor White
Write-Host "2. Copy file: $SnippetFile" -ForegroundColor White
Write-Host "3. To target directory: $VSSnippetsPath" -ForegroundColor White
Write-Host "4. Restart Visual Studio" -ForegroundColor White
Write-Host ""
Write-Host "Or use Tools > Code Snippets Manager > Import in Visual Studio" -ForegroundColor Yellow
return
}
# Create directory if it doesn't exist
if (-not (Test-Path $VSSnippetsPath)) {
Write-Host "Creating snippets directory..." -ForegroundColor Yellow
New-Item -ItemType Directory -Path $VSSnippetsPath -Force | Out-Null
}
# Copy snippet file
$DestinationFile = Join-Path $VSSnippetsPath "NetCorePalTemplates.snippet"
try {
Copy-Item -Path $SnippetFile -Destination $DestinationFile -Force
Write-Host "Code snippets installed successfully!" -ForegroundColor Green
Write-Host " Source file: $SnippetFile" -ForegroundColor Gray
Write-Host " Target file: $DestinationFile" -ForegroundColor Gray
Write-Host ""
Write-Host "Available snippet shortcuts:" -ForegroundColor Cyan
Write-Host " postproc - PostProcessor class" -ForegroundColor White
Write-Host " tstclass - Test class" -ForegroundColor White
Write-Host " ncpcmd - NetCorePal command" -ForegroundColor White
Write-Host " ncpcmdres - Command response" -ForegroundColor White
Write-Host " evnt - Domain event" -ForegroundColor White
Write-Host " ncprepo - Repository interface" -ForegroundColor White
Write-Host " epp - FastEndpoint" -ForegroundColor White
Write-Host ""
Write-Host "Usage:" -ForegroundColor Cyan
Write-Host "1. Open C# file in Visual Studio" -ForegroundColor White
Write-Host "2. Type shortcut (like 'postproc')" -ForegroundColor White
Write-Host "3. Press Tab key twice" -ForegroundColor White
Write-Host "4. Fill parameters and press Tab to switch to next parameter" -ForegroundColor White
Write-Host ""
Write-Host "Note: If Visual Studio is running, restart it to load new snippets." -ForegroundColor Yellow
}
catch {
Write-Error "Installation failed: $($_.Exception.Message)"
exit 1
}
Write-Host ""
Write-Host "Installation completed!" -ForegroundColor Green

File diff suppressed because it is too large Load Diff