mcpField
D
dotnet-legacy-api-controllers

dotnet-legacy-api-controllers

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

关于 dotnet-legacy-api-controllers

用于生成 .NET RESTful API 控制器的技能,提供 CRUD 和复杂操作模板,涵盖 MediatR 的 ISender、API 版本控制、授权、DTO 及 HTTP 响应约定。

主要功能

  1. 01CRUD 控制器模板
  2. 02集成 MediatR ISender
  3. 03基于 URL 段的 API 版本控制
  4. 04基于角色和权限的授权
  5. 05请求与响应 DTO
  6. 06HTTP 状态码约定

适用场景

  1. 01创建带版本控制的 REST 端点
  2. 02将控制器连接到命令和查询
  3. 03为 API 操作配置授权

安装

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

$npx skills add https://github.com/ronnythedev/dotnet-clean-architecture-skills --skill dotnet-legacy-api-controllers

安装后在 Claude Code 中输入/dotnet-legacy-api-controllers确认已加载。

详细介绍

API 控制器生成器

概述

此技能可遵循最佳实践生成 RESTful API 控制器:

  • MediatR 集成 - 通过 ISender 发送命令/查询
  • API 版本控制 - URL 段版本控制
  • 授权 - 基于角色和权限
  • 一致的响应 - 使用恰当的 HTTP 状态码
  • 请求/响应 DTO - 与领域层分离

快速参考

HTTP 方法操作返回值
GET /{id}按 ID 获取200 OK / 404 Not Found
GET /获取全部/列表200 OK
POST /创建201 Created / 400 Bad Request
PUT /{id}全量更新200 OK / 404 Not Found
PATCH /{id}部分更新200 OK / 404 Not Found
DELETE /{id}删除204 No Content / 404 Not Found

控制器结构

/API/Controllers/
├── {Feature}/
│   ├── {Entity}Controller.cs
│   ├── Request{Action}{Entity}.cs
│   └── ...
└── ...

模板:完整 CRUD 控制器

// src/{name}.api/Controllers/{Feature}/{Entity}Controller.cs
using Asp.Versioning;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using {name}.application.{feature}.Create{Entity};
using {name}.application.{feature}.Delete{Entity};
using {name}.application.{feature}.Get{Entity}ById;
using {name}.application.{feature}.Get{Entities};
using {name}.application.{feature}.Update{Entity};
using {name}.infrastructure.authorization;

namespace {name}.api.Controllers.{Feature};

[Authorize]
[ApiController]
[ApiVersion(ApiVersions.V1)]
[Route("api/v{version:apiVersion}/{entities}")]
public class {Entity}Controller : ControllerBase
{
    private readonly ISender _sender;

    public {Entity}Controller(ISender sender)
    {
        _sender = sender;
    }

    // ═══════════════════════════════════════════════════════════════
    // GET: api/v1/{entities}/{id}
    // ═══════════════════════════════════════════════════════════════
    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof({Entity}Response), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(
        Guid id,
        CancellationToken cancellationToken)
    {
        var query = new Get{Entity}ByIdQuery(id);

        var result = await _sender.Send(query, cancellationToken);

        if (result.IsFailure)
        {
            return NotFound(result.Error);
        }

        return Ok(result.Value);
    }

    // ═══════════════════════════════════════════════════════════════
    // GET: api/v1/{entities}
    // ═══════════════════════════════════════════════════════════════
    [HttpGet]
    [ProducesResponseType(typeof(IReadOnlyList<{Entity}ListResponse>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll(CancellationToken cancellationToken)
    {
        var query = new GetAll{Entities}Query();

        var result = await _sender.Send(query, cancellationToken);

        return Ok(result.Value);
    }

    // ═══════════════════════════════════════════════════════════════
    // GET: api/v1/{entities}/organization/{organizationId}
    // ═══════════════════════════════════════════════════════════════
    [HttpGet("organization/{organizationId:guid}")]
    [HasPermission(Permissions.{Entities}Read)]
    [ProducesResponseType(typeof(IReadOnlyList<{Entity}Response>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetByOrganizationId(
        Guid organizationId,
        CancellationToken cancellationToken)
    {
        var query = new Get{Entities}ByOrganizationIdQuery(organizationId);

        var result = await _sender.Send(query, cancellationToken);

        return Ok(result.Value);
    }

    // ═══════════════════════════════════════════════════════════════
    // POST: api/v1/{entities}
    // ═══════════════════════════════════════════════════════════════
    [HttpPost]
    [HasPermission(Permissions.{Entities}Write)]
    [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create(
        [FromBody] RequestCreate{Entity} request,
        CancellationToken cancellationToken)
    {
        var command = new Create{Entity}Command(
            request.Name,
            request.Description,
            request.OrganizationId);

        var result = await _sender.Send(command, cancellationToken);

        if (result.IsFailure)
        {
            return BadRequest(result.Error);
        }

        return CreatedAtAction(
            nameof(GetById),
            new { id = result.Value },
            result.Value);
    }

    // ═══════════════════════════════════════════════════════════════
    // PUT: api/v1/{entities}/{id}
    // ═══════════════════════════════════════════════════════════════
    [HttpPut("{id:guid}")]
    [HasPermission(Permissions.{Entities}Write)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Update(
        Guid id,
        [FromBody] RequestUpdate{Entity} request,
        CancellationToken cancellationToken)
    {
        var command = new Update{Entity}Command(
            id,
            request.Name,
            request.Description);

        var result = await _sender.Send(command, cancellationToken);

        if (result.IsFailure)
        {
            return result.Error.Code.Contains("NotFound")
                ? NotFound(result.Error)
                : BadRequest(result.Error);
        }

        return Ok();
    }

    // ═══════════════════════════════════════════════════════════════
    // PATCH: api/v1/{entities}/{id}
    // ═══════════════════════════════════════════════════════════════
    [HttpPatch("{id:guid}")]
    [HasPermission(Permissions.{Entities}Write)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> PartialUpdate(
        Guid id,
        [FromBody] RequestPatch{Entity} request,
        CancellationToken cancellationToken)
    {
        var command = new Patch{Entity}Command(id, request);

        var result = await _sender.Send(command, cancellationToken);

        if (result.IsFailure)
        {
            return result.Error.Code.Contains("NotFound")
                ? NotFound(result.Error)
                : BadRequest(result.Error);
        }

        return Ok();
    }

    // ═══════════════════════════════════════════════════════════════
    // DELETE: api/v1/{entities}/{id}
    // ═══════════════════════════════════════════════════════════════
    [HttpDelete("{id:guid}")]
    [HasPermission(Permissions.{Entities}Write)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Delete(
        Guid id,
        CancellationToken cancellationToken)
    {
        var command = new Delete{Entity}Command(id);

        var result = await _sender.Send(command, cancellationToken);

        if (result.IsFailure)
        {
            return result.Error.Code.Contains("NotFound")
                ? NotFound(result.Error)
                : BadRequest(result.Error);
        }

        return NoContent();
    }
}

模板:请求 DTO

// src/{name}.api/Controllers/{Feature}/RequestCreate{Entity}.cs
namespace {name}.api.Controllers.{Feature};

public sealed class RequestCreate{Entity}
{
    public required string Name { get; init; }
    public string? Description { get; init; }
    public Guid OrganizationId { get; init; }
}

// src/{name}.api/Controllers/{Feature}/RequestUpdate{Entity}.cs
public sealed class RequestUpdate{Entity}
{
    public required string Name { get; init; }
    public string? Description { get; init; }
}

// src/{name}.api/Controllers/{Feature}/RequestPatch{Entity}.cs
public sealed class RequestPatch{Entity}
{
    public string? Name { get; init; }
    public string? Description { get; init; }
    public bool? IsActive { get; init; }
}

模板:包含复杂操作的控制器

// src/{name}.api/Controllers/{Feature}/{Entity}Controller.cs
[Authorize]
[ApiController]
[ApiVersion(ApiVersions.V1)]
[Route("api/v{version:apiVersion}/{entities}")]
public class {Entity}Controller : ControllerBase
{
    private readonly ISender _sender;
    private readonly IConfiguration _configuration;

    public {Entity}Controller(ISender sender, IConfiguration configuration)
    {
        _sender = sender;
        _configuration = configuration;
    }

    // ═══════════════════════════════════════════════════════════════
    // POST: api/v1/{entities}/batch
    // ═══════════════════════════════════════════════════════════════
    [HttpPost("batch")]
    [HasPermission(Permissions.{Entities}Write)]
    public async Task<IActionResult> CreateBatch(
        [FromBody] RequestCreateBatch{Entity} request,
        CancellationToken cancellationToken)
    {
        var command = new CreateBatch{Entity}Command(request);

        var result = await _sender.Send(command, cancellationToken);

        if (result.IsFailure)
        {
            return BadRequest(result.Error);
        }

        return Ok(result.Value);
    }

    // ═══════════════════════════════════════════════════════════════
    // POST: api/v1/{entities}/{id}/activate
    // ═══════════════════════════════════════════════════════════════
    [HttpPost("{id:guid}/activate")]
    [HasPermission(Permissions.{Entities}Write)]
    public async Task<IActionResult> Activate(
        Guid id,
        CancellationToken cancellationToken)
    {
        var command = new Activate{Entity}Command(id);

        var result = await _sender.Send(command, cancellationToken);

        if (result.IsFailure)
        {
            return BadRequest(result.Error);
        }

        return Ok();
    }

    // ═══════════════════════════════════════════════════════════════
    // POST: api/v1/{entities}/{id}/deactivate
    // ═══════════════════════════════════════════════════════════════
    [HttpPost("{id:guid}/deactivate")]
    [HasPermission(Permissions.{Entities}Write)]
    public async Task<IActionResult> Deactivate(
        Guid id,
        CancellationToken cancellationToken)
    {
        var command = new Deactivate{Entity}Command(id);

        var result = await _sender.Send(command, cancellationToken);

        if (result.IsFailure)
        {
            return BadRequest(result.Error);
        }

        return Ok();
    }

    // ═══════════════════════════════════════════════════════════════
    // GET: api/v1/{entities}/search
    // ═══════════════════════════════════════════════════════════════
    [HttpGet("search")]
    public async Task<IActionResult> Search(
        [FromQuery] string? term,
        [FromQuery] int pageNumber = 1,
        [FromQuery] int pageSize = 10,
        CancellationToken cancellationToken = default)
    {
        var query = new Search{Entities}Query(term, pageNumber, pageSize);

        var result = await _sender.Send(query, cancellationToken);

        return Ok(result.Value);
    }

    // ═══════════════════════════════════════════════════════════════
    // POST: api/v1/{entities}/{parentId}/children
    // ═══════════════════════════════════════════════════════════════
    [HttpPost("{parentId:guid}/children")]
    [HasPermission(Permissions.{Entities}Write)]
    public async Task<IActionResult> AddChild(
        Guid parentId,
        [FromBody] RequestAddChild request,
        CancellationToken cancellationToken)
    {
        var command = new AddChildCommand(parentId, request.Name, request.SortOrder);

        var result = await _sender.Send(command, cancellationToken);

        if (result.IsFailure)
        {
            return BadRequest(result.Error);
        }

        return Created($"api/v1/{entities}/{parentId}/children/{result.Value}", result.Value);
    }

    // ═══════════════════════════════════════════════════════════════
    // DELETE: api/v1/{entities}/{parentId}/children/{childId}
    // ═══════════════════════════════════════════════════════════════
    [HttpDelete("{parentId:guid}/children/{childId:guid}")]
    [HasPermission(Permissions.{Entities}Write)]
    public async Task<IActionResult> RemoveChild(
        Guid parentId,
        Guid childId,
        CancellationToken cancellationToken)
    {
        var command = new RemoveChildCommand(parentId, childId);

        var result = await _sender.Send(command, cancellationToken);

        if (result.IsFailure)
        {
            return BadRequest(result.Error);
        }

        return NoContent();
    }
}

模板:基于角色授权的控制器

// src/{name}.api/Controllers/{Feature}/{Entity}Controller.cs
[Authorize]
[ApiController]
[ApiVersion(ApiVersions.V1)]
[Route("api/v{version:apiVersion}/{entities}")]
public class {Entity}Controller : ControllerBase
{
    private readonly ISender _sender;

    public {Entity}Controller(ISender sender)
    {
        _sender = sender;
    }

    // Public endpoint (no specific role required, just authenticated)
    [HttpGet("{id:guid}")]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        // ...
    }

    // Multiple roles allowed
    [HttpPost]
    [Authorize(Roles = Roles.SuperAdmin + "," + Roles.Manager)]
    public async Task<IActionResult> Create(
        [FromBody] RequestCreate{Entity} request,
        CancellationToken ct)
    {
        // ...
    }

    // Only super admin
    [HttpDelete("{id:guid}")]
    [Authorize(Roles = Roles.SuperAdmin)]
    public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
    {
        // ...
    }

    // Permission-based (custom attribute)
    [HttpPut("{id:guid}")]
    [HasPermission(Permissions.{Entities}Write)]
    public async Task<IActionResult> Update(
        Guid id,
        [FromBody] RequestUpdate{Entity} request,
        CancellationToken ct)
    {
        // ...
    }

    // Anonymous endpoint
    [HttpGet("public")]
    [AllowAnonymous]
    public async Task<IActionResult> GetPublicData(CancellationToken ct)
    {
        // ...
    }
}

API 版本控制设置

// src/{name}.api/ApiVersions.cs
namespace {name}.api;

public static class ApiVersions
{
    public const string V1 = "1.0";
    public const string V2 = "2.0";
}

// src/{name}.infrastructure/DependencyInjection.cs
private static void AddApiVersioning(IServiceCollection services)
{
    services.AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(1);
        options.ReportApiVersions = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
        options.AssumeDefaultVersionWhenUnspecified = true;
    })
    .AddMvc();
}

基于权限的授权

// src/{name}.infrastructure/Authorization/Permissions.cs
namespace {name}.infrastructure.authorization;

public static class Permissions
{
    // Organizations
    public const string OrganizationsRead = "organizations:read";
    public const string OrganizationsWrite = "organizations:write";

    // Users
    public const string UsersRead = "users:read";
    public const string UsersWrite = "users:write";

    // {Entities}
    public const string {Entities}Read = "{entities}:read";
    public const string {Entities}Write = "{entities}:write";
}

// src/{name}.infrastructure/Authorization/Roles.cs
namespace {name}.infrastructure.authorization;

public static class Roles
{
    public const string SuperAdmin = "SuperAdmin";
    public const string Admin = "Admin";
    public const string Manager = "Manager";
    public const string Consultant = "Consultant";
    public const string Associate = "Associate";
}

// src/{name}.infrastructure/Authorization/HasPermissionAttribute.cs
using Microsoft.AspNetCore.Authorization;

namespace {name}.infrastructure.authorization;

public sealed class HasPermissionAttribute : AuthorizeAttribute
{
    public HasPermissionAttribute(string permission) : base(permission)
    {
    }
}

全局错误处理

// src/{name}.api/Middleware/ExceptionHandlingMiddleware.cs
using {name}.application.exceptions;
using Microsoft.AspNetCore.Mvc;

namespace {name}.api.Middleware;

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(
        RequestDelegate next,
        ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception exception)
        {
            _logger.LogError(exception, "Exception occurred: {Message}", exception.Message);

            var problemDetails = CreateProblemDetails(exception);

            context.Response.StatusCode = problemDetails.Status ?? 500;
            await context.Response.WriteAsJsonAsync(problemDetails);
        }
    }

    private static ProblemDetails CreateProblemDetails(Exception exception)
    {
        return exception switch
        {
            ValidationException validationException => new ProblemDetails
            {
                Status = StatusCodes.Status400BadRequest,
                Title = "Validation Error",
                Detail = "One or more validation errors occurred.",
                Extensions = { ["errors"] = validationException.Errors }
            },
            ConcurrencyException => new ProblemDetails
            {
                Status = StatusCodes.Status409Conflict,
                Title = "Concurrency Error",
                Detail = "The record was modified by another user."
            },
            _ => new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "Server Error",
                Detail = "An unexpected error occurred."
            }
        };
    }
}

// Extension method
public static class ExceptionHandlingMiddlewareExtensions
{
    public static IApplicationBuilder UseCustomExceptionHandler(
        this IApplicationBuilder app)
    {
        return app.UseMiddleware<ExceptionHandlingMiddleware>();
    }
}

REST 约定

操作HTTP 方法URL成功代码失败代码
获取单个GET/{entities}/{id}200404
获取全部GET/{entities}200-
获取筛选结果GET/{entities}?filter=x200-
获取子项GET/{entities}/{id}/children200404
创建POST/{entities}201400
全量更新PUT/{entities}/{id}200400, 404
部分更新PATCH/{entities}/{id}200400, 404
删除DELETE/{entities}/{id}204400, 404
操作POST/{entities}/{id}/action200400, 404

关键规则

  1. 注入 ISender,而非 IMediator - 只发送,不发布
  2. 使用 CancellationToken - 传递给所有异步操作
  3. 返回恰当的状态码 - 创建时返回 201,删除时返回 204
  4. POST 使用 CreatedAtAction - 返回 location 标头
  5. DTO 放在 API 层 - 不要直接公开应用层 DTO
  6. 路由约束 - 使用 {id:guid} 确保类型安全
  7. 默认启用授权 - 在控制器上添加 [Authorize]
  8. API 版本控制 - 支持多个版本
  9. ProducesResponseType - 记录可能的响应
  10. 不要捕获异常 - 交由中间件处理

应避免的反模式

// ❌ WRONG: Business logic in controller
[HttpPost]
public async Task<IActionResult> Create([FromBody] Request request)
{
    if (await _repository.ExistsAsync(request.Name))
        return BadRequest("Already exists");  // Logic belongs in handler!
    
    var entity = new Entity { Name = request.Name };
    _repository.Add(entity);
    await _unitOfWork.SaveChangesAsync();
    return Ok(entity.Id);
}

// ✅ CORRECT: Controller only orchestrates
[HttpPost]
public async Task<IActionResult> Create([FromBody] Request request, CancellationToken ct)
{
    var command = new CreateCommand(request.Name);
    var result = await _sender.Send(command, ct);
    
    return result.IsFailure 
        ? BadRequest(result.Error) 
        : CreatedAtAction(nameof(GetById), new { id = result.Value }, result.Value);
}

// ❌ WRONG: Returning domain entities
[HttpGet("{id}")]
public async Task<User> GetById(Guid id)  // Exposes domain!

// ✅ CORRECT: Return DTOs
[HttpGet("{id}")]
public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
{
    var result = await _sender.Send(new GetQuery(id), ct);
    return result.IsFailure ? NotFound(result.Error) : Ok(result.Value);
}

// ❌ WRONG: Catching and wrapping exceptions
try { ... }
catch (Exception ex)
{
    return StatusCode(500, ex.Message);
}

// ✅ CORRECT: Let middleware handle exceptions
// No try-catch, middleware handles it globally

相关技能

  • dotnet-cqrs-command-generator - 为控制器生成命令
  • dotnet-cqrs-query-generator - 为控制器生成查询
  • dotnet-clean-architecture - 整体项目结构
  • dotnet-result-pattern - 处理命令/查询结果

评论

支持 Markdown