Skip to main content

dotnet-cqrs-api

Guide for building .NET APIs using CQRS pattern with MediatR, FluentValidation, and Carter modules Use when this capability is needed.

Jump to install

Source facts

Repository
tomevault-io/skills-registry
Last source activity
April 28, 2026 at 22:53
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
dotnet-cqrs-api
description
Guide for building .NET APIs using CQRS pattern with MediatR, FluentValidation, and Carter modules Use when this capability is needed.
metadata
{"author":"alanben"}
# .NET CQRS API Development Skill ## Purpose This skill guides the specification and creation of .NET APIs using the CQRS pattern with MediatR, FluentValidation, and Carter modules. It provides a structured approach to building feature-based, vertically-sliced APIs with robust validation and clear separation of concerns. ## Core Principles ### Architectural Philosophy - **Vertical Slice Architecture**: Features are self-contained units organized by business capability - **CQRS Separation**: Commands (write operations) and Queries (read operations) are explicitly separated - **Result Pattern**: All operations return `Result<T>` for explicit success/failure handling - **Functional Composition**: Prefer declarative, immutable approaches over imperative mutations ### Technology Stack - **MediatR**: Implements mediator pattern for decoupling endpoint handlers from business logic - **FluentValidation**: Provides declarative, strongly-typed validation rules - **Carter**: Enables minimal API-style endpoint definition with module organization - **Mapster** (inferred): Used for object mapping (`.Adapt<T>()` pattern) ## Project Structure ``` /Features /{FeatureName} - Add{Feature}.cs # Command endpoint + handler - Update{Feature}.cs # Command endpoint + handler - Get{Feature}.cs # Single item query endpoint + handler - Get{Feature}s.cs # Collection query endpoint + handler - {Feature}AddRequest.cs # Command DTO - {Feature}UpdateRequest.cs # Command DTO - {Feature}Response.cs # Query response DTO ``` ## Feature Specification Process ### Step 1: Define the Feature Domain Before writing code, establish: 1. **Feature Name**: Clear, business-focused identifier (e.g., "Order", "Customer", "Invoice") 2. **Business Context**: What problem does this feature solve? 3. **Operations Needed**: Which CQRS operations are required? - Create (Add) - Read Single (Get) - Read Collection (GetAll/List) - Update - Delete (not in templates, but may be needed) 4. **Domain Model**: What properties define this entity? 5. **Validation Rules**: What business rules must be enforced? 6. **Authorization Requirements**: Who can access these endpoints? ### Step 2: Design the Data Contract Define three types of DTOs: **Request DTOs** (for commands): - Contain only the data needed to perform the operation - Exclude computed or system-managed fields (IDs, timestamps) - Should be immutable or follow init-only patterns **Response DTOs** (for queries): - May inherit from domain models - Include data needed by consumers - Can include computed/derived properties **Domain Models** (internal): - Represent the entity in the data layer - Should not be directly exposed via API ### Step 3: Specify Validation Rules For each operation, define: - Required fields and null checks - Range validations (GreaterThan, LessThan) - Format validations (Regex patterns, date formats) - Business rule validations (cross-field rules) - Custom validation logic requirements ### Step 4: Define Routing Strategy Establish URL patterns following RESTful conventions: - Collection routes: `/{feature-plural}` or `/Features/{FeatureName}` - Item routes: `/{feature-plural}/{id}` or `/Features/{FeatureName}/{id}` - Consider route parameters vs query strings - Maintain consistency across features ## Implementation Templates ### Command Pattern (Add/Create) **Use when**: Creating new entities **Structure**: ```csharp namespace {ProjectName}.Features.{FeatureName}; // 1. Carter Endpoint Module public class Add{Feature}Endpoint : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapPost("{route}", async ({Feature}AddRequest request, ISender sender) => { var command = new Add{Feature}.Command { {Feature} = request }; var result = await sender.Send(command); if (result.IsFailure) { return Results.BadRequest(result.Error); } return Results.Ok(result.Value); }) .Produces<int>() // Or appropriate return type .RequireAuthorization("{PolicyName}") .WithMetadata(new RouteMetadata { Tags = new[] { "{TagName}" } }); } } // 2. Command Definition public static partial class Add{Feature} { public class Command : IRequest<Result<int>> { // Or TResult public {Feature}AddRequest? {Feature} { get; set; } } // 3. Validation Rules public class Validator : AbstractValidator<Command> { public Validator() { RuleFor(x => x.{Feature}) .NotNull() .WithMessage("No {feature} data provided."); // Add specific field validations RuleFor(x => x.{Feature}!.PropertyName) .GreaterThan(0) .WithMessage("PropertyName cannot be zero."); } } // 4. Command Handler internal sealed partial class {Feature}Handler : IRequestHandler<Command, Result<int>> { private readonly ILogger<{Feature}Handler> _logger; private readonly IValidator<Command> _validator; private readonly I{Feature}Data _data; public {Feature}Handler( IValidator<Command> validator, ILogger<{Feature}Handler> logger, I{Feature}Data data ) { _validator = validator; _logger = logger; _data = data; } public async Task<Result<int>> Handle(Command request, CancellationToken cancellationToken) { // Validate var validationResult = _validator.Validate(request); if (!validationResult.IsValid) { return Result.Failure<int>(new Error("Add{Feature}.Validation", validationResult.ToString())); } try { // Map and execute {Feature}Model new{Feature} = request.{Feature}!.Adapt<{Feature}Model>(); var {feature} = await _data.Add{Feature}(new{Feature}); if ({feature} is null) { return Result.Failure<int>(new Error("Add{Feature}", "Failed to add {feature}")); } return {feature}.ID; } catch (Exception ex) { return Result.Failure<int>(new Error("Add{Feature}.Exception", ex.Message)); } } } } ``` **Key Patterns**: - Endpoint delegates to MediatR command via `ISender` - Validation happens in handler, not endpoint - Result pattern for explicit error handling - Exception handling wraps unexpected failures - Returns entity ID on success ### Query Pattern (Get Single) **Use when**: Retrieving a specific entity by identifier **Structure**: ```csharp namespace {ProjectName}.Features.{FeatureName}; // 1. Carter Endpoint Module public class Get{Feature}Endpoint : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapGet("{route}/{id:int}", async (int id, ISender sender) => { var query = new Get{Feature}.{Feature}Query { ID = id }; var result = await sender.Send(query); if (result.IsFailure) { return Results.NotFound(result.Error); } return Results.Ok(result.Value); }) .Produces<{Feature}Response>() .RequireAuthorization("{PolicyName}") .WithMetadata(new RouteMetadata { Tags = new[] { "{TagName}" } }); } } // 2. Query Definition public static class Get{Feature} { public class {Feature}Query : IRequest<Result<{Feature}Response>> { public int ID { get; set; } = 0; // Additional filter properties } // 3. Validation Rules public class Validator : AbstractValidator<{Feature}Query> { public Validator() { RuleFor(x => x.ID) .GreaterThan(0) .WithMessage("ID cannot be zero."); } } // 4. Query Handler internal sealed class Handler : IRequestHandler<{Feature}Query, Result<{Feature}Response>> { private readonly ILogger<Handler> _logger; private readonly IValidator<{Feature}Query> _validator; private readonly I{Feature}Data _data; public Handler( ILogger<Handler> logger, IValidator<{Feature}Query> validator, I{Feature}Data data ) { _logger = logger; _validator = validator; _data = data; } public async Task<Result<{Feature}Response>> Handle({Feature}Query request, CancellationToken cancellationToken) { // Validate var validationResult = _validator.Validate(request); if (!validationResult.IsValid) { return Result.Failure<{Feature}Response>(new Error("Get{Feature}.Validation", validationResult.ToString())); } // Retrieve and map var {feature} = await _data.Get{Feature}(request.ID); if ({feature} is null) { return Result.Failure<{Feature}Response>(new Error( "Get{Feature}.Null", "The {feature} with the specified ID was not found")); } var response = {feature}.Adapt<{Feature}Response>(); // Optional: Enrich response with additional data return response; } } } ``` **Key Patterns**: - Returns `NotFound` for missing entities - Validation ensures query parameters are valid - Response mapping allows projection/transformation - Separate method for complex retrieval logic ### Query Pattern (Get Collection) **Use when**: Retrieving multiple entities with filtering **Structure**: ```csharp namespace {ProjectName}.Features.{FeatureName}; // 1. Carter Endpoint Module public class Get{Feature}sEndpoint : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapGet("{route}", async (int param1, string param2, ISender sender) => { var query = new Get{Feature}s.{Feature}Query { Param1 = param1, Param2 = param2 }; var result = await sender.Send(query); if (result.IsFailure) { return Results.NotFound(result.Error); } return Results.Ok(result.Value); }) .Produces<IEnumerable<{Feature}Response>>() .RequireAuthorization("{PolicyName}") .WithMetadata(new RouteMetadata { Tags = new[] { "{TagName}" } }); } } // 2. Query Definition public static class Get{Feature}s { public class {Feature}Query : IRequest<Result<IEnumerable<{Feature}Response>>> { public int Param1 { get; set; } = 0; public string Param2 { get; set; } = string.Empty; // Filter/pagination properties } // 3. Validation Rules public class Validator : AbstractValidator<{Feature}Query> { public Validator() { RuleFor(x => x.Param1) .GreaterThan(0) .WithMessage("Param1 cannot be zero."); // Date format validation example RuleFor(x => x.DateParam) .Matches(@"\d{4}-\d{2}-\d{2}") .WithMessage("DateParam must be in the format yyyy-MM-dd."); } } // 4. Query Handler internal sealed class Handler : IRequestHandler<{Feature}Query, Result<IEnumerable<{Feature}Response>>> { private readonly ILogger<Handler> _logger; private readonly IValidator<{Feature}Query> _validator; private readonly I{Feature}Data _data; public Handler( ILogger<Handler> logger, IValidator<{Feature}Query> validator, I{Feature}Data data ) { _logger = logger; _validator = validator; _data = data; } public async Task<Result<IEnumerable<{Feature}Response>>> Handle({Feature}Query request, CancellationToken cancellationToken) { // Validate var validationResult = _validator.Validate(request); if (!validationResult.IsValid) { return Result.Failure<IEnumerable<{Feature}Response>>(new Error("Get{Feature}s.Validation", validationResult.ToString())); } // Retrieve collection var {feature}s = await _data.List{Feature}s(/* filter params */); if ({feature}s is null) { return Result.Failure<IEnumerable<{Feature}Response>>(new Error( "Get{Feature}s.Null", "Failed to get {feature}s")); } var response = {feature}s.Adapt<List<{Feature}Response>>(); return response; } } } ``` **Key Patterns**: - Query parameters come from route or query string - Returns collections (IEnumerable<T>) - Validation includes format checks (dates, etc.) - Consider pagination for large datasets ### Command Pattern (Update) **Use when**: Modifying existing entities **Structure**: ```csharp namespace {ProjectName}.Features.{FeatureName}; // 1. Carter Endpoint Module public class Update{Feature}Endpoint : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapPut("{route}", async ({Feature}UpdateRequest request, ISender sender) => { var command = new Update{Feature}.Command { {Feature} = request }; var result = await sender.Send(command); if (result.IsFailure) { return Results.BadRequest(result.Error); } return Results.Ok(result.Value); }) .Produces<int>() .RequireAuthorization("{PolicyName}") .WithMetadata(new RouteMetadata { Tags = new[] { "{TagName}" } }); } } // 2. Command Definition public static partial class Update{Feature} { public class Command : IRequest<Result<int>> { public {Feature}UpdateRequest? {Feature} { get; set; } } // 3. Validation Rules public class Validator : AbstractValidator<Command> { public Validator() { RuleFor(x => x.{Feature}) .NotNull()
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub