skills/cleanddd-dotnet-coding/SKILL.md
在 CleanDDD 项目中落地已建模的需求(聚合/命令/查询/API 端点(Endpoints)/事件/仓储/配置/测试)的编码指南;用于编写或修改业务功能、端点与数据访问时
npx skillsauth add netcorepal/cleanddd-skills cleanddd-dotnet-codingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
public partial record,与实体/聚合同文件。{Entity}Id 同文件)。示例:User 聚合根与强类型 ID
namespace ProjectName.Domain.AggregatesModel.UserAggregate;
public partial record UserId : IGuidStronglyTypedId;
public class User : Entity<UserId>, IAggregateRoot
{
protected User() { }
public User(string name, string email)
{
Name = name;
Email = email;
this.AddDomainEvent(new UserCreatedDomainEvent(this));
}
public string Name { get; private set; } = string.Empty;
public string Email { get; private set; } = string.Empty;
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
// 一对多:User 拥有多个 RefreshToken(子实体有独立主键)
public ICollection<UserRefreshToken> RefreshTokens { get; private set; } = [];
public void ChangeEmail(string email)
{
Email = email;
this.AddDomainEvent(new UserEmailChangedDomainEvent(this));
}
public UserRefreshToken AddRefreshToken(string token)
{
var refreshToken = new UserRefreshToken(token);
RefreshTokens.Add(refreshToken);
return refreshToken;
}
}
示例:拥有独立主键的子实体(一对多,推荐继承 Entity<TId>)
namespace ProjectName.Domain.AggregatesModel.UserAggregate;
// 子实体推荐继承 Entity<TId>:定义强类型 ID,提供 protected 无参构造
public partial record UserRefreshTokenId : IInt64StronglyTypedId;
public class UserRefreshToken : Entity<UserRefreshTokenId>
{
protected UserRefreshToken() { }
public UserRefreshToken(string token)
{
Token = token;
ExpiresTime = DateTimeOffset.UtcNow.AddDays(1);
}
public UserId UserId { get; private set; } = null!; // 外键,EF 配置中用 HasForeignKey 关联
public string Token { get; private set; } = string.Empty;
public DateTimeOffset ExpiresTime { get; private set; }
public bool IsUsed { get; private set; }
public void Use()
{
if (IsUsed || ExpiresTime < DateTimeOffset.UtcNow)
throw new KnownException("无效的刷新令牌");
IsUsed = true;
}
}
示例:User 领域事件
namespace ProjectName.Domain.DomainEvents;
public record UserCreatedDomainEvent(User User) : IDomainEvent;
public record UserEmailChangedDomainEvent(User User) : IDomainEvent;
示例:创建用户命令
using ProjectName.Domain.AggregatesModel.UserAggregate;
namespace ProjectName.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().MaximumLength(50);
RuleFor(x => x.Email).NotEmpty().EmailAddress().MaximumLength(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;
}
}
示例:查询用户
using ProjectName.Domain.AggregatesModel.UserAggregate;
using ProjectName.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace ProjectName.Web.Application.Queries.Users;
public record GetUserQuery(UserId UserId) : IQuery<UserDto>;
public class GetUserQueryValidator : AbstractValidator<GetUserQuery>
{
public GetUserQueryValidator()
{
RuleFor(x => x.UserId).NotEmpty();
}
}
public class GetUserQueryHandler(ApplicationDbContext context)
: IQueryHandler<GetUserQuery, UserDto>
{
public async Task<UserDto> Handle(GetUserQuery request, CancellationToken cancellationToken)
{
return 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}");
}
}
public record UserDto(UserId Id, string Name, string Email);
示例:创建用户 API 端点(Endpoint)
using ProjectName.Domain.AggregatesModel.UserAggregate;
using ProjectName.Web.Application.Commands.Users;
using Microsoft.AspNetCore.Authorization;
namespace ProjectName.Web.Endpoints.Users;
public record CreateUserRequest(string Name, string Email);
public record CreateUserResponse(UserId UserId);
[Tags("Users")]
[HttpPost("/api/users")]
[AllowAnonymous]
public class CreateUserEndpoint(IMediator mediator)
: Endpoint<CreateUserRequest, ResponseData<CreateUserResponse>>
{
public override async Task HandleAsync(CreateUserRequest req, CancellationToken ct)
{
var id = await mediator.Send(new CreateUserCommand(req.Name, req.Email), ct);
await Send.OkAsync(new CreateUserResponse(id).AsResponseData(), ct);
}
}
示例:领域事件处理器触发命令
using ProjectName.Domain.DomainEvents;
using ProjectName.Web.Application.Commands.Users;
namespace ProjectName.Web.Application.DomainEventHandlers;
public class UserCreatedDomainEventHandlerForSendWelcome(IMediator mediator)
: IDomainEventHandler<UserCreatedDomainEvent>
{
public async Task Handle(UserCreatedDomainEvent domainEvent, CancellationToken cancellationToken)
{
var command = new SendWelcomeEmailCommand(domainEvent.User.Id, domainEvent.User.Email, domainEvent.User.Name);
await mediator.Send(command, cancellationToken);
}
}
示例:用户仓储
using ProjectName.Domain.AggregatesModel.UserAggregate;
namespace ProjectName.Infrastructure.Repositories;
public interface IUserRepository : IRepository<User, UserId>
{
Task<bool> EmailExistsAsync(string email, CancellationToken cancellationToken = default);
}
public class UserRepository(ApplicationDbContext context)
: RepositoryBase<User, UserId, ApplicationDbContext>(context), IUserRepository
{
public async Task<bool> EmailExistsAsync(string email, CancellationToken cancellationToken = default)
{
return await DbContext.Users.AnyAsync(x => x.Email == email, cancellationToken);
}
}
示例:实体配置(聚合根 + 子实体一对多)
// 聚合根配置
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.HasMany(u => u.RefreshTokens).WithOne().HasForeignKey("UserId").OnDelete(DeleteBehavior.Cascade);
}
}
// 子实体配置
internal class UserRefreshTokenEntityTypeConfiguration : IEntityTypeConfiguration<UserRefreshToken>
{
public void Configure(EntityTypeBuilder<UserRefreshToken> builder)
{
builder.ToTable("user_refresh_token");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).UseSnowFlakeValueGenerator().HasComment("刷新令牌标识");
builder.Property(x => x.Token).HasMaxLength(500).IsRequired().HasComment("令牌");
}
}
public DbSet<T> Name => Set<T>();)。示例:DbSet 注册
using ProjectName.Domain.AggregatesModel.UserAggregate;
namespace ProjectName.Infrastructure;
public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IMediator mediator)
: AppDbContextBase(options, mediator)
{
public DbSet<User> Users => Set<User>();
}
示例:用户创建集成事件
using ProjectName.Domain.AggregatesModel.UserAggregate;
namespace ProjectName.Web.Application.IntegrationEvents;
public record UserCreatedIntegrationEvent(UserId UserId, string Name, string Email, DateTime CreatedTime);
示例:领域事件到集成事件
using ProjectName.Domain.DomainEvents;
using ProjectName.Web.Application.IntegrationEvents;
namespace ProjectName.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);
}
}
示例:处理集成事件
using ProjectName.Web.Application.Commands.Users;
using ProjectName.Web.Application.IntegrationEvents;
namespace ProjectName.Web.Application.IntegrationEventHandlers;
public class UserCreatedIntegrationEventHandlerForSendWelcomeEmail(
ILogger<UserCreatedIntegrationEventHandlerForSendWelcomeEmail> logger,
IMediator mediator)
: IIntegrationEventHandler<UserCreatedIntegrationEvent>
{
public async Task HandleAsync(UserCreatedIntegrationEvent integrationEvent, CancellationToken cancellationToken)
{
logger.LogInformation("发送欢迎邮件:{UserId}", integrationEvent.UserId);
var command = new SendWelcomeEmailCommand(integrationEvent.UserId, integrationEvent.Email, integrationEvent.Name);
await mediator.Send(command, cancellationToken);
}
}
示例:聚合单测
public class UserTests
{
[Fact]
public void Constructor_ShouldRaiseCreatedEvent()
{
var user = new User("Alice", "[email protected]");
Assert.Equal("Alice", user.Name);
Assert.Single(user.GetDomainEvents());
Assert.IsType<UserCreatedDomainEvent>(user.GetDomainEvents().First());
}
[Fact]
public void ChangeEmail_ShouldRaiseChangedEvent()
{
var user = new User("Bob", "[email protected]");
user.ClearDomainEvents();
user.ChangeEmail("[email protected]");
Assert.Equal("[email protected]", user.Email);
Assert.IsType<UserEmailChangedDomainEvent>(user.GetDomainEvents().Single());
}
}
if (Paid) throw new KnownException("Order has been paid");
var order = await orderRepository.GetAsync(request.OrderId, cancellationToken)
?? throw new KnownException($"未找到订单,OrderId = {request.OrderId}");
order.OrderPaid();
research
需求澄清与拆解,识别干系人并将需求条目标注所属对象与业务实体;仅产出结构化需求描述,不做建模定义,可作为后续建模输入
development
基于 cleanddd-requirements-analysis 结果,输出符合 CleanDDD 的聚合、命令、事件、查询、API 端点(Endpoints)、定时任务模型;用于从已拆解的需求快速得到建模蓝图
development
初始化 CleanDDD dotnet 项目(netcorepal-web 模板);在创建新项目或为客户演示快速拉起解决方案时使用,可直接调用 dotnet new 或包装脚本
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.