Apariencia
Invariantes: preservar el estado de una reserva
Índice · Anterior: valores · Siguiente: operaciones
Motivación
Una reserva cuya salida ocurre antes de la entrada no debe convertirse en el estado actual de la entidad. La condición debe mantenerse al crear y modificar la reserva, independientemente de qué pantalla o proceso solicite el cambio.
El ejemplo completo puede ejecutarse con:
powershell
dotnet run --file examples/DomainInvariants.csLos fragmentos siguientes explican sus partes; el archivo enlazado incluye imports, referencias y todas las declaraciones.
Paso 1. Representar un estado inmutable
csharp
sealed record ReservationState(DateOnly Start, DateOnly End, bool Closed);Las fechas y el indicador son valores. No existe una colección mutable que permita cambiar el contenido sin pasar por la entidad.
Paso 2. Expresar la condición como plan
csharp
private static readonly ValidationPlan<ReservationState, NoRuleContext> StatePlan =
Validation.For<ReservationState>()
.Must(s => s.End >= s.Start,
"La salida debe ser igual o posterior a la entrada.", "reservation.dates")
.Build();El plan describe una relación entre propiedades y devuelve un mensaje propio de esa relación. No se asigna una ruta de campo: la aplicación puede presentar el mensaje a nivel de reserva.
Paso 3. Registrar la invariante
csharp
static Reservation() => ConfigureInvariants(
Greencore.Domain.Entities.Invariants.For<ReservationState>()
.Include(StatePlan).Build());Reservation deriva de Entity<Reservation, ReservationState>. El constructor base valida el estado inicial. Una creación inválida produce InvariantViolationException. El registro se realiza una vez en el constructor estático y queda congelado al utilizarse.
El nombre completo evita la colisión con la propiedad heredada Invariants. No hace falta construir TreeInvariant manualmente. Un plan con reglas asíncronas se rechaza: las invariantes de publicación son locales y síncronas.
Paso 4. Construir y validar el cambio
csharp
return TryChangeState(State with { Start = start, End = end });TryChangeState evalúa el candidato antes de reemplazar State. Si falla, devuelve un resultado con error y conserva el estado anterior. ChangeState ofrece la variante basada en excepción.
Paso 5. Observar el rechazo
El ejemplo comienza el 10 de mayo de 2030 y termina el 12. Intenta luego una salida el 9:
text
Estado: 10 → 12
Propuesta: 10 → 9
Invariante: 9 >= 10 es falso
Resultado: reservation.dates
Estado final: 10 → 12Después propone 11 → 13. La condición se cumple y el candidato reemplaza el estado actual.
Cuándo no usar esta invariante
«La reserva no está cerrada» no debe incluirse en StatePlan: impediría publicar el estado de cierre. Es una condición para ciertas acciones, que se aplica en el siguiente paso del recorrido.
La entidad tampoco garantiza inmutabilidad profunda por el tipo genérico ni admite uso concurrente. En estados con listas usa colecciones inmutables. Una consulta a reservas ajenas pertenece a la validación contextual y necesita garantías de almacenamiento al confirmar.