ParkVision Docs
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 API

Domain 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: Guid para IDs, ushort para códigos pequeños, uint para números positivos
  • Propiedades string nullable con ? cuando son opcionales
  • Incluir UpdatedAt para 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ía
  • Update[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:

  1. Validar primero (lanzar BusinessException si falla)
  2. Verificar reglas de negocio (unicidad, etc.)
  3. Crear entidad con Guid.NewGuid() para Id
  4. Establecer UpdatedAt = DateTime.UtcNow
  5. Retornar RequestResponse con Success = true y RequestResult con 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

ElementoConvenciónEjemplo
Tablasnake_casesales_point
Columnasnake_casepos_code, is_active
Índiceidx_[tabla]_[campo(s)]idx_sales_point_pos_code
Primary Keyid 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 ParkingDBContext

Checklist para nueva entidad

  1. Crear entidad en Domain/[Entity].cs
  2. Crear DTOs en Application/Domain/DTOs/[Entity]Dto.cs
  3. Crear repository interface en Application/Contracts/Persistence/I[Entity]Repository.cs
  4. Crear repository implementation en Infrastructure/Repositories/[Entity]Repository.cs
  5. Configurar DbContext en Infrastructure/Persistence/ParkingDBContext.cs
  6. Registrar repository en Infrastructure/InfrastructureServiceRegistration.cs
  7. Crear Commands y Handlers en Application/Features/[Entity]/Commands/
  8. Crear Queries y Handlers en Application/Features/[Entity]/Queries/
  9. Agregar mapeos en Application/Mappings/MappingProfile.cs
  10. Agregar códigos de error en Application/Constants/Result.cs
  11. Crear Controller en API/Controllers/[Entity]sController.cs
  12. Generar migración (NO ejecutar)
  13. Probar endpoints
Patrones Backend