ModelingEvolution.Booking 1.0.0-preview.6

This is a prerelease version of ModelingEvolution.Booking.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package ModelingEvolution.Booking --version 1.0.0-preview.6
                    
NuGet\Install-Package ModelingEvolution.Booking -Version 1.0.0-preview.6
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ModelingEvolution.Booking" Version="1.0.0-preview.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ModelingEvolution.Booking" Version="1.0.0-preview.6" />
                    
Directory.Packages.props
<PackageReference Include="ModelingEvolution.Booking" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ModelingEvolution.Booking --version 1.0.0-preview.6
                    
#r "nuget: ModelingEvolution.Booking, 1.0.0-preview.6"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ModelingEvolution.Booking@1.0.0-preview.6
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ModelingEvolution.Booking&version=1.0.0-preview.6&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ModelingEvolution.Booking&version=1.0.0-preview.6&prerelease
                    
Install as a Cake Tool

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_contended; another hold overlapping ⇒ 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_unavailable | booking_moved | slot_taken | slot_contended | 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.

Hazard 6 — ReadEventsOfType<T>(stream, maxCount) caps the events READ, not the matches. MicroPlumberd applies maxCount to the store read BEFORE the type filter: ReadEventsOfType<MeetingCancelled>(booking, maxCount: 1) reads MeetingBooked@0 and stops — never true — so preview.≤4's CancelAsync appended a second MeetingCancelled on a second cancel (found by dev-1b on the ERP, test-1's double cancel). preview.5 reads the tiny Booking-{id} stream without a cap; RealStoreCancelIdempotencyTests cancels twice on a real store and counts ONE. Do not put a maxCount on a typed read unless the type is the stream's first event AND you can prove it stays so.

preview.5: IBookingSlotArbiter.TryGetHoldAsync(id) — a PURE READ of the window this booking holds (reserved-and- unexpired or confirmed) or null; no reserve, no append (§6.103/§6.104: the worker that must know "does THIS booking hold THIS slot" before materialising asks here instead of probing ReserveAsync). AddBookingReadModels() is public — the ONE registration of the five read models (BookingConfigModel and AvailabilitySyncStatusModel End−1; AvailableSlotsModel, BookingCalendarModel, BookingLookupModel from Start). A host composing a la carte calls it rather than mirroring the lines; AddBooking() calls it too.

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.

preview.6: BookingFault codes split (rev-b2's S6_91 finding): slot_unavailable (not offered / not free in the availability model), booking_moved (this booking holds another window — a booking does not move), slot_taken (another booking's hold overlaps — the genuine contest; the word an ERP maps stays), slot_contended (concurrent writers, retry exhausted); the translated exception rides on BookingFaultException.Cause. ExternalReference moved to the ModelingEvolution.Tags package (namespace ModelingEvolution.Tags, was ModelingEvolution.Booking.ValueTypes) — ONE tag type across Booking and Chat (a channel's DefineChannel.Tags is the same type, so BookingLookupModel.ByTag(offer:…) and ChannelReadModel.ByTag(offer:…) are one question). Rules unchanged; consumers change one using. Booking depends on the package.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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