Skip to content

Commit 84312a0

Browse files
xenobiasoftclaude
andcommitted
Document CQRS command/query separation rule for API endpoints
Adds explicit guidance that command endpoints must not return domain data — only a status code — and that queries are the sole place to read state. Updates the REST controller example to show a command returning 201 with no body alongside a query returning data, and adds the pattern to the anti-patterns list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 60f9d33 commit 84312a0

1 file changed

Lines changed: 37 additions & 26 deletions

File tree

agents.md

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ This is a modern Sudoku game built with .NET, Blazor, and C# following Clean Arc
1010

1111
```
1212
Sudoku.sln
13-
├── Sudoku.Domain/ # Core domain entities and business rules
14-
├── Sudoku.Application/ # Application use cases and orchestration
15-
├── Sudoku.Infrastructure/ # External concerns (storage, external APIs)
16-
├── Sudoku.Web.Server/ # Blazor Server presentation layer
1713
├── Sudoku.Api/ # REST API presentation layer
1814
├── Sudoku.AppHost/ # Application orchestration
19-
├── Sudoku.Storage.Azure/ # Azure storage implementations
15+
├── Sudoku.Application/ # Application use cases and orchestration
16+
├── Sudoku.Blazor/ # Blazor Server presentation layer
17+
├── Sudoku.Domain/ # Core domain entities and business rules
18+
├── Sudoku.Infrastructure/ # External concerns (storage, external APIs)
19+
├── Sudoku.McpServer # MPC server for AI tooling
2020
├── Sudoku.ServiceDefaults/ # Default service configurations
21-
└── Tests/ # Unit and integration tests
21+
└── Tests/ # Unit and integration tests
2222
```
2323

2424
### Key Architectural Principles
@@ -34,14 +34,12 @@ Sudoku.sln
3434
### C# Coding Standards
3535

3636
- **Naming Conventions**:
37-
3837
- Use PascalCase for public members, classes, and methods
3938
- Use camelCase for private fields and local variables
4039
- Use UPPER_CASE for constants
4140
- Prefix private fields with underscore: `_fieldName`
4241

4342
- **File Organization**:
44-
4543
- One public class per file
4644
- File name should match class name
4745
- Group related classes in appropriate namespaces
@@ -295,30 +293,42 @@ public static class ServiceCollectionExtensions
295293

296294
## API Design Guidelines
297295

296+
### CQRS Command vs Query Separation
297+
298+
This is a CQRS application. Command endpoints and query endpoints are strictly separate:
299+
300+
- **Command endpoints** (`POST`, `PUT`, `PATCH`, `DELETE`) mutate state and **must not return domain data**. Return only an HTTP status code — `201 Created` with a `Location` header for creates, `204 No Content` for updates and deletes.
301+
- **Query endpoints** (`GET`) read state and return data.
302+
303+
If a client needs the updated resource after a command, it issues a separate GET request. Mixing the two breaks the CQRS separation.
304+
298305
### REST API Controllers
299306

300307
```csharp
301-
[ApiController]
302-
[Route("api/[controller]")]
303-
public class GamesController : ControllerBase
308+
// COMMAND endpoint — mutates state, returns no data
309+
[HttpPost("{alias}/games/{difficulty}")]
310+
[ProducesResponseType(StatusCodes.Status201Created)]
311+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
312+
public async Task<ActionResult> CreateGame(string alias, string difficulty)
304313
{
305-
private readonly IGameApplicationService _gameService;
314+
var result = await _gameService.CreateGameAsync(alias, difficulty);
315+
if (!result.IsSuccess)
316+
return BadRequest(result.Error);
306317

307-
[HttpPost]
308-
public async Task<ActionResult<GameDto>> CreateGame(CreateGameRequest request)
309-
{
310-
var command = new CreateGameCommand(request.PlayerAlias, request.Difficulty);
311-
var result = await _gameService.CreateGameAsync(command);
318+
return Created($"/api/players/{alias}/games/{result.Value.Id}", null);
319+
}
312320

313-
if (result.IsSuccess)
314-
{
315-
return Ok(result.Value);
316-
}
317-
else
318-
{
319-
return BadRequest(result.Error);
320-
}
321-
}
321+
// QUERY endpoint — reads state, returns data
322+
[HttpGet("{alias}/games/{gameId}")]
323+
[ProducesResponseType(typeof(GameDto), StatusCodes.Status200OK)]
324+
[ProducesResponseType(StatusCodes.Status404NotFound)]
325+
public async Task<ActionResult<GameDto>> GetGame(string alias, string gameId)
326+
{
327+
var result = await _gameService.GetGameAsync(gameId);
328+
if (!result.IsSuccess)
329+
return NotFound();
330+
331+
return Ok(result.Value);
322332
}
323333
```
324334

@@ -419,6 +429,7 @@ public class GameByPlayerSpecification : ISpecification<Game>
419429
5. **Magic Numbers**: Use constants or enums instead of magic numbers
420430
6. **Long Methods**: Keep methods focused and under 20 lines when possible
421431
7. **Deep Nesting**: Avoid deeply nested if statements and loops
432+
8. **Returning data from command endpoints**: Command endpoints mutate state — they must not return domain data. If the client needs the updated resource after a command, it should issue a separate GET request. This keeps commands and queries cleanly separated.
422433

423434
Remember: This is a learning project focused on TDD, clean architecture, and modern .NET development practices. Prioritize code quality, testability, and maintainability over quick solutions.
424435

0 commit comments

Comments
 (0)