关于 dotnet-legacy-api-controllers
用于生成 .NET RESTful API 控制器的技能,提供 CRUD 和复杂操作模板,涵盖 MediatR 的 ISender、API 版本控制、授权、DTO 及 HTTP 响应约定。
主要功能
- 01CRUD 控制器模板
- 02集成 MediatR ISender
- 03基于 URL 段的 API 版本控制
- 04基于角色和权限的授权
- 05请求与响应 DTO
- 06HTTP 状态码约定
适用场景
- 01创建带版本控制的 REST 端点
- 02将控制器连接到命令和查询
- 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} | 200 | 404 |
| 获取全部 | GET | /{entities} | 200 | - |
| 获取筛选结果 | GET | /{entities}?filter=x | 200 | - |
| 获取子项 | GET | /{entities}/{id}/children | 200 | 404 |
| 创建 | POST | /{entities} | 201 | 400 |
| 全量更新 | PUT | /{entities}/{id} | 200 | 400, 404 |
| 部分更新 | PATCH | /{entities}/{id} | 200 | 400, 404 |
| 删除 | DELETE | /{entities}/{id} | 204 | 400, 404 |
| 操作 | POST | /{entities}/{id}/action | 200 | 400, 404 |
关键规则
- 注入 ISender,而非 IMediator - 只发送,不发布
- 使用 CancellationToken - 传递给所有异步操作
- 返回恰当的状态码 - 创建时返回 201,删除时返回 204
- POST 使用 CreatedAtAction - 返回 location 标头
- DTO 放在 API 层 - 不要直接公开应用层 DTO
- 路由约束 - 使用
{id:guid}确保类型安全 - 默认启用授权 - 在控制器上添加
[Authorize] - API 版本控制 - 支持多个版本
- ProducesResponseType - 记录可能的响应
- 不要捕获异常 - 交由中间件处理
应避免的反模式
// ❌ 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