SAPFIAI implements the CQRS (Command Query Responsibility Segregation) pattern using MediatR. This guide shows you how to create new use cases for your application.
Architecture Overview
Use cases are organized in the Application layer under feature folders:
src/Application/
├── Permissions/
│ ├── Commands/
│ └── Queries/
├── Roles/
│ ├── Commands/
│ └── Queries/
└── Users/
├── Commands/
└── Queries/
Each use case consists of three files:
Command/Query - The request object
Handler - The business logic
Validator - FluentValidation rules (optional but recommended)
Using the Template Generator
The fastest way to create a new use case is using the dotnet new ca-usecase template.
Creating a Command
Commands modify state and typically return a result:
dotnet new ca-usecase \
--name CrearUsuario \
--feature-name Usuarios \
--usecase-type command \
--return-type int
Creating a Query
Queries retrieve data without side effects:
dotnet new ca-usecase \
--name ObtenerUsuarios \
--feature-name Usuarios \
--usecase-type query \
--return-type "List<UsuarioDto>"
Template Options
View all available options:
dotnet new ca-usecase --help
Creating Commands Manually
Here’s a complete example of creating a command to create permissions.
Step 1: Define the Command
Create CreatePermissionCommand.cs in src/Application/Permissions/Commands/CreatePermission/:
using SAPFIAI . Application . Common . Models ;
namespace SAPFIAI . Application . Permissions . Commands . CreatePermission ;
public record CreatePermissionCommand : IRequest < Result < int >>
{
public required string Name { get ; init ; }
public string ? Description { get ; init ; }
public required string Module { get ; init ; }
}
Commands and Queries use record types for immutability and value-based equality.
Step 2: Implement the Handler
Create CreatePermissionCommandHandler.cs:
using SAPFIAI . Application . Common . Interfaces ;
using SAPFIAI . Application . Common . Models ;
using SAPFIAI . Domain . Entities ;
using Microsoft . Extensions . Logging ;
namespace SAPFIAI . Application . Permissions . Commands . CreatePermission ;
public class CreatePermissionCommandHandler : IRequestHandler < CreatePermissionCommand , Result < int >>
{
private readonly IApplicationDbContext _context ;
private readonly ILogger < CreatePermissionCommandHandler > _logger ;
public CreatePermissionCommandHandler (
IApplicationDbContext context ,
ILogger < CreatePermissionCommandHandler > logger )
{
_context = context ;
_logger = logger ;
}
public async Task < Result < int >> Handle (
CreatePermissionCommand request ,
CancellationToken cancellationToken )
{
_logger . LogInformation ( "Creating permission: {PermissionName}" , request . Name );
// Business logic: Check if permission already exists
var exists = await _context . Permissions
. AnyAsync ( p => p . Name == request . Name , cancellationToken );
if ( exists )
{
_logger . LogWarning ( "Permission already exists: {PermissionName}" , request . Name );
return Result . Failure < int >( new Error (
"PermissionExists" ,
"El permiso ya existe"
));
}
// Create entity
var permission = new Permission
{
Name = request . Name ,
Description = request . Description ,
Module = request . Module ,
IsActive = true ,
CreatedAt = DateTime . UtcNow
};
_context . Permissions . Add ( permission );
await _context . SaveChangesAsync ( cancellationToken );
_logger . LogInformation ( "Permission created successfully: {PermissionId}" , permission . Id );
return Result . Success ( permission . Id );
}
}
Key patterns in handlers:
Dependency Injection : Inject IApplicationDbContext and ILogger
Logging : Log important operations for debugging and monitoring
Business Validation : Check business rules before persisting
Result Pattern : Return Result<T> for success/failure scenarios
CancellationToken : Support request cancellation
Step 3: Add Validation
Create CreatePermissionCommandValidator.cs:
namespace SAPFIAI . Application . Permissions . Commands . CreatePermission ;
public class CreatePermissionCommandValidator : AbstractValidator < CreatePermissionCommand >
{
public CreatePermissionCommandValidator ()
{
RuleFor ( x => x . Name )
. NotEmpty (). WithMessage ( "El nombre del permiso es requerido" )
. MaximumLength ( 100 ). WithMessage ( "El nombre no puede exceder 100 caracteres" )
. Matches ( "^[a-z0-9._-]+$" ). WithMessage ( "Use formato: modulo.accion (ej: users.create)" );
RuleFor ( x => x . Module )
. NotEmpty (). WithMessage ( "El módulo es requerido" )
. MaximumLength ( 50 ). WithMessage ( "El módulo no puede exceder 50 caracteres" );
RuleFor ( x => x . Description )
. MaximumLength ( 500 ). WithMessage ( "La descripción no puede exceder 500 caracteres" )
. When ( x => ! string . IsNullOrEmpty ( x . Description ));
}
}
Validators are automatically discovered and executed by MediatR’s validation pipeline before the handler runs.
Creating Queries Manually
Queries follow a similar pattern but focus on data retrieval.
Step 1: Define the Query
Create GetPermissionsQuery.cs in src/Application/Permissions/Queries/GetPermissions/:
using SAPFIAI . Application . Common . Models ;
namespace SAPFIAI . Application . Permissions . Queries . GetPermissions ;
public record GetPermissionsQuery : IRequest < List < PermissionDto >>
{
public bool ActiveOnly { get ; init ; } = false ;
}
Step 2: Implement the Handler
Create GetPermissionsQueryHandler.cs:
using SAPFIAI . Application . Common . Interfaces ;
using SAPFIAI . Application . Common . Models ;
namespace SAPFIAI . Application . Permissions . Queries . GetPermissions ;
public class GetPermissionsQueryHandler : IRequestHandler < GetPermissionsQuery , List < PermissionDto >>
{
private readonly IApplicationDbContext _context ;
public GetPermissionsQueryHandler ( IApplicationDbContext context )
{
_context = context ;
}
public async Task < List < PermissionDto >> Handle (
GetPermissionsQuery request ,
CancellationToken cancellationToken )
{
var query = _context . Permissions . AsQueryable ();
if ( request . ActiveOnly )
{
query = query . Where ( p => p . IsActive );
}
return await query
. Select ( p => new PermissionDto
{
Id = p . Id ,
Name = p . Name ,
Description = p . Description ,
Module = p . Module ,
IsActive = p . IsActive ,
CreatedAt = p . CreatedAt
})
. ToListAsync ( cancellationToken );
}
}
Queries typically project directly to DTOs to avoid loading unnecessary data and prevent accidental entity modification.
DTOs (Data Transfer Objects)
Define DTOs in src/Application/Common/Models/:
namespace SAPFIAI . Application . Common . Models ;
public class PermissionDto
{
public int Id { get ; set ; }
public string Name { get ; set ; } = string . Empty ;
public string ? Description { get ; set ; }
public string Module { get ; set ; } = string . Empty ;
public bool IsActive { get ; set ; }
public DateTime CreatedAt { get ; set ; }
}
Using Commands and Queries
In Controllers/Endpoints
Inject ISender from MediatR:
public class PermissionsController : ApiControllerBase
{
[ HttpPost ]
public async Task < ActionResult < int >> Create ( CreatePermissionCommand command )
{
var result = await Mediator . Send ( command );
if ( result . IsFailure )
{
return BadRequest ( result . Error );
}
return Ok ( result . Value );
}
[ HttpGet ]
public async Task < ActionResult < List < PermissionDto >>> GetAll ([ FromQuery ] bool activeOnly = false )
{
var query = new GetPermissionsQuery { ActiveOnly = activeOnly };
return await Mediator . Send ( query );
}
}
In Other Handlers
Handlers can call other handlers:
public class SomeCommandHandler : IRequestHandler < SomeCommand , Result >
{
private readonly ISender _mediator ;
public SomeCommandHandler ( ISender mediator )
{
_mediator = mediator ;
}
public async Task < Result > Handle ( SomeCommand request , CancellationToken cancellationToken )
{
// Call another query or command
var permissions = await _mediator . Send (
new GetPermissionsQuery { ActiveOnly = true },
cancellationToken
);
// Continue with business logic...
}
}
Result Pattern
Use the Result<T> pattern for operations that can fail:
// Success
return Result . Success ( value );
// Failure
return Result . Failure < T >( new Error ( "ErrorCode" , "Error message" ));
// Checking results
if ( result . IsSuccess )
{
var value = result . Value ;
}
else
{
var error = result . Error ;
var code = error . Code ;
var message = error . Description ;
}
Best Practices
Each handler should do one thing well. If logic becomes complex, extract it into domain services or separate handlers.
Validate all input at the command/query level. Business rules go in the handler, input validation in the validator.
Project to DTOs in Queries
Never return domain entities from queries. Always project to DTOs to control what data is exposed.
Always pass and respect CancellationToken to support request cancellation and improve resource utilization.
Log important operations (Information), warnings (Warning), and errors (Error). Avoid logging sensitive data.
Real-World Examples
Explore these examples in the codebase:
Commands with validation : src/Application/Permissions/Commands/CreatePermission/
Queries with filtering : src/Application/Permissions/Queries/GetPermissions/
Complex commands : src/Application/Users/Commands/Login/
Multiple validators : src/Application/Users/Commands/Login/LoginCommand.cs:39
Next Steps
Testing Learn how to test your use cases
Database Migrations Update your database schema