Arquitectura
Patrones Backend
Guía completa de patrones y convenciones del backend de ParkVision. Clean Architecture, CQRS, Repository Pattern y más.
Patrones Backend - Parking API
Arquitectura
Este proyecto sigue Clean Architecture con los siguientes principios:
- CQRS (Command Query Responsibility Segregation) usando MediatR
- Repository Pattern para acceso a datos
- Dependency Injection para desacoplamiento
- DTOs para transferencia de datos
- AutoMapper para mapeo de entidades
Estructura de carpetas
Parking.Domain/ # Entidades y interfaces de dominio
Parking.Application/ # Lógica de negocio (Commands, Queries, DTOs)
Parking.Infrastructure/ # Implementación de persistencia
Parking.API/ # Controllers y configuración de APIDomain Layer
Entidades
public class SalesPoint
{
public Guid Id { get; set; }
public ushort PosCode { get; set; }
public string? Name { get; set; }
public uint NextNumber { get; set; }
public bool IsActive { get; set; }
public bool IsDefaultAuto { get; set; }
public bool IsDefaultManual { get; set; }
public DateTime UpdatedAt { get; set; }
}Reglas:
- Usar tipos específicos:
Guidpara IDs,ushortpara códigos pequeños,uintpara números positivos - Propiedades string nullable con
?cuando son opcionales - Incluir
UpdatedAtpara auditoría - No incluir lógica de negocio compleja
Application Layer
DTOs
Se crean tres DTOs por entidad:
[Entity]Dto: Incluye todos los campos (para GET)Create[Entity]Dto: Sin Id, sin campos de auditoríaUpdate[Entity]Dto: Con Id, sin campos de auditoría
Commands
public class CreateSalesPointCommand : IRequest<RequestResponse>
{
public ushort PosCode { get; set; }
public string? Name { get; set; }
// ...propiedades del DTO de creación
}Reglas:
- Heredar de
IRequest<RequestResponse> - Propiedades coinciden con el DTO de creación/actualización
- Un archivo por comando
Command Handlers
Reglas para Handlers:
- Validar primero (lanzar
BusinessExceptionsi falla) - Verificar reglas de negocio (unicidad, etc.)
- Crear entidad con
Guid.NewGuid()para Id - Establecer
UpdatedAt = DateTime.UtcNow - Retornar
RequestResponseconSuccess = trueyRequestResultcon el DTO
Validaciones de unicidad
Cuando una entidad tiene flags que deben ser únicos (solo un registro puede tener el flag en true):
// En CREATE: rechazar si ya existe otro
if (request.IsDefaultAuto)
{
var existing = await _repository.GetDefaultAutoAsync();
if (existing != null)
throw new BusinessException(code, "Ya existe un registro predeterminado");
}
// En UPDATE: permitir si es el mismo registro
if (request.IsDefaultAuto)
{
var existing = await _repository.GetDefaultAutoAsync();
if (existing != null && existing.Id != request.Id)
throw new BusinessException(code, "Ya existe un registro predeterminado");
}Infrastructure Layer
Repository Interface
public interface ISalesPointRepository : IAsyncRepository<SalesPoint>
{
Task<SalesPoint?> GetByPosCodeAsync(ushort posCode);
Task<SalesPoint?> GetDefaultAutoAsync();
Task<List<SalesPoint>> GetAllActiveAsync();
}Reglas:
- Heredar de
IAsyncRepository<TEntity> - Retornar
Task<Entity?>para búsquedas que pueden no encontrar resultados - Retornar
Task<List<Entity>>para colecciones
Nomenclatura en base de datos
| Elemento | Convención | Ejemplo |
|---|---|---|
| Tabla | snake_case | sales_point |
| Columna | snake_case | pos_code, is_active |
| Índice | idx_[tabla]_[campo(s)] | idx_sales_point_pos_code |
| Primary Key | id tipo char(36) | -- |
API Layer
Controllers
[ApiController]
[Route("api/v1/[controller]")]
public class SalesPointsController : ControllerBase
{
[HttpGet]
public async Task<ActionResult<RequestResponse>> GetAll([FromQuery] bool? activeOnly)
{
var response = await _mediator.Send(new GetAllSalesPointsQuery { ActiveOnly = activeOnly });
return Ok(response);
}
}Reglas:
- Nombre del controller:
[Entity]sController(plural) - Route:
api/v1/[controller] - Siempre retornar
RequestResponse - Usar
[FromBody]para DTOs en POST/PUT - Usar
[FromQuery]para parámetros opcionales en GET
Manejo de errores
Las excepciones de negocio se manejan con BusinessException:
throw new BusinessException(
Result.SALES_POINT_POS_CODE_ALREADY_EXISTS,
"Ya existe un punto de venta con ese código"
);El middleware convierte esto en una respuesta HTTP 400 con la estructura:
{
"success": false,
"code": 752,
"message": "Ya existe un punto de venta con ese código"
}Códigos de error
Se asigna un rango de 10 códigos por entidad (ej: SalesPoint 750-759, NextEntity 760-769).
Migraciones
Nunca ejecutar migraciones automáticamente. Se crean con:
cd Parking.Infrastructure
dotnet ef migrations add Add[Entity]Table \
--startup-project ../Parking.API/Parking.API.csproj \
--context ParkingDBContextChecklist para nueva entidad
- Crear entidad en
Domain/[Entity].cs - Crear DTOs en
Application/Domain/DTOs/[Entity]Dto.cs - Crear repository interface en
Application/Contracts/Persistence/I[Entity]Repository.cs - Crear repository implementation en
Infrastructure/Repositories/[Entity]Repository.cs - Configurar DbContext en
Infrastructure/Persistence/ParkingDBContext.cs - Registrar repository en
Infrastructure/InfrastructureServiceRegistration.cs - Crear Commands y Handlers en
Application/Features/[Entity]/Commands/ - Crear Queries y Handlers en
Application/Features/[Entity]/Queries/ - Agregar mapeos en
Application/Mappings/MappingProfile.cs - Agregar códigos de error en
Application/Constants/Result.cs - Crear Controller en
API/Controllers/[Entity]sController.cs - Generar migración (NO ejecutar)
- Probar endpoints