Table of Contents

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