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.

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