ronnythedev/dotnet-clean-architecture-skills/07-1-dotnet-legacy-api-controllers

Claude Code and agent skill by ronnythedev/dotnet-clean-architecture-skills

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Generates RESTful API Controllers with proper routing, versioning, authorization, and MediatR integration. Follows REST conventions and Clean Architecture patterns.

Install

npx vskill@latest install ronnythedev/dotnet-clean-architecture-skills/07-1-dotnet-legacy-api-controllers

SKILL.md

API Controller Generator

Overview

This skill generates RESTful API Controllers following best practices:

  • MediatR integration - Send commands/queries via ISender
  • API versioning - URL segment versioning
  • Authorization - Role and permission-based
  • Consistent responses - Proper HTTP status codes
  • Request/Response DTOs - Separate from domain

Quick Reference

HTTP MethodActionReturns
GET /{id}Get by ID200 OK / 404 Not Found
GET /Get all/list200 OK
POST /Create201 Created / 400 Bad Request
PUT /{id}Full update200 OK / 404 Not Found
PATCH /{id}Partial update200 OK / 404 Not Found
DELETE /{id}Delete204 No Content / 404 Not Found

Controller Structure

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

Template: Complete CRUD Controller

csharp
// 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();
}
}

Template: Request DTOs

csharp
// 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; }
}

Template: Controller with Complex Operations

csharp
// 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);
}

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
64
Repository forks
11
7-day trend score
21

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/ronnythedev/dotnet-clean-architecture-skills/07-1-dotnet-legacy-api-controllers/badge)](https://verified-skill.com/skills/ronnythedev/dotnet-clean-architecture-skills/07-1-dotnet-legacy-api-controllers)

Something wrong with this skill or its page? Tell us.