Binary rule packs
A binary rule pack (.bcal) is the compiled form of a notable-date document: a compact, integrity-checked, sealed encoding of a validated NotableDateResource. Packs are written at build/authoring time and loaded at run time without parsing or semantic validation - the trim- and AOT-friendly load path for calendar data.
When to use a pack
- Trimmed / native-AOT deployments. Loading a pack touches no XML/JSON machinery and no reflection; every strategy is reconstructed through a closed table of one-byte discriminators. This is the data-driven alternative the plugin loader's
[RequiresUnreferencedCode]annotations point at. - Startup cost. A pack skips the parse → import-resolution → validation pipeline entirely; only bounds checks and an integrity digest run at load.
- Sealed deployment artifacts. A pack cannot carry anything the shipped engine does not already implement, and any corruption or tampering fails the payload digest.
Producing and loading packs
// Compile: author (or load) a document, then save it as a pack. Build() runs first,
// so only content that passed the canonical loader's validation is ever encoded.
NotableDateDocumentBuilder builder = NotableDateDocumentBuilder.Create("corp.holidays")
.AddNotableDate("company-day", "Company Day", NotableDateCategory.Observance, d => d
.AddRule("default", r => r.Fixed(3, 14)));
builder.Save("corp-holidays.bcal"); // extension selects the binary format
builder.SaveBinary("corp-holidays.bcal"); // or explicitly
// Load: the runtime-side entry point skips parsing and validation.
using FileStream stream = File.OpenRead("corp-holidays.bcal");
NotableDateResource resource = NotableDateResourceLoader.LoadBinary(stream);
INotableDateService service = new NotableDateService(resource);
The symmetric low-level surface is NotableDateBinaryResource - Write(resource, stream) / Read(stream) - usable with any already-built resource, including the bundled catalogues.
A pack is compiled output, not an authoring source: NotableDateDocumentBuilder.Load rejects .bcal paths with NotSupportedException. Keep the XML/JSON document as the editable source of truth and recompile.
Guarantees
| Guarantee | Meaning |
|---|---|
| Pre-validated content | The writer only accepts a built NotableDateResource; SaveBinary runs Build() (the canonical loader) first, so an invalid document can never reach a pack. |
| Byte stability | The same resource always encodes to the same bytes - dictionary content is key-sorted and string interning follows deterministic traversal - so build systems can rely on pack outputs for up-to-date checks. |
| Integrity | The header carries a SHA-256 digest of the payload; any corruption or modification fails the load before content is interpreted. |
| Sealed | The reader rejects unknown format versions, unknown discriminators, undefined enum values, out-of-range string references, truncation at any byte, trailing bytes, and values outside a model constructor's domain - always as NotableDateBinaryFormatException (a FormatException), never as an unrelated failure. |
| Behavioural fidelity | A round-tripped resource resolves identically to the original; the test suite pins resolved-occurrence parity for every bundled catalogue and a synthetic document covering every strategy, recurrence, and duration type. |
Format layout (version 1)
Multi-byte integers are little-endian. varuint is the 7-bit variable-length encoding; signed values use ZigZag over varuint. Strings are referenced by 1-based index into a deduplicating table (index 0 = null); dates are day numbers.
header := "BCAL" version:u16 flags:u16 payload-sha256:32B
payload := string-table body
string-table := count:varuint { length:varuint utf8-bytes }*
body := resourceId schemaVersion resolution-policy
adjustment-policies:count-prefixed
notable-dates:count-prefixed
Rules encode nullable fields as presence bytes and their occurrence source as a marker byte (1 = calculation strategy, 2 = recurrence) followed by a discriminator byte and that type's fields. The discriminator tables enumerate the engine's 13 calculation strategies, 4 recurrence strategies, and 2 duration definitions exhaustively; additions require a new format version, which this reader rejects.
Compiling packs from the command line
The bodu-calendar dotnet tool wraps the same compile pipeline for scripts and CI - it validates a notable-date document with the stable BODU-CAL-* diagnostics and compiles it to a sealed pack without writing any C#:
# Not on nuget.org: pack the tool from a clone and install it from that local feed.
dotnet pack Bodu.Globalization.Calendar.Tool/src -c Release -o ./artifacts
dotnet tool install --global --add-source ./artifacts Bodu.Globalization.Calendar.Tool
bodu-calendar lint holidays.xml
bodu-calendar compile holidays.xml -o holidays.bcal
bodu-calendar info holidays.bcal
lint reports every diagnostic in collect mode, compile refuses to write a pack from a document that fails validation, and info prints a compiled pack's header, counts, and integrity digest.
Compiling packs during build
The Bodu.Globalization.Calendar.Build package adds MSBuild integration - a development dependency that compiles NotableDatePack items to .bcal incrementally on every build via the bundled bodu-calendar tool, with no runtime reference added to the consuming project:
It is not published to nuget.org, so reference the project from a clone:
<ProjectReference Include="../Bodu.Globalization.Calendar.Build/src/Bodu.Globalization.Calendar.Build.csproj"
ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
<ItemGroup>
<NotableDatePack Include="rules\holidays.xml" />
<NotableDatePack Include="rules\corporate.json" ResolverDir="rules\shared" />
</ItemGroup>
Each item compiles to $(NotableDatePackOutputPath)<Filename>.bcal (defaulting under the intermediate output path) and is copied to the project output directory; set NotableDatePackCopyToOutput to false to keep packs out of the output folder.
Where to go next
- Builder round-trip guarantees - the XML/JSON serialization contract the pack compiler builds on.
- Authoring with the notable-date builder - producing the documents packs are compiled from.
- Calendar plugin trust - why data packs are the AOT-compatible alternative to code plugins.
- Globalization & Calendars guides - every guide in this topic: the runtime, companions, data packs, and the notable-date catalogue.