ModelingEvolution.Booking
1.0.0-preview.4
See the version list below for details.
dotnet add package ModelingEvolution.Booking --version 1.0.0-preview.4
NuGet\Install-Package ModelingEvolution.Booking -Version 1.0.0-preview.4
<PackageReference Include="ModelingEvolution.Booking" Version="1.0.0-preview.4" />
<PackageVersion Include="ModelingEvolution.Booking" Version="1.0.0-preview.4" />
<PackageReference Include="ModelingEvolution.Booking" />
paket add ModelingEvolution.Booking --version 1.0.0-preview.4
#r "nuget: ModelingEvolution.Booking, 1.0.0-preview.4"
#:package ModelingEvolution.Booking@1.0.0-preview.4
#addin nuget:?package=ModelingEvolution.Booking&version=1.0.0-preview.4&prerelease
#tool nuget:?package=ModelingEvolution.Booking&version=1.0.0-preview.4&prerelease
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
Three hazards, stated: (1) ICalendarService.CreateEventAsync — the 8-argument member (with transactionId) is
the abstract one; the 7-argument call is a default-interface convenience forwarding transactionId: null. (2) An
implementer written before the key does not compile — add the parameter and honour it (same key ⇒ same event).
(3) If you MOCK ICalendarService (NSubstitute etc.), set up the 8-argument overload: production code calls the
keyed member; a setup on the 7-argument overload is never hit and your test sees an empty event id.
preview.3: MeetingIntent.InviteName/InviteEmail (init) — the calendar invite only, never on MeetingBooked
(a delegated buyer: Name/Email = null, Invite* = the address you hold and can erase); IBookingMaterialiser.CancelAsync(id, reason)
(calendar delete → release AS THE GATE → MeetingCancelled; idempotent; nothing-booked releases and appends nothing);
AvailabilitySyncChecked beat every sync on its own category + AvailabilitySyncStatusModel (LastSyncAt,
LastChangeAt, IsFresh(now, threshold)) — register it End−1 beside your read models; TimeProvider honoured by
AvailableSlotsModel and the arbiter (the host's registration wins; TryAddSingleton(TimeProvider.System) otherwise).
Hazard 4 — End−1 is for single-type models. AddSingletonEventHandler<T>(FromRelativeStreamPosition.End - 1)
subscribes T's event types as ONE joined stream and starts one event before its end: a model folding two types gets
exactly ONE event — whichever landed last — so a config written after the last sync left the slots model with no
bitmap and no dates (found on the ERP host, §6.105). Read models never depend on other read models, so
AvailableSlotsModel (folds BookingConfigured + AvailabilitySynced) is registered from START — the stream is
a handful of small events. If you write your own multi-type model, register it from Start too; the package pins
"every End−N registration folds ≤ N types" (EndMinusOneJoinTests, container-derived) and proves both arrival
orders on a real store.
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.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MicroPlumberd (>= 1.2.2)
- MicroPlumberd.Services (>= 1.2.2)
- MicroPlumberd.SourceGenerators (>= 1.2.2)
- ModelingEvolution.JsonParsableConverter (>= 1.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-preview.11 | 25 | 8/19/2026 |
| 1.0.0-preview.10 | 28 | 8/19/2026 |
| 1.0.0-preview.9 | 34 | 8/19/2026 |
| 1.0.0-preview.8 | 55 | 8/17/2026 |
| 1.0.0-preview.7 | 51 | 8/17/2026 |
| 1.0.0-preview.6 | 45 | 8/17/2026 |
| 1.0.0-preview.5 | 48 | 8/17/2026 |
| 1.0.0-preview.4 | 53 | 8/17/2026 |
| 1.0.0-preview.3 | 51 | 8/17/2026 |
| 1.0.0-preview.2 | 56 | 8/17/2026 |
| 1.0.0-preview.1 | 52 | 8/17/2026 |