Created
August 11, 2021 16:44
-
-
Save skleanthous/11ab343a9358b492bddef04f433580ad to your computer and use it in GitHub Desktop.
The parking lot implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Collections.Immutable; | |
using System.Linq; | |
using EventStore.Client; | |
namespace BullOak.Repositories.EventStore.Test.Integration | |
{ | |
internal record InspectionId(Guid Id); | |
internal record LicencePlate(string PlateNumber); | |
internal class Product | |
{ | |
public DateTimeOffset CalculateFinishTimeFrom(DateTimeOffset startTime) | |
=> startTime + TimeSpan.FromMinutes(30); | |
} | |
internal record VehicleRegistered(string LicencePlate); | |
internal record VehicleBooked(string LicencePlate, string Location, long Start, long Finish); | |
internal record VehicleUnbooked(Guid InspectionId, long inspectionTime, string LicencePlate, string Location); | |
internal record Booking(DateTimeOffset startTime, DateTimeOffset endTime); | |
internal record VehicleState(bool IsRegistered) | |
{ | |
public ImmutableList<Booking> Bookings { get; init; } = ImmutableList<Booking>.Empty; | |
public static VehicleState Default() => new VehicleState(false); | |
} | |
internal class Vehicle | |
{ | |
public Maybe<VehicleUnbooked> Inspect(VehicleState state, InspectionId inspectionId, DateTimeOffset when, | |
LicencePlate licencePlate, string location) | |
{ | |
bool conflictsWithBooking = state.Bookings.Any(x => x.startTime <= when && when <= x.endTime); | |
if (!conflictsWithBooking) | |
return Maybe<VehicleUnbooked>.Some(new VehicleUnbooked(inspectionId.Id, when.ToUnixTimeSeconds(), | |
licencePlate.PlateNumber, location)); | |
return Maybe<VehicleUnbooked>.None; | |
} | |
public IEnumerable<object> Book(VehicleState state, LicencePlate licencePlate, string location, | |
DateTimeOffset start, Product product) | |
{ | |
if (!state.IsRegistered) | |
yield return new VehicleRegistered(licencePlate.PlateNumber); | |
var finish = product.CalculateFinishTimeFrom(start); | |
yield return new VehicleBooked( | |
LicencePlate: licencePlate.PlateNumber, | |
Location: location, | |
Start: start.ToUnixTimeSeconds(), | |
Finish: finish.ToUnixTimeSeconds()); | |
} | |
} | |
internal class Rehydration | |
{ | |
public VehicleState RehydrateFrom(object[] events) | |
{ | |
var state = VehicleState.Default(); | |
foreach (var @event in events) | |
state = @event switch | |
{ | |
VehicleRegistered registered => Apply(state, registered), | |
VehicleBooked booked => Apply(state, booked), | |
VehicleUnbooked unbooked => Apply(state, unbooked), | |
_ => throw new NotSupportedException() | |
}; | |
return state; | |
} | |
private VehicleState Apply(VehicleState before, VehicleRegistered @event) | |
=> before with {IsRegistered = true}; | |
private VehicleState Apply(VehicleState before, VehicleBooked @event) | |
=> before with | |
{ | |
Bookings = before.Bookings.Add(new Booking(DateTimeOffset.FromUnixTimeSeconds(@event.Start), | |
DateTimeOffset.FromUnixTimeSeconds(@event.Finish))) | |
}; | |
private VehicleState Apply(VehicleState before, VehicleUnbooked @event) | |
{ | |
var timeOfInspection = DateTimeOffset.FromUnixTimeSeconds(@event.inspectionTime); | |
return before with | |
{ | |
Bookings = before.Bookings.Where(x => x.startTime > timeOfInspection || x.endTime < timeOfInspection) | |
.ToImmutableList() | |
}; | |
} | |
} | |
internal class CommandHandler | |
{ | |
private EventStoreWrapper EventStore { get; init; } | |
private Rehydration Rehydrator { get; init; } | |
private Vehicle Vehicle { get; init; } | |
public void BookTheCar(LicencePlate licencePlate, string location, | |
DateTimeOffset start, Product product) | |
{ | |
var read = EventStore.ReadVehicleFrom(licencePlate.PlateNumber); | |
var newEvents = Vehicle.Book(read.State, licencePlate, location, start, product); | |
if (newEvents.Any()) | |
EventStore.WriteToVehicleStream(licencePlate.PlateNumber, newEvents, Check.ExpectConcurrency(read.Concurrency)); | |
} | |
public void ScheduleInspection(InspectionId inspectionId, DateTimeOffset when, | |
LicencePlate licencePlate, string location) | |
{ | |
var read = EventStore.ReadVehicleFrom(licencePlate.PlateNumber); | |
var newEvents = Vehicle.Inspect(read.State, inspectionId, when, licencePlate, location); | |
if (newEvents.hasValue) | |
EventStore.WriteToVehicleStream(licencePlate.PlateNumber, newEvents.value, Check.ExpectConcurrency(read.Concurrency)); | |
} | |
} | |
internal interface EventStoreWrapper | |
{ | |
ReadStreamResponse ReadVehicleFrom(string streamName); | |
WriteStreamResponse WriteToVehicleStream(string streamName, object[] events, Check check); | |
WriteStreamResponse WriteToVehicleStream(string streamName, object @event, Check check); | |
} | |
internal record ReadStreamResponse(object[] Events, VehicleState State, bool PreExisting, long Concurrency); | |
internal record WriteStreamResponse(bool Success, string errorMessageIfNotSuccess); | |
internal interface Check | |
{ | |
static Check ExpectConcurrency(long expectedVersion) | |
=> new ConcurrencyCheck(expectedVersion); | |
static Check Pass() => new AppendWithoutConcurrency(); | |
static Check IfIsNewStream() => new AppendIfNewStream(); | |
} | |
internal record ConcurrencyCheck(long ExpectedVersion) : Check; | |
internal record AppendWithoutConcurrency() : Check; | |
internal record AppendIfNewStream() : Check; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment