ModelingEvolution.Booking 1.0.0-preview.2
ModelingEvolution.Booking
Meeting booking as an event-sourced module on MicroPlumberd 1.2.x / KurrentDB: one BookingCalendar-{mailbox}
stream arbitrates non-overlap of meetings on a mailbox (optimistic concurrency — a lost version race retries once,
then refuses as slot_taken); each booking lives on its own Booking-{BookingId} stream (MeetingBooked,
MeetingCancelled); availability is synced from O365 and offered per meeting type; bookings carry an
ExternalReference (kind:value) a consumer uses to find them again.
builder.Services.AddPlumberd(KurrentDBClientSettings.Create(connectionString));
builder.Services.AddBooking(builder.Configuration); // command handlers, read models, calendar integration (Booking:* options)
The pipeline as two public primitives (1.0.0-preview.2, ADR-3 split)
A host that only TAKES intents (no calendar credential) and a worker that MATERIALISES them can each compose the
half they need — the website's own BookingCommandHandler runs the very same two objects:
// intent-taking unit: read models + step 1, no ICalendarService
builder.Services.AddSingletonEventHandler<BookingConfigModel>(FromRelativeStreamPosition.End - 1);
builder.Services.AddSingletonEventHandler<AvailableSlotsModel>(FromRelativeStreamPosition.End - 1);
builder.Services.Configure<BookingOptions>(cfg.GetSection("Booking")); // Mailbox ⇒ calendar stream; ReservationLease
builder.Services.AddBookingSlotArbiter();
var hold = await arbiter.ReserveAsync(bookingId, date, time, MeetingTypeKey.Standard); // idempotent for the same id + window; faults: slot_taken | invalid_* | too_close
await arbiter.TryReleaseAsync(bookingId, "reason");
// materialising worker: + ICalendarService (yours) + steps 2–4
builder.Services.AddSingleton<ICalendarService, YourCalendar>(); // or the sink
builder.Services.AddBookingMaterialiser();
var result = await materialiser.MaterialiseAsync(new MeetingIntent(bookingId, date, time, type, name, email, phone, comments, reference, tags, "portal"));
// result.WasAlreadyBooked: a re-delivered intent — nothing created twice; result.Booked is the stored event
MaterialiseAsync checks Booking-{id} for MeetingBooked FIRST, calls CreateEventAsync(…, transactionId: id)
(the calendar's idempotency key — a crash between the calendar and the append cannot orphan a second event on retry),
appends MeetingBooked + BookingTagAttached per key in ONE append, and confirms the hold.
Read models: BookingLookupModel (by id / by tag), AvailableSlotsModel, BookingConfigModel.
The concurrency invariant is tested against a real KurrentDB in the source repository
(RealStoreSlotArbitrationTests), on both the pre- and post-1.2 client libraries.
No packages depend on ModelingEvolution.Booking.
.NET 10.0
- MicroPlumberd (>= 1.2.2)
- MicroPlumberd.Services (>= 1.2.2)
- MicroPlumberd.SourceGenerators (>= 1.2.2)
- ModelingEvolution.JsonParsableConverter (>= 1.0.1)
| Version | Downloads | Last updated |
|---|---|---|
| 1.0.0-preview.11 | 0 | 08/19/2026 |
| 1.0.0-preview.10 | 0 | 08/19/2026 |
| 1.0.0-preview.9 | 0 | 08/19/2026 |
| 1.0.0-preview.8 | 6 | 08/17/2026 |
| 1.0.0-preview.7 | 0 | 08/17/2026 |
| 1.0.0-preview.6 | 5 | 08/17/2026 |
| 1.0.0-preview.5 | 1 | 08/17/2026 |
| 1.0.0-preview.4 | 3 | 08/17/2026 |
| 1.0.0-preview.3 | 0 | 08/17/2026 |
| 1.0.0-preview.2 | 3 | 08/17/2026 |
| 1.0.0-preview.1 | 0 | 08/17/2026 |