mcpField
D
dotnet-cqrs-command-generator

dotnet-cqrs-command-generator

github.com/ronnythedev开发7763 次安装15 周前更新
代码仓库

关于 dotnet-cqrs-command-generator

为 .NET 开发提供 CQRS 命令生成指南,涵盖命令记录、验证器、处理器及可选请求 DTO,并强调 Result 模式与职责分离。

主要功能

  1. 01命令记录与处理器模板
  2. 02FluentValidation 输入规则
  3. 03支持有返回值及无返回值命令
  4. 04复杂请求对象与批量操作模式
  5. 05事务及错误处理指导

适用场景

  1. 01为创建、更新和删除操作设计命令
  2. 02为命令添加输入验证与处理器
  3. 03构建批量或事务性写入流程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ronnythedev/dotnet-clean-architecture-skills --skill dotnet-cqrs-command-generator

安装后在 Claude Code 中输入/dotnet-cqrs-command-generator确认已加载。

详细介绍

CQRS 命令生成器

概述

此技能可按照 CQRS(命令查询职责分离)模式生成命令。命令代表更改系统状态的意图。每个命令包含:

  • 命令记录 - 包含请求参数的不可变数据结构
  • 验证器 - 用于输入验证的 FluentValidation 规则
  • 处理器 - 返回 Result 的业务逻辑实现
  • 请求 DTO(可选)- API 层的请求模型

快速参考

命令类型返回值使用场景
ICommandResult不返回值的操作(更新、删除)
ICommand<T>Result<T>返回数据的操作(创建时返回 Id)

命令结构

/Application/{Feature}/
├── Create{Entity}/
│   ├── Create{Entity}Command.cs        # Record + Validator + Handler
│   └── Create{Entity}Request.cs        # Optional API DTO
├── Update{Entity}/
│   ├── Update{Entity}Command.cs
│   └── Update{Entity}Request.cs
└── Delete{Entity}/
    └── Delete{Entity}Command.cs

模板:带返回值的命令(创建)

用于返回数据的操作(通常是在创建后返回实体 ID)。

// src/{name}.application/{Feature}/Create{Entity}/Create{Entity}Command.cs
using FluentValidation;
using {name}.application.abstractions.clock;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;
using {name}.domain.{entities};

namespace {name}.application.{feature}.Create{Entity};

// ═══════════════════════════════════════════════════════════════
// COMMAND RECORD
// ═══════════════════════════════════════════════════════════════
public sealed record Create{Entity}Command(
    string Name,
    string? Description,
    Guid? ParentId) : ICommand<Guid>;

// ═══════════════════════════════════════════════════════════════
// VALIDATOR
// ═══════════════════════════════════════════════════════════════
internal sealed class Create{Entity}CommandValidator : AbstractValidator<Create{Entity}Command>
{
    public Create{Entity}CommandValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .WithMessage("{Entity} name is required")
            .MaximumLength(100)
            .WithMessage("{Entity} name must not exceed 100 characters");

        RuleFor(x => x.Description)
            .MaximumLength(500)
            .When(x => x.Description is not null);
    }
}

// ═══════════════════════════════════════════════════════════════
// HANDLER
// ═══════════════════════════════════════════════════════════════
internal sealed class Create{Entity}CommandHandler 
    : ICommandHandler<Create{Entity}Command, Guid>
{
    private readonly I{Entity}Repository _{entity}Repository;
    private readonly IDateTimeProvider _dateTimeProvider;
    private readonly IUnitOfWork _unitOfWork;

    public Create{Entity}CommandHandler(
        I{Entity}Repository {entity}Repository,
        IDateTimeProvider dateTimeProvider,
        IUnitOfWork unitOfWork)
    {
        _{entity}Repository = {entity}Repository;
        _dateTimeProvider = dateTimeProvider;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result<Guid>> Handle(
        Create{Entity}Command request,
        CancellationToken cancellationToken)
    {
        // 1. Validate business rules
        var existingEntity = await _{entity}Repository
            .GetByNameAsync(request.Name, cancellationToken);

        if (existingEntity is not null)
        {
            return Result.Failure<Guid>({Entity}Errors.AlreadyExists);
        }

        // 2. Create domain entity using factory method
        var {entity}Result = {Entity}.Create(
            request.Name,
            request.Description,
            _dateTimeProvider.UtcNow);

        if ({entity}Result.IsFailure)
        {
            return Result.Failure<Guid>({entity}Result.Error);
        }

        // 3. Persist to repository
        _{entity}Repository.Add({entity}Result.Value);

        // 4. Save changes (via Unit of Work)
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        // 5. Return created entity ID
        return {entity}Result.Value.Id;
    }
}

模板:不带返回值的命令(更新)

用于不返回数据的操作。

// src/{name}.application/{Feature}/Update{Entity}/Update{Entity}Command.cs
using FluentValidation;
using {name}.application.abstractions.clock;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;
using {name}.domain.{entities};

namespace {name}.application.{feature}.Update{Entity};

// ═══════════════════════════════════════════════════════════════
// COMMAND RECORD
// ═══════════════════════════════════════════════════════════════
public sealed record Update{Entity}Command(
    Guid Id,
    string Name,
    string? Description) : ICommand;

// ═══════════════════════════════════════════════════════════════
// VALIDATOR
// ═══════════════════════════════════════════════════════════════
internal sealed class Update{Entity}CommandValidator : AbstractValidator<Update{Entity}Command>
{
    public Update{Entity}CommandValidator()
    {
        RuleFor(x => x.Id)
            .NotEmpty()
            .WithMessage("{Entity} ID is required");

        RuleFor(x => x.Name)
            .NotEmpty()
            .MaximumLength(100);
    }
}

// ═══════════════════════════════════════════════════════════════
// HANDLER
// ═══════════════════════════════════════════════════════════════
internal sealed class Update{Entity}CommandHandler 
    : ICommandHandler<Update{Entity}Command>
{
    private readonly I{Entity}Repository _{entity}Repository;
    private readonly IDateTimeProvider _dateTimeProvider;
    private readonly IUnitOfWork _unitOfWork;

    public Update{Entity}CommandHandler(
        I{Entity}Repository {entity}Repository,
        IDateTimeProvider dateTimeProvider,
        IUnitOfWork unitOfWork)
    {
        _{entity}Repository = {entity}Repository;
        _dateTimeProvider = dateTimeProvider;
        _unitOfWork = unitOfWork;
    }

    public async Task<r> Handle(
        Update{Entity}Command request,
        CancellationToken cancellationToken)
    {
        // 1. Retrieve existing entity
        var {entity} = await _{entity}Repository
            .GetByIdAsync(request.Id, cancellationToken);

        if ({entity} is null)
        {
            return Result.Failure({Entity}Errors.NotFound);
        }

        // 2. Call domain method to update
        var updateResult = {entity}.Update(
            request.Name,
            request.Description,
            _dateTimeProvider.UtcNow);

        if (updateResult.IsFailure)
        {
            return Result.Failure(updateResult.Error);
        }

        // 3. Save changes
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return Result.Success();
    }
}

模板:删除命令

// src/{name}.application/{Feature}/Delete{Entity}/Delete{Entity}Command.cs
using FluentValidation;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;
using {name}.domain.{entities};

namespace {name}.application.{feature}.Delete{Entity};

public sealed record Delete{Entity}Command(Guid Id) : ICommand;

internal sealed class Delete{Entity}CommandValidator : AbstractValidator<Delete{Entity}Command>
{
    public Delete{Entity}CommandValidator()
    {
        RuleFor(x => x.Id).NotEmpty();
    }
}

internal sealed class Delete{Entity}CommandHandler 
    : ICommandHandler<Delete{Entity}Command>
{
    private readonly I{Entity}Repository _{entity}Repository;
    private readonly IUnitOfWork _unitOfWork;

    public Delete{Entity}CommandHandler(
        I{Entity}Repository {entity}Repository,
        IUnitOfWork unitOfWork)
    {
        _{entity}Repository = {entity}Repository;
        _unitOfWork = unitOfWork;
    }

    public async Task<r> Handle(
        Delete{Entity}Command request,
        CancellationToken cancellationToken)
    {
        var {entity} = await _{entity}Repository
            .GetByIdAsync(request.Id, cancellationToken);

        if ({entity} is null)
        {
            return Result.Failure({Entity}Errors.NotFound);
        }

        // Check business rules before deletion
        if ({entity}.HasActiveRelationships())
        {
            return Result.Failure({Entity}Errors.CannotDeleteWithActiveRelationships);
        }

        _{entity}Repository.Remove({entity});

        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return Result.Success();
    }
}

模板:带复杂请求对象的命令

对于包含许多参数的命令,请使用嵌套请求对象。

// src/{name}.application/{Feature}/Create{Entity}/Create{Entity}Command.cs
using FluentValidation;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;

namespace {name}.application.{feature}.Create{Entity};

// Request object for complex data
public sealed class Create{Entity}Request
{
    public required string Name { get; init; }
    public string? Description { get; init; }
    public required Guid OrganizationId { get; init; }
    public List<Create{Child}Request> Children { get; init; } = new();
}

public sealed class Create{Child}Request
{
    public required string Name { get; init; }
    public int SortOrder { get; init; }
}

// Command wraps the request
public sealed record Create{Entity}Command(
    Create{Entity}Request Request) : ICommand<Guid>;

// Validator for nested structures
internal sealed class Create{Entity}CommandValidator 
    : AbstractValidator<Create{Entity}Command>
{
    public Create{Entity}CommandValidator()
    {
        RuleFor(x => x.Request.Name)
            .NotEmpty()
            .MaximumLength(100);

        RuleFor(x => x.Request.OrganizationId)
            .NotEmpty();

        RuleForEach(x => x.Request.Children)
            .ChildRules(child =>
            {
                child.RuleFor(c => c.Name).NotEmpty();
                child.RuleFor(c => c.SortOrder).GreaterThanOrEqualTo(0);
            });
    }
}

// Handler processes the complex request
internal sealed class Create{Entity}CommandHandler 
    : ICommandHandler<Create{Entity}Command, Guid>
{
    // ... dependencies

    public async Task<Result<Guid>> Handle(
        Create{Entity}Command command,
        CancellationToken cancellationToken)
    {
        var request = command.Request;
        
        // Process parent entity
        var {entity} = {Entity}.Create(
            request.Name,
            request.Description,
            request.OrganizationId);

        // Process children
        foreach (var childRequest in request.Children)
        {
            var child = {Child}.Create(
                childRequest.Name,
                childRequest.SortOrder);
            
            {entity}.AddChild(child);
        }

        _{entity}Repository.Add({entity});
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return {entity}.Id;
    }
}

验证规则参考

常见验证器
// String validations
RuleFor(x => x.Name)
    .NotEmpty().WithMessage("Name is required")
    .NotNull().WithMessage("Name cannot be null")
    .MaximumLength(100).WithMessage("Name too long")
    .MinimumLength(3).WithMessage("Name too short")
    .Matches("^[a-zA-Z]+$").WithMessage("Only letters allowed");

// Numeric validations
RuleFor(x => x.Amount)
    .GreaterThan(0).WithMessage("Must be positive")
    .LessThanOrEqualTo(1000).WithMessage("Max 1000")
    .InclusiveBetween(1, 100).WithMessage("Must be 1-100");

// GUID validations
RuleFor(x => x.Id)
    .NotEmpty().WithMessage("ID is required")
    .NotEqual(Guid.Empty).WithMessage("Invalid ID");

// Email validation
RuleFor(x => x.Email)
    .NotEmpty()
    .EmailAddress().WithMessage("Invalid email format");

// Conditional validation
RuleFor(x => x.ParentId)
    .NotEmpty()
    .When(x => x.RequiresParent);

// Collection validation
RuleFor(x => x.Items)
    .NotEmpty().WithMessage("At least one item required")
    .Must(items => items.Count <= 10).WithMessage("Max 10 items");

// Custom validation
RuleFor(x => x.DateRange)
    .Must(BeValidDateRange).WithMessage("End date must be after start date");

private bool BeValidDateRange(DateRange range) => range.End > range.Start;
异步验证(谨慎使用)
internal sealed class Create{Entity}CommandValidator 
    : AbstractValidator<Create{Entity}Command>
{
    private readonly I{Entity}Repository _{entity}Repository;

    public Create{Entity}CommandValidator(I{Entity}Repository {entity}Repository)
    {
        _{entity}Repository = {entity}Repository;

        RuleFor(x => x.Name)
            .MustAsync(BeUniqueName)
            .WithMessage("Name already exists");
    }

    private async Task<bool> BeUniqueName(string name, CancellationToken ct)
    {
        var existing = await _{entity}Repository.GetByNameAsync(name, ct);
        return existing is null;
    }
}

注意:为更好地分离关注点,优先在处理器中而非验证器中执行存在性检查。


处理器模式

模式 1:单实体操作
public async Task<Result<Guid>> Handle(CreateCommand request, CancellationToken ct)
{
    // Create
    var entity = Entity.Create(request.Data);
    _repository.Add(entity);
    await _unitOfWork.SaveChangesAsync(ct);
    return entity.Id;
}
模式 2:涉及相关实体
public async Task<Result<Guid>> Handle(CreateCommand request, CancellationToken ct)
{
    // Load related entity
    var parent = await _parentRepository.GetByIdAsync(request.ParentId, ct);
    if (parent is null)
        return Result.Failure<Guid>(ParentErrors.NotFound);

    // Create with relationship
    var entity = Entity.Create(request.Data, parent);
    _repository.Add(entity);
    await _unitOfWork.SaveChangesAsync(ct);
    return entity.Id;
}
模式 3:批量操作
public async Task<r> Handle(CreateBatchCommand request, CancellationToken ct)
{
    var entities = new List<Entity>();

    foreach (var item in request.Items)
    {
        var entityResult = Entity.Create(item);
        if (entityResult.IsFailure)
            return Result.Failure(entityResult.Error);
        
        entities.Add(entityResult.Value);
    }

    _repository.AddRange(entities);
    await _unitOfWork.SaveChangesAsync(ct);
    return Result.Success();
}
模式 4:使用事务
public async Task<r> Handle(ComplexCommand request, CancellationToken ct)
{
    using var transaction = await _unitOfWork.BeginTransactionAsync(ct);
    
    try
    {
        // Multiple operations
        var entity1 = await CreateEntity1(request);
        var entity2 = await CreateEntity2(request, entity1);
        
        await _unitOfWork.SaveChangesAsync(ct);
        await transaction.CommitAsync(ct);
        
        return Result.Success();
    }
    catch
    {
        await transaction.RollbackAsync(ct);
        throw;
    }
}

关键规则

  1. 命令是记录 - 不可变,具有值相等性
  2. 每个命令对应一个处理器 - 不要共享处理器
  3. 验证器为内部实现 - 不向 Application 层之外公开
  4. 使用 Result 模式 - 业务错误绝不抛出异常
  5. 注入 IUnitOfWork - 不要在存储库中调用 SaveChanges
  6. 始终使用 CancellationToken - 传递给所有异步调用
  7. 领域逻辑放在 Domain 中 - 处理器负责协调,不包含业务规则
  8. 创建时返回 ID - 创建操作使用 ICommand<Guid>
  9. 按顺序验证 - 先检查是否存在,再创建,然后验证业务规则
  10. 保持处理器专注 - 每个处理器只负责一项职责

应避免的反模式

// ❌ WRONG: Throwing exceptions for business errors
if (entity is null)
    throw new NotFoundException("Entity not found");

// ✅ CORRECT: Return Result
if (entity is null)
    return Result.Failure<Guid>(EntityErrors.NotFound);

// ❌ WRONG: Saving in repository
public void Add(Entity entity)
{
    _dbContext.Add(entity);
    _dbContext.SaveChanges(); // Don't do this!
}

// ✅ CORRECT: Handler calls SaveChanges via UnitOfWork
_repository.Add(entity);
await _unitOfWork.SaveChangesAsync(ct);

// ❌ WRONG: Business logic in handler
if (request.Amount > 1000 && user.Level < 5)
    return Result.Failure(Error.InsufficientLevel);

// ✅ CORRECT: Business logic in domain
var result = entity.ProcessOrder(request.Amount, user);
if (result.IsFailure)
    return Result.Failure(result.Error);

相关技能

  • dotnet-cqrs-query-generator - 生成读取端查询
  • dotnet-domain-entity-generator - 使用工厂方法生成领域实体
  • dotnet-result-pattern - 完整实现 Result 模式
  • dotnet-pipeline-behaviors - 验证和日志记录行为

评论

支持 Markdown