Skip to content

Instantly share code, notes, and snippets.

@dcomartin
Created March 6, 2026 14:29
Show Gist options
  • Select an option

  • Save dcomartin/848c7f81d025da1b4366428ba4244e72 to your computer and use it in GitHub Desktop.

Select an option

Save dcomartin/848c7f81d025da1b4366428ba4244e72 to your computer and use it in GitHub Desktop.
public sealed record ShipmentDispatched(Guid ShipmentId, DateTime DispatchedAt);
public sealed record ShipmentArrived(Guid ShipmentId, DateTime ArrivedAt);
public sealed record ShipmentLoaded(Guid ShipmentId, int PalletsLoaded, DateTime LoadedAt);
public sealed record ShipmentEmptied(Guid ShipmentId, DateTime EmptiedAt);
public enum ShipmentStatus
{
Created,
Dispatched,
Arrived,
Loading,
Empty
}
public sealed class Shipment
{
public Guid Id { get; private set; }
public ShipmentStatus Status { get; private set; } = ShipmentStatus.Created;
public DateTime? DispatchedAt { get; private set; }
public DateTime? ArrivedAt { get; private set; }
public int TotalPalletsLoaded { get; private set; }
public DateTime? EmptiedAt { get; private set; }
private readonly List<object> _uncommitted = new();
public IReadOnlyList<object> UncommittedEvents => _uncommitted;
private Shipment() { }
public void Dispatch(DateTime now)
{
if (Status != ShipmentStatus.Created)
throw new InvalidOperationException("Can only dispatch from Created.");
Raise(new ShipmentDispatched(Id, now));
}
public void Arrive(DateTime now)
{
if (Status != ShipmentStatus.Dispatched)
throw new InvalidOperationException("Can only arrive from Dispatched.");
Raise(new ShipmentArrived(Id, now));
}
public void Load(int pallets, DateTime now)
{
if (Status != ShipmentStatus.Arrived && Status != ShipmentStatus.Loading)
throw new InvalidOperationException("Can only load after Arrived (or while Loading).");
if (pallets <= 0)
throw new InvalidOperationException("PalletsLoaded must be > 0.");
Raise(new ShipmentLoaded(Id, pallets, now));
}
public void Empty(DateTime now)
{
if (Status != ShipmentStatus.Arrived && Status != ShipmentStatus.Loading)
throw new InvalidOperationException("Can only empty after Arrived/Loading.");
Raise(new ShipmentEmptied(Id, now));
}
public void ClearUncommittedEvents() => _uncommitted.Clear();
private void Raise(object @event)
{
Apply(@event);
_uncommitted.Add(@event);
}
private void Apply(object @event)
{
switch (@event)
{
case ShipmentDispatched e:
Status = ShipmentStatus.Dispatched;
DispatchedAt = e.DispatchedAt;
break;
case ShipmentArrived e:
Status = ShipmentStatus.Arrived;
ArrivedAt = e.ArrivedAt;
break;
case ShipmentLoaded e:
Status = ShipmentStatus.Loading;
TotalPalletsLoaded += e.PalletsLoaded;
break;
case ShipmentEmptied e:
Status = ShipmentStatus.Empty;
EmptiedAt = e.EmptiedAt;
break;
default:
throw new InvalidOperationException($"Unknown event type '{@event.GetType().Name}'.");
}
}
public static Shipment Rehydrate(Guid shipmentId, IEnumerable<object> history)
{
var agg = new Shipment { Id = shipmentId };
foreach (var e in history)
agg.Apply(e);
return agg;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment