Skip to content

v1.3.0

Latest

Choose a tag to compare

@bslater bslater released this 16 Oct 01:50
613759c

1.3.0 is the next lock-step release. Every Stable package moves to 1.3.0, including the three that shipped ahead of the lock-step version: Bodu.Core 1.0.1, Bodu.Security.Cryptography 1.2.0 and Bodu.Globalization.Calendar 1.0.1 rejoin it here. The version skips 1.1.0 and 1.2.0 because Bodu.Security.Cryptography already holds both on nuget.org. Ten packages that shipped on the preview stream move to Stable at 1.3.0 (see Promoted to Stable), and the three Experimental exchange-rate providers move from 0.7.1 to 0.7.2 (see Experimental packages).

dotnet add package Bodu.Core --version 1.3.0

Compatibility

  • One breaking change: WeekPattern is now DayOfWeekSet. Bodu.Core replaces WeekPattern with DayOfWeekSet, and every member of Bodu.Core, Bodu.Globalization.Calendar and Bodu.Globalization.Calendar.Builder that took or returned a WeekPattern takes or returns a DayOfWeekSet. Code that names WeekPattern, or calls one of those members, must be updated and recompiled; the breaking change below maps each member. A Stable package does not normally break in a minor release; this one does, deliberately. Text that a WeekPattern wrote, including the workingDays attributes of calendar documents, parses unchanged, and compiled .bcal packs load unchanged.
  • Otherwise, additions only. Apart from that change, every Stable package's public API is that of its previous release (1.0.0, or 1.0.1 and 1.2.0 for the three above), with the additions listed under New, on both net8.0 and net10.0. ApiCompat, run against each package's latest version on nuget.org, finds no other removal.
  • Behaviour changes in the format libraries. The readers, writers and serializers for Bencode, TOML, YAML, CSV, dotenv, INI and EditorConfig, and the compound-file and .xls readers, fix the defects that other libraries' release notes led to. Some of the fixes reject input that 1.0 accepted, read input differently, or write output that 1.0 wrote differently; each is marked Behaviour change under the format libraries.
  • Behaviour changes found by the documentation review. Where a package's documentation described the sensible behaviour and its code did not, the code now does what the documentation says. Some of these fixes throw where 1.0 returned, return where 1.0 threw, or read or write differently; each is marked Behaviour change under Found by the documentation review.
  • Source compatibility. A literal null passed to BencodeElement.GetProperty or TryGetProperty no longer compiles: the new UTF-8 overloads make the call ambiguous, as it is for JsonElement.GetProperty. Cast it to string.
  • Trimming and native AOT. BencodeSerializer's public methods now carry [RequiresUnreferencedCode] and [RequiresDynamicCode], and YamlSerializer's, which carried only the first, now carry both, as the other format serializers' do. The converter factories of Bodu.Numerics.Serialization.Json and Bodu.Financial.Serialization.Json, with the methods that register them, carry [RequiresDynamicCode]. A trimmed or native-AOT application now gets IL2026 or IL3050 where it calls them, and each factory's message names the closed converter to register instead. Nothing else changes.
  • Dependencies. Each package's Bodu dependencies now require 1.3.0, as the lock-step model intends. No other dependency changed.
  • Ten packages promoted to Stable. Bodu.IO.Pst, Bodu.Formats.Outlook, Bodu.Formats.Outlook.Msg and Bodu.Formats.Outlook.Pst, and the BoE, ECB, RBA, Fixer, exchangerate.host and FRED providers, move from 0.7.1 to 1.3.0, and from this release keep the Stable promise. Their changes since 0.7.1, breaking ones included, are listed under Promoted to Stable.
  • Experimental packages. The IMF, Yahoo and OFX providers, which read feeds their operators do not publish as APIs, are retiered from Preview to Experimental and move to 0.7.2; they depend on the 1.3.0 Stable packages. No 0.7.1 package uses WeekPattern or a member that changed with it, so 0.7.1 also runs against 1.3.0 as it does against 1.0.0.

Breaking change: WeekPattern is now DayOfWeekSet

DayOfWeekSet keeps WeekPattern's presets and its seven-character text forms (_MTWTF_, MTWTF__, 0111110 and the placeholder variants), so masks that WeekPattern wrote parse unchanged. The rest takes the shape of the other calendar value sets (see New):

1.0 1.3.0
WeekPattern DayOfWeekSet
WeekPattern.AllDays DayOfWeekSet.All
Empty, Weekdays, Weekend and the regional presets, as static fields the same names, as static properties
new WeekPattern(string) DayOfWeekSet.Parse(string)
pattern[day] set.Contains(day)
Contains(day) throws for a value that is not a day Contains(day) returns false for it
FromByte(byte), ToByte(), ToInt32(), with Sunday at bit 6 FromUInt64(ulong), ToUInt64(), with Sunday at bit 0
the implicit conversion to byte, and IEquatable<byte> removed: use ToUInt64()
<, <=, >, >= and IComparable removed: the order meant nothing as a relation between sets
[Serializable] and ISerializable removed: store the text form
ToString(IFormatProvider) ToString()
an unsupported format in ToString throws ArgumentException throws FormatException; a null or empty format writes the default form
ToWeekPattern() on WorkingDaysOfWeek ToDayOfWeekSet()
ToWeekPattern() on IWeekendDefinitionProvider ToWorkingWeek(), named for what it returns: the working days, the complement of the provider's weekend
the WeekPattern overloads of IsInWorkingWeek, IsRestDay, NextWeekday and PreviousWeekday on DateTime and DateOnly, and the WeekPattern receivers of ToWorkingDaysOfWeek and TryGetWorkingDaysOfWeek the same members over DayOfWeekSet
Bodu.Globalization.Calendar's WeekPattern? workingWeek parameters on the working-day extensions, ResolutionPolicy.WorkingWeek and its constructor parameter, and Bodu.Globalization.Calendar.Builder's ResolutionPolicyBuilder.WithWorkingWeek the same members over DayOfWeekSet

A WeekPattern.ToByte() value stored outside a calendar pack must be converted: bit 6 - (int)day held the day, so reverse the seven bits, or rebuild the set from the days whose bit is set. The DayOfWeekSet guide has the same table.

New

  • Bodu.Core: calendar value sets. MonthSet, DayOfMonthSet, HourSet, MinuteSet and SecondSet are immutable sets of months (1 to 12), days of the month (1 to 31), hours (0 to 23), minutes and seconds (0 to 59), and DayOfWeekSet, which replaces WeekPattern, is the same over DayOfWeek. Contains is a range check and a bit test and returns false outside the range; With and Without return new sets; |, &, ^ and ~ combine them; and foreach yields the values in ascending order without allocating.
    • Text. The numeric sets write a list of values and ranges such as 1-3,12 by default, every value with format L, or binary text, one digit per value, with format B; MonthSet also writes a mask of the months' initials such as JFM________D with format J. Parse and TryParse tell the forms apart, and ParseExact and TryParseExact read the one you name.
    • Spans and UTF-8. TryFormat writes any of the forms into characters or UTF-8 bytes without allocating, Parse and TryParse also read spans of characters or UTF-8 bytes, and ParseExact and TryParseExact read spans of characters (#813).
    • One interface. All six implement ICalendarValueSet<TSelf, TValue>, so a generic method can take any of them: Empty, All, FromUInt64, ParseExact and TryParseExact are static abstract members, with Count, Contains, With, Without, ToUInt64, ToString(string), the operators, and IParsable<TSelf>, ISpanParsable<TSelf>, IUtf8SpanParsable<TSelf>, IFormattable, ISpanFormattable and IUtf8SpanFormattable.
    • One layout. Each set is a single 64-bit value. ToUInt64 returns its bits, bit n for the value at offset n of the range, and FromUInt64 takes them back, throwing ArgumentOutOfRangeException for a bit outside the range.
  • Bodu.Core: ThrowHelper.ThrowIfBitsOutsideMask<T> throws ArgumentOutOfRangeException when a value sets a bit its mask does not, for any IBinaryInteger<T>. Unlike a comparison with the largest value the mask allows, it holds for a mask with gaps.
  • Bodu.Core: weekday-in-month helpers for dates. IsNthDateOfWeekInMonth(DayOfWeek, WeekOrdinal) on DateOnly and DateTime tells whether a date is the first to fifth, or the last, occurrence of a weekday in its month, returning false for a fifth occurrence the month lacks. NearestWeekdayInMonth() moves a Saturday to the Friday before it and a Sunday to the Monday after it, without leaving the month, as cron's W token does. DateTime.TryToDateTimeOffset(TimeSpan, out DateTimeOffset) is the non-throwing counterpart of ToDateTimeOffset. CronExpression now uses all three.
  • Bodu.Financial.ExchangeRates: WebRateProvider.LoadFileFeedAsync<TTable>. A protected helper for providers that download a whole file feed. It runs the coverage check, the download, the logging of expected and unexpected failures, the timestamp, and the publish under the provider's lock, leaving the parsing and the provider's own logging to delegates. The BoE, ECB, RBA and IMF providers load through it.
  • Bodu.Globalization.Recurrence: sub-daily recurrence rules enumerate. RecurrenceRule and RecurrenceSet answer GetOccurrences, GetNextOccurrence and GetPreviousOccurrence for FREQ=HOURLY, MINUTELY and SECONDLY, which until now parsed and round-tripped but threw NotSupportedException when queried. BYMINUTE and BYSECOND expand an hourly period and BYSECOND a minutely one; BYHOUR, the finer parts and the date parts limit which periods produce occurrences, as RFC 5545 directs. The answers match RFC 5545's examples, libical's counts, and python-dateutil's output for 200 generated rules. A query passes over the days and times of day a rule rejects without visiting the periods between them, so FREQ=SECONDLY;BYHOUR=9;BYMINUTE=30;BYSECOND=0, one second a day since 2000, answers its next or previous occurrence in about 0.2 microseconds. Code that caught the exception to detect a sub-daily rule now gets occurrences instead.
  • Bodu.Globalization.Recurrence: cron accepts the Quartz day tokens. CronExpression parses L (the last day of the month), L-n (n days before it), nW (the weekday nearest day n), LW and L-nW in the day-of-month field, dL (the month's last weekday d) and d#k (its k-th) in the day-of-week field, and ? for * in either, all of which it rejected with NotSupportedException before. The grammar and answers are Cronos's, held to its 319 token vectors. Two Vixie rules still apply: weekday numbers run from 0 (or 7) for Sunday, not from Quartz's 1, so FRI#3 is the portable spelling of the third Friday; and a token beside values in the other day field combines with them by union, as two restricted day fields always have. A token where none is allowed, such as L-31 or 1,15W, fails with FormatException like any malformed field.
  • Bodu.Globalization.Recurrence: the rule builder takes the calendar value sets. RecurrenceRuleBuilder gains BySecond(SecondSet), ByMinute(MinuteSet), ByHour(HourSet), ByDay(DayOfWeekSet), ByMonthDay(DayOfMonthSet) and ByMonth(MonthSet), one overload per part, each writing the set's values in ascending order (BYDAY Monday first, as RFC 5545 lists the days). An empty set throws ArgumentException naming the part. The int overloads stay, for a month day counted from the end and for second 60.
  • Bodu.Globalization.Calendar: JSON documents take an algorithm registry. NotableDateResourceLoader.LoadJson and TryLoadJson gain overloads that take an INotableDateAlgorithmRegistry, as the XML overloads do, so a JSON rule can name a custom algorithm key, where every JSON load passed no registry and such a rule failed with BODU-CAL-ALGORITHM (#1099).
  • Bodu.Text.Bencode: dictionary keys by their bytes. BencodeProperty.GetNameBytes() and NameEquals(ReadOnlySpan<byte>), and BencodeElement.GetProperty(ReadOnlySpan<byte>) and TryGetProperty(ReadOnlySpan<byte>, out BencodeElement), read and match a key exactly, so a key that is not text, such as the info hash that keys a tracker's scrape response, no longer has to go through a string (#818).
  • Bodu.Text.Configuration: ConfigurationKeyOptions.LowercaseKeys lowercases keys with the invariant culture. The EditorConfigCompatible profile sets it, as the EditorConfig specification requires (#866).
  • Bodu.Text.Delimited: CommentChar on DelimitedWriterOptions and DelimitedSerializerOptions names the comment character, # by default, so the writer can quote a first field that begins with it (#847).

Behaviour changes

  • Bodu.Core: ThrowHelper.ThrowIfNotEqual<T> throws ArgumentOutOfRangeException rather than ArgumentException (#739). The new type derives from the old, so catch (ArgumentException) still catches it; only a check for the exact type sees the change.
  • Bodu.Globalization.Calendar.Plugins: AssemblyName may be null. PluginMissingAttributeException.AssemblyName and PluginNotTrustedException.AssemblyName are now annotated string? (#714). Code that dereferences them without a check may see a new nullable warning.

Fixed

  • Bodu.IO.Hashing: NonCryptographicHashAlgorithmExtensions.ComputeHashAsync(Stream) now resets the algorithm before reading, as ComputeHash(Stream) does, so data appended before the call no longer enters the digest (#741).
  • Bodu.Globalization.Recurrence: RecurrenceRuleBuilder.ByDay and WithWeekStart throw ArgumentOutOfRangeException for a weekday that is not defined, and ByDay for an ordinal outside 0 and ±1 to ±53, as the rule guide says. They accepted such values and wrote rule text that did not describe the rule (#777). For a value of -2147483648, RecurrenceRule.TryParse and RecurrenceSet.TryParse return false, Parse throws FormatException, and the builder's signed parts throw ArgumentOutOfRangeException, where each threw OverflowException (#783).
  • Bodu.Globalization.Calendar: for an ordinal or count of int.MinValue, the nth-weekday and working-day-in-month strategies return no date, and the AddWorkingDays extensions throw ArgumentOutOfRangeException, as they do for its neighbour; each threw OverflowException (#784).
  • Bodu.Globalization.Recurrence: WeekDayNum.ToString() returns the BYDAY token it stands for (MO, -1FR), where it returned the record's generated text, and RecurrenceRule writes BYDAY through it (#794).
  • Bodu.Globalization.Recurrence: CronExpression.GetNextOccurrence and GetPreviousOccurrence return null near either end of the calendar, where a search that would step past 9999-12-31 or before 0001-01-01 threw ArgumentOutOfRangeException, and the DateTimeOffset overloads return null for an occurrence whose UTC instant falls outside DateTime's range (#798). Their searches also scan 400 years, one whole cycle of the Gregorian calendar, rather than twelve, so an expression whose occurrences are decades apart gets its answer rather than null: 0 0 * 2 MON#5, the 29th of February when it is a Monday, fires next on 29 February 2044 after 2016. null now means that no occurrence follows (or precedes) anywhere in the calendar (#799).
  • Bodu.Security.Cryptography: ICryptoTransformExtensions.Transform(Stream, Stream, int) and TransformAsync no longer write a finalized block to the target when the transform fails or is cancelled. A call cancelled before it starts writes nothing, and one cancelled or faulted midway leaves only the whole blocks already transformed (#740).
  • Bodu.Numerics.Serialization.Json: the native-AOT registration the documentation gave, AddNumericsJsonConverters() on the options of a source-generated context, published without a warning and threw NotSupportedException on the first Fraction<T> it serialized, because each converter factory closes its converter with MakeGenericType. The factories and AddNumericsJsonConverters now declare [RequiresDynamicCode], and the documentation and the NativeAOT smoke test register the closed converters, such as FractionJsonConverter<T>, instead (#814).
  • Bodu.Text.Bencode and Bodu.Financial.Serialization.Json: BencodeSerializer and the Money<TCurrency> converter factory gave a trimmed or native-AOT application no warning, and could fail at run time. BencodeSerializer's public methods and BencodeSerializerOptions.GetConverter now declare [RequiresUnreferencedCode] and [RequiresDynamicCode], and the factory, AddFinancialJsonConverters and AddFinancialJson declare [RequiresDynamicCode]; the Financial JSON guide shows the native-AOT registration (#809).
  • Bodu.Text.Yaml: YamlSerializer gave a native-AOT application no warning, although it closes its enumeration, integer, collection, dictionary and nullable converters with MakeGenericType at run time. Its public methods and YamlSerializerOptions.GetConverter, which carried [RequiresUnreferencedCode], now declare [RequiresDynamicCode] too (#1022).

The format libraries, held to other libraries' fixes

Each reader of a text format, and the compound-file and .xls readers, was held to every defect fix in the release notes of the other projects that read the same format: 56 projects for the text formats, from BencodeNET and libtorrent to libyaml, CsvHelper, python-dotenv, configparser and the EditorConfig cores, five for compound files, among them OpenMcdf and olefile, and six for .xls workbooks, among them xlrd, ExcelDataReader and Apache POI. Each fix became a row in a catalogue under corpus/, classed by whether it applies to Bodu, meets a dialect Bodu documents differently, or concerns something Bodu does not have, always with the reason. The rows that apply run in each package's tests. The 158 defects that they and the reviews that followed found, in the code and in its documentation, are fixed below, with two further alignments with System.Text.Json (#1020, #1048); each fix to the code came after a regression test that failed without it. A fix that rejects input 1.0 accepted, reads valid input differently, or writes differently is marked Behaviour change.

Bodu.Text.Bencode

  • Behaviour change: Utf8BencodeReader rejects empty input, as BencodeDocument and BencodeNode already did, so BencodeSerializer reports empty input as BencodeFormatException where it threw BencodeSerializationException (#822).
  • Behaviour change: serializing a null root value throws BencodeSerializationException from every overload except SerializeToNode, which returns null. A string root threw ArgumentNullException, a Nullable<T> root InvalidOperationException, and a class root wrote nothing (#823).
  • Behaviour change: a dictionary key that is not valid UTF-8 is refused wherever it must become a string: BencodeNode.Parse throws BencodeFormatException, and the serializer's string-keyed and char-keyed dictionaries and extension data throw BencodeSerializationException. Such keys were merged silently. The reader and BencodeDocument read any key exactly, and the new byte members reach it (#818, #819).
  • Behaviour change: a case-insensitive BencodeNode.Parse that meets two keys differing only in case throws, where the second replaced the first (#861).
  • Utf8BencodeReader.ValueSpan holds the current token only: an integer's digits, and an empty span for a container token. It could hold the previous string's bytes (#820).
  • The reader's unexpected-token message is formatted, and an out-of-range integer names the range the reader supports (#821).
  • Behaviour change: a well-formed integer above long.MaxValue bound to a signed or narrower member, Int128 included, throws BencodeSerializationException, where it threw BencodeFormatException, which is reserved for bytes that are not Bencode. Every integer that does not fit its member reports its BytesOffset, and the converter guide lists nuint with ulong, on the unsigned surface it reads and writes through (#1005, #1011, #1013).
  • A failed add, insert or construction of a BencodeObject or BencodeArray leaves every value with the parent it had (#862, #909).
  • The README, the concepts page and the sample README are corrected where they contradicted the library, and say that a null list element throws, since dropping it would shift the elements after it (#824).

The Bencode, TOML and YAML serializers

These fixes are in the object binding the three serializers share.

  • Behaviour change: ObjectCreationHandling.Populate fills a member of a class or struct type through the instance it already holds, as System.Text.Json does, where it read a new instance, and dropped it when the member had no setter. YAML now also populates settable members and get-only dictionaries, as TOML and Bencode did. Where Populate cannot apply, as to a fixed-size or read-only collection, the value is replaced rather than the BCL's NotSupportedException thrown (#827, #828, #829).
  • Behaviour change: a delegate, MemberInfo (such as Type) or pointer member fails with NotSupportedException naming its type, where it was serialized as an object of its own members (#859).
  • [Include] takes effect on a property with no public accessor, including one a base class declares (#826).
  • A member that hides a base member with new maps to its most derived declaration, where the type was rejected for mapping two members to one key (#857).
  • In TOML and YAML, a missing required member's exception Path names the member: Name at the root, Inner.Name below it (#858).
  • Behaviour change: an integer outside the range of an enum's underlying type is a serialization error, where it was narrowed into another value: for an enum based on byte, 300 bound as 44 and -1 as 255. A value of an enum based on ulong above long.MaxValue is written as each format writes such a ulong member, where every format threw OverflowException: TOML, whose integers are signed 64-bit, now throws TomlSerializationException, and YAML's number enum converter reads the value back (#996, #1003, #1004).
  • Behaviour change: a string enum converter created with allowIntegerValues: false reads and writes names only, as System.Text.Json's JsonStringEnumConverter does from .NET 9, on both target frameworks. A string that holds an integer, such as "2", " +2 " or "Read, 2", is a serialization error rather than the integer it holds, unless it is a member's configured name; in YAML that covers a quoted or !!str-tagged 2 and an integer beyond 64 bits. A value that is neither a member nor a combination of defined flags throws on write, rather than being written as decimal text the converter would then refuse to read (#1016, #1017).
  • Behaviour change: YamlStringEnumConverter writes member names whatever WriteEnumsAsStrings says, as its documentation says; the default enum handling still follows the option. A YAML integer that a converter rejects is reported as not allowed, where it was reported as an undefined value (#1018, #1019).
  • Behaviour change: the string enum converters, and the default enum handling, name flags and members as System.Text.Json's JsonStringEnumConverter does on .NET 10 (#1020). A combination of flags is written with each flag's wire name, the [StringEnumMemberName] name or the naming policy's, so under SnakeCaseLower CanRead | CanExecute is written "can_read, can_execute", where it was "CanRead, CanExecute", and a list of wire names reads back in any order. A wire name binds only in its own case, and a member renamed with [StringEnumMemberName] no longer binds by its CLR name; the CLR name of a member that is not renamed still binds in any case. In YAML a float scalar never binds to an enum, where the default handling bound 2.0 as 2.
  • Behaviour change: an enum used as a dictionary key is written and read by the same names as an enum value, as System.Text.Json writes and reads it as a property name (#1048). A key is written by its [StringEnumMemberName] name, where it was the CLR name, a combination of flags by its flags' names, and any other value as its decimal text. Where a string enum converter handles the key type, keys also follow its naming policy and its rule for integer values. A renamed member's CLR name no longer binds as a key, and a wire name binds only in its own case, while the CLR name of a member that is not renamed still binds in any case. In YAML a key that does not bind raises a YamlSerializationException that names the key, where the message named only the type.

Bodu.Text.Toml

  • Behaviour change: a finite float outside the range of a float or Half member is a TomlSerializationException, where it became an infinity. Utf8TomlReader.TryGetSingle and TomlValue.TryGetValue<float> return false for such a value, and GetSingle throws FormatException. inf, -inf and nan read as before (#831, #910).
  • Behaviour change: a case-insensitive TomlNode.Parse that meets two keys differing only in case throws at the second key, where the second replaced the first (#861).
  • A key error, such as a redefined key or a dotted key over a value, is reported where the key starts, and a misplaced digit separator at the separator, in integers of every radix and in floats (#832, #860).
  • A failed add, insert or construction of a TomlObject or TomlArray leaves every value with the parent it had (#862, #909).
  • A float literal beyond the binary64 range reads as an infinity, as IEEE 754 conversion and Python's tomllib give, and GetDouble and the converter guide now say so (#863).

Bodu.Text.Yaml

  • Documents and indentation: every document of a stream follows Parse's document-end rules, and a byte-order mark may begin any document; a sequence entry's content, block scalars included, is indented from its dash; tabs after node properties and before compact collections follow the specification (#875, #876, #882, #893, #918).
  • Keys: an explicit key's value is optional and may be a compact collection, ? in a flow sequence starts an explicit-key pair, and the JSON-like key rules for : apply. Each key is named by its text as written, so 0x10 and 16 are two keys (#877, #878, #888, #917).
  • Scalars and node properties: plain scalars fold across lines in flow collections, a backslash before a literal tab is the escaped tab, and an escaped UTF-16 surrogate pair decodes to one character. Tag escapes decode, a verbatim tag reads to its closing bracket, and a property belongs to the node that the token after it starts (#880, #884, #887, #891, #892).
  • Behaviour change: input the YAML 1.2 specification forbids is rejected: an empty flow-mapping entry, an implicit key that begins with an indicator or is a flow collection, an implicit key over 1,024 characters, a second anchor or tag on one node, an empty anchor name, and a property written against its content (#879, #881, #885, #886, #889).
  • Behaviour change: only the plain, untagged << is a merge key, a mapping holds at most one, and its value must be a mapping or a sequence of mappings. A merge source's own merges are expanded before it is merged (#894, #972).
  • Typing: under V1_1, integers follow YAML 1.1's patterns, and a float with a misplaced underscore stays a string; a tagged scalar resolves from its text as written; a radix literal beyond the 64-bit range stays a string, as an out-of-range decimal does; and a float literal beyond the binary64 range reads as an infinity, tagged !!float or not (#883, #895, #901, #970, #974, #994).
  • Behaviour change: MaxDepth counts sequences and mappings, as its documentation says, where it counted every node, and exceeding it while deserializing is a YamlSerializationException (#890, #899).
  • Behaviour change: a finite value outside a float member's range is rejected, where it became an infinity (#900).
  • Errors: an undefined alias, a duplicate key, a repeated anchor, a cyclic alias and a complex key are each reported at their position. A binding error is reported at the value, under its keys as the document writes them, including for the root value and for a non-boolean bound to bool (#897, #898, #971, #973, #975, #993, #995).
  • Behaviour change: the writer quotes a string that a YAML 1.2 or 1.1 reader would read as another value, such as 019, 08, yes, 1:30 and <<, and writes a key longer than 1,024 characters as an explicit ? key (#886, #896).
  • The README and guides state the merge-key default, sexagesimal numbers, float overflow, alias keys and the tab rules (#901, #975).

Bodu.Text.Delimited

  • Behaviour change: only the delimiter, a line break or the end of the input may follow a closing quote, after spaces and tabs under TrimFields. Other text makes the record malformed, where the rest of the line was read as a new record, and SkipRecord skips the whole record (#846).
  • Behaviour change: the delimiter, quote and comment characters must be ASCII, not CR or LF, and different from each other. The reader and writer constructors, and the serializer and document methods that create them, throw ArgumentException otherwise, where a non-ASCII character was cut to one byte (#849).
  • Behaviour change: the writer quotes a field the reader would read back differently: a lone empty field, a first field that begins with the comment character, and a field with leading or trailing spaces or tabs (#847).
  • Behaviour change: a field of white space binds null to a nullable value-type member, as an empty field does, except char?, which reads the space (#853).
  • Behaviour change: DateTime and DateTimeOffset members are written in the round-trip O format, and DateOnly, TimeOnly and TimeSpan in invariant round-trip forms, where they were written in the general format (#854).
  • Under TrimFields, spaces and tabs before an opening quote and after a closing one are skipped, so 1, 2, "test" reads test (#848).
  • A field-count error and a duplicate header are reported where the record starts (#850). Under TakeFirst, a later column with a duplicated name is read under an empty name, as under TakeLast (#911).
  • DeserializeAsyncEnumerableAsync yields each record as soon as its line ending is read, reports each error at its line and offset in the whole stream, and keeps a U+FEFF that begins a record in a later buffer (#851, #852, #912).
  • An empty collection writes the header row alone in header mode, from every Serialize and SerializeAsync overload (#855), and a member hidden with new maps to one column (#856).
  • Behaviour change: a record type that is a scalar, such as int, string, decimal, an enum or a date, or a collection, and a string root, raise DelimitedSerializationException from every reflection overload before anything is read or written. They were written as blank rows or as the type's own properties, such as a Length column for List<string>, and read as default values. A record type with no public parameterless constructor raises DelimitedSerializationException on read, where the runtime's MissingMethodException escaped (#1050).
  • A conversion error from a record or section factory reaches the caller as DelimitedSerializationException or IniSerializationException, as the reflection binders report one (#924).
  • The writer's API overview shows the CRLF line endings the writer writes, and the parser policies state the rules for a lone CR, a blank line and an extra field under Ragged (#914, #939).

Bodu.Text.DotEnv

  • Behaviour change: a # after the whitespace that follows = starts a comment, so KEY= # note reads as an empty value; KEY=#note and URL=http://host/#top keep their # (#833).
  • Behaviour change: only whitespace and a # comment may follow a closing quote; other text throws DotEnvFormatException, where it was dropped (#834).
  • Behaviour change: whitespace is any Unicode whitespace character other than CR and LF, wherever the reader skips or trims it, and the writer quotes a value whose whitespace the reader would otherwise trim (#835).
  • Behaviour change: double-quoted values resolve \\, \', \", \a, \b, \f, \n, \r, \t, \v and \$, and a backslash before a line break continues the value; any other escape keeps its backslash (#836).
  • Behaviour change: DotEnvDocument's GetProperty and TryGetProperty return the last definition of a repeated key, as the mutable document and the serializer do (#837).
  • An error reports its real column, counted in bytes, and an unterminated quoted value is reported at its opening quote. LineNumber is the line on which the current token begins, where it was the line after (#838, #905).
  • Non-ASCII text after a backslash in double quotes decodes correctly, an invalid byte becomes U+FFFD without swallowing the closing quote, and a lone CR inside a double-quoted value counts as a line (#902, #903). The invalid-key message names the character as written (#840).
  • Behaviour change: a scalar root, such as a decimal, Guid, DateTime, DateOnly, Uri or BigInteger, or the nullable form of one, raises DotEnvSerializationException on write and on read, where its public properties were written as keys and reading it returned its default value (#1053).
  • The reader options DisallowExportPrefix, DisallowInlineComments and SkipComments, and INI's SkipComments, are documented by what true does: their summaries, and DisallowExportPrefix's value, described the opposite (#1054).

Bodu.Text.Ini

  • Behaviour change: a section name runs to the first ] that only whitespace or a comment follows, so [foo]bar] names foo]bar. Any other text after a header throws IniFormatException, where it was dropped (#845).
  • Behaviour change: Utf8IniWriter, and IniObject.WriteTo through it, throw ArgumentException for text that would read back differently: a value with a line break, or with a leading or trailing space or tab; a key that is empty, has surrounding whitespace, contains = or a line break, or begins with [, ; or #; a section name with a ] that a comment marker follows; and any name that begins with U+FEFF. IniSerializer reports the same as IniSerializationException, naming the section and key. A comment with line breaks is written as one comment line per line (#841, #842, #843).
  • LineNumber is the line on which the current token begins, and a duplicate section or key is reported at its own header or key, where both were reported a line late (#906, #907).
  • The documentation says that the reader trims the spaces and tabs around keys and values, and keeps the rest of a value as written (#844).
  • Behaviour change: a DateOnly, TimeOnly, Int128, UInt128, Half or BigInteger root, or the nullable form of one, raises IniSerializationException on write and on read, as the other scalar roots do, where its public properties were written as global keys and reading it returned its default value (#1053).

Bodu.Text.Configuration and Bodu.Extensions.Configuration.Text

  • Behaviour change: globs follow the EditorConfig grammar under every profile: a bracket expression that holds a / is literal text, a brace group expands only on a top-level comma or a range of two integers, and a glob that starts with / matches a relative target as if it began with / (#864, #867, #868).
  • Behaviour change: EditorConfigCompatible lowercases keys with the invariant culture, and keeps dotted keys whole through the Identity mapping its documentation named. Identity keeps a key exactly as written, where it rejoined it on the first separator, so a:b became a.b (#866, #869, #916).
  • Behaviour change: ConfigurationDocument.Load(path) resolves globs against the full path of the file's directory when no PathRoot is set, as the documentation said it did. Each entry resolved from a document loaded by path reports that path in SourceLocation.Path, which was always null (#915, #922).
  • The preamble's root pair is never resolved as a property, and each segment of a key is trimmed, so a . b is a:b (#870, #921).
  • Parse, TryParse, ParseWithDiagnostics and Load(TextReader) ignore a leading U+FEFF, as Load did for a stream (#865).
  • The documentation no longer says that the Normalized write preset sorts, marks InvalidEscape as reserved, corrects four passages that the code contradicted, and documents the registration methods Bodu.Extensions.Configuration.Text has (#871, #872, #873, #874).

Bodu.IO.Compound

  • Behaviour change: CompoundValidationLevel.Strict rejects every condition [MS-CFB] makes a requirement: in the header's fixed fields, in the FAT and DIFAT tables whole (sectors no chain reaches included), in the header's FAT and DIFAT sector counts, and in each directory entry. The other levels keep reading what they can (#943, #944, #945, #947).
  • Behaviour change: every level reads the mini stream with the 4,096-byte cutoff [MS-CFB] fixes. A header that declares another cutoff is rejected under Strict, and read with 4,096 elsewhere (#943).
  • Below Strict, a DIFAT chain that ends in DIFSECT or FREESECT ends there, and a DIFAT sector count of 0 means no chain. A directory entry with an odd name length is skipped, and Compatible prunes a link to an entry it skipped, where it rejected the directory (#944, #945, #947, #950).
  • A final sector that the file cuts short reads when it holds every byte the stream declares. Minimal zero-pads a stream that runs past the end of the file, and the other levels reject a chain too short for its stream when the stream opens, rather than part way through a read (#946, #951).
  • CompoundFile.Open reads a container from the stream's current position, not from offset 0, under every read strategy (#958), and three failures report the CompoundFileError category their documentation names (#949).
  • Behaviour change: a writable storage throws the documented ArgumentException for a name already taken and KeyNotFoundException for a missing one, where it threw CompoundFileSerializationException. The CreateStream overload that returns a cursor throws for a taken name, where it truncated the existing stream (#948).
  • Behaviour change: a version-3 container larger than 2 GB is rejected before any byte is written, from the builder and from Commit, whose update mode writes version 3, as [MS-CFB] 2.9 requires. A version-4 container larger than 2 GB keeps the range lock sector free of data, and an undefined CompoundFileVersion is rejected at every entry point, where it was written as version 3 (#953, #954, #955).
  • Commit and CommitAsync plan the whole container before they truncate the destination, so a staged tree that cannot be written leaves the destination as it was (#964). Unused directory slots are written as free entries with NOSTREAM links, where their links were zero (#952, #985).
  • Behaviour change: OlePropertySet reads and writes vectors of 1- and 2-byte values packed, as [MS-OLEPS] lays them out, where it padded every element to four bytes; such a vector written by an earlier version now reads its padding as elements (#957).
  • OlePropertySet reads property identifiers unsigned, so PID_LOCALE and PID_BEHAVIOR read, where they threw OverflowException and kept ExcelBinaryWorkbook from opening a workbook whose document summary carries PID_LOCALE. An oversized length is reported as malformed, where it too threw OverflowException (#961).
  • OlePropertySet reads vector strings whether or not they are padded, as [MS-OSHARED]'s document parts and heading pairs are not, and a VT_LPWSTR whose writer aligned the string rather than the property. A property whose type word carries VT_ARRAY, VT_BYREF or a reserved bit is dropped, where it was read as its base type (#956, #962, #963).
  • The documentation no longer calls entry names case-sensitive, since they match without regard to case, or claims a cross-check against OpenMcdf (#959).

Bodu.IO.Biff and Bodu.Formats.Excel.Binary

  • Behaviour change: a sheet's substream ends at the EOF record that matches its own BOF, and a chart substream nested in a worksheet is skipped. An embedded chart no longer ends its worksheet early, so the worksheet's cells after it now appear, and the chart's data cache is no longer read as cells. A chart sheet, macro sheet or module yields no cells, where it yielded its cached chart data (#926, #927).
  • A sheet that has lost its EOF record ends where the next sheet's substream begins, or at the end of the stream, where it read on into the next sheet's cells, and globals that have lost theirs end at the first sheet's BOF. A worksheet's used range comes from its own DIMENSIONS record, never an embedded chart's or the next sheet's (#927, #928, #992).
  • Behaviour change: a workbook whose shared string table declares more strings than it holds opens, as Excel writes such files, where it failed: BiffSstReader ends the table where its records end. It also passes over a CONTINUE record that holds only a continued string's flags byte, and decodes a surrogate pair split across a CONTINUE boundary to its character, where each half became U+FFFD (#935, #937, #1012).
  • A formula's text result is read across its CONTINUE records, where it failed, and after the ARRAY, TABLE or SHRFMLA record of an array, table or shared formula, where it read as an empty string (#929, #930).
  • Behaviour change: IsDateFormatted is true for the Thai built-in formats 71 to 81, and a FORMAT record decides its index's code and date classification, built-in index or not, the last record for an index applying (#931, #932).
  • A nine-byte BOOLERR, the layout OpenOffice.org wrote, reads as its error or boolean, where an error read as TRUE. BIFF8 strings outside the shared string table ignore their reserved flag bits, as [MS-XLS] requires, where a reserved bit was read as a rich-text or extended-data flag (#934, #936).
  • A sheet whose own BOF record declares an unsupported version raises ExcelBinaryUnsupportedException, where the codec's BiffUnsupportedVersionException escaped. The documentation says when a malformed record fails, at open for the workbook globals and when the sheet is read for a sheet, and lists RSTRING among the records handled (#933, #938).

Found by the documentation review

Before this release every package's XML documentation, guides, concept pages, API overviews, READMEs and samples were checked against the code, about 690 findings across fifteen areas, and the code merged for 1.3.0 was reviewed again for duplication and speed. The documentation now says what the code does. Where the documentation described the sensible behaviour and the code did not, the code is fixed below, each after a regression test that failed without the fix; a fix that throws where 1.0 returned, returns where 1.0 threw, or reads or writes differently is marked Behaviour change.

Bodu.Core

  • Behaviour change: GetIsoWeeksInYear, and everything built on it (GetFirstDateOfIsoWeek, GetLastDateOfIsoWeek, and the calendar's ISO-week rules), agree with System.Globalization.ISOWeek in every year. The weekday of 1 January was computed as that of 31 December, so 650 of the years 1 to 9998 had the wrong week count: 2003 had 53 weeks and 2004 had 52, the reverse of the truth (#1062).
  • Behaviour change: invalid arguments throw the documented exception: ToReversed(Array) throws ArgumentException for a multidimensional array, where it threw RankException (#1063); BufferConverter.ToArray throws ArgumentOutOfRangeException for a negative count, where OverflowException escaped (#1064); GetNearestDateOfWeek(year, month, day, dayOfWeek) throws for an invalid date, where month 13 rolled into the next year, 30 February became March and month 0 threw IndexOutOfRangeException (#1068); and ThrowIfSpanLengthNotPositiveMultipleOf(Span) validates its divisor, as its ReadOnlySpan overload does, where 0 threw DivideByZeroException (#1074).
  • Quarter bounds from FiscalWeekQuarterProvider keep the receiver's DateTimeKind (#1065); the default GetFiscalYear accepts a time of day on the last fiscal day (#1066); and quarter bounds late in 9999 are computed, where they threw (#1067).
  • Behaviour change: DateOnly.NextOccurrence(start, interval, after) returns start when after equals it, as the DateTime overload does (#1069).
  • Behaviour change: ToSafePathSegment replaces both / and \ on every platform, where \ survived on Linux and macOS (#1070).
  • Behaviour change: ReverseDigits and the RotateDigits methods throw OverflowException when the result does not fit the type, where they returned a wrapped value (ushort 60009 reversed gave 24470) (#1071).
  • Behaviour change: XorShiftRandom.Next(0) returns 0, and Next(minValue, maxValue) with minValue > maxValue throws ArgumentOutOfRangeException, as System.Random does; it threw, and threw ArgumentException, respectively (#1072).
  • Behaviour change: NearestDateOfWeek and GetNearestDateOfWeek on DateTime and DateOnly return the occurrence in the other direction when the nearer one lies outside the range of the type, where they threw ArgumentOutOfRangeException within six days of either end (#1151).
  • Behaviour change: WithExceptionFallbacks and WithReplacementFallbacks return a clone of the encoding they are given, where they returned the registered encoding for its code page: new UTF8Encoding(false) came back emitting a byte order mark, and an Encoding subclass that is not registered threw NotSupportedException (#1073).
  • Behaviour change: when the last writer queued on an AsyncReaderWriterLock cancels, the readers queued behind it are admitted at once, alongside any readers already holding the lock, where they waited for every active reader to release and readers arriving later went ahead of them (#1184).
  • Behaviour change: NextDateOfWeek, NextOrSameDateOfWeek, PreviousDateOfWeek and PreviousOrSameDateOfWeek on DateTime and DateOnly throw ArgumentOutOfRangeException naming dateTime or date when the occurrence lies outside the type's range, where the exception named ticks or value (#1195).
  • The async primitives allocate less per contended wait (AsyncSemaphore and AsyncAutoResetEvent 144 bytes where 528, AsyncLock 144 where 384 and nothing for a hand-off, the reader-writer lock's reader and writer waits 160 where 448 and 512), a wait whose token cannot be cancelled registers nothing, and TryToDateTimeOffset checks its rules rather than catching an exception, so a failed conversion allocates nothing (#1186).
  • Behaviour change: NthDateOfWeekInMonth and GetNthDateOfWeekInMonth on DateTime and DateOnly, asked for a fifth Saturday, Sunday, Monday or Tuesday of December 9999, throw the documented ArgumentOutOfRangeException naming ordinal, as in every other month, where the runtime's exception named dayNumber or ticks (#1237).

Bodu.Collections

  • Behaviour change: the Deque and CircularBuffer constructors that take a collection validate the capacity before anything else, and EvictingDictionary rejects an undefined EvictingDictionaryPolicy when it is constructed, each throwing the documented ArgumentOutOfRangeException. Before, a capacity of -1 or 0 could succeed, or throw InvalidOperationException, and an undefined policy failed at the first eviction (#1075, #1076, #1077).
  • The Deque collection constructors document that the deque always grows and that its capacity becomes the source's length when that is larger (#1150).
  • The CircularBuffer, Deque and ring-backed collection constructors document their largest capacity (#1194).

Bodu.Security.Cryptography

  • Behaviour change: Shake.HashSize throws ArgumentOutOfRangeException for a size that is not a positive multiple of 8, as its documentation and the constructor do, where it threw CryptographicException (#1059).
  • The padding modes' block-size CryptographicException states the rule, where its message was the parameter name; five more guards that passed an already formatted message through the formatting constructor now pass it as the message (#1058).

Bodu.Financial and Bodu.Financial.Serialization.Json

  • Behaviour change: MoneyParseOptions.RoundTripOnly accepts only the round-trip shape its documentation describes (the uppercase ISO code, one space, an invariant number), read with the invariant culture whatever FormatProvider says, and keeps a scale finer than the currency's minor units (USD 145.678912 keeps six places), where it accepted padding, signs, symbols and the code last, and rounded to the minor units (#1081).
  • Behaviour change: converting a default Money with a decimal rate throws InvalidOperationException, as IsDefault documents (#1082).
  • Behaviour change: ToMoney(), the implicit conversion to Money and ToCalculated() throw InvalidOperationException for a Money<TCurrency> whose tag names no currency in the ISO 4217 catalogue, where they produced a Money with no currency; MoneyBag.Add<T> and Subtract<T> throw it too (#1094).
  • Behaviour change: reading a MoneyBag from JSON honours the policy: Strict rejects a repeated ISO code and a repeated balances property, and Lenient rejects a repeated code and accepts lowercase or padded codes (#1083).
  • Behaviour change: a MoneyBag read under the Compact policy rejects a repeated ISO code as Strict and Lenient do, where it added the balances together (#1142).
  • Behaviour change: MonetaryContext.ResolveScale and Round under ScalePolicy.Custom throw ArgumentOutOfRangeException naming CustomScale for a scale outside 0 to 28, where a negative scale returned the value unrounded and one above 28 named a decimals parameter (#1143).
  • Behaviour change: MoneyFormatterBuilder.WithMinorUnits, and the MoneyFormatter constructor for a MinorUnitsOverride, throw ArgumentOutOfRangeException outside 0 to 28, where every later Format call threw FormatException (#1144).
  • Behaviour change: MonetaryContext.ResolveScale and Round throw ArgumentOutOfRangeException naming currencyMinorUnits for a count outside 0 to 28, under every scale policy, where a negative count returned the value unrounded (#1164).
  • Behaviour change: a CurrencyInfo (constructed, initialized or derived with with) throws ArgumentOutOfRangeException for MinorUnits outside 0 to 28, where such a currency registered at runtime made rounding fail later naming a parameter its caller never passed (#1168).
  • Behaviour change: a CurrencyInfo with a negative cash-rounding increment, or one finer than its minor units, throws ArgumentOutOfRangeException, the rule currency tag types already follow (#1171); and a currency tag with a whole increment at high precision (8 at 28 minor units) is accepted, where its first use threw OverflowException (#1172).

Bodu.Financial.ExchangeRates and its caching packages

  • Behaviour change (Bodu.Financial.ExchangeRates.DependencyInjection): the named HttpClient that each Add<Source>ExchangeRates registers is capped at WebRateProviderOptions.MaxResponseContentBufferSize, 64 MiB by default, as a client the provider builds itself is, where it kept HttpClient's 2 GB limit whatever the options said (#1326).
  • Behaviour change: the undated and timeless lookups of CachingRateProvider and AggregatingRateProvider return the most recent rate on or before today, as IDatedRateProvider documents and the web providers do, where they required a rate dated today and so failed at weekends; CachingRateOptions.DefaultLookupOptions and RateAggregationOptions.DefaultLookupOptions default to PreviousWithin(100000) (#1091).
  • Behaviour change: in range reads, AggregatingRateProvider passes over a child that cannot carry the pair, where its RateSeriesNotFoundException escaped, and the average leaves out an empty candidate, where one empty child emptied the result (#1087, #1088).
  • Behaviour change: an inverted ExchangeRate of zero throws ArgumentOutOfRangeException, where it threw DivideByZeroException (#1089).
  • Behaviour change: AddDistributedRateCache registers one cache per provider name, where a second provider shared the first one's cache and stored its rates under the first provider's key (#1092).
  • Behaviour change: the log level of each ingested observation, and of each cache hit and miss, defaults to Trace, as the guides say, where it was Information (#1095).
  • A caller that retries as soon as it sees a shared fetch fail (or succeed) starts a fresh fetch, where it could rejoin the finished one and be handed its outcome again: the single-flight key is now released before the shared result is published (#1152).
  • The options-validation messages of the caching packages and of every provider registration come from each package's resources; the text is unchanged (#1141).
  • Behaviour change: a file feed (BoE, ECB, RBA, IMF) that quotes a retired or unknown currency skips it and publishes the rest, where the whole load failed, and a load whose publish fails is tried again on the next request, where the failure was kept (#1174).
  • Behaviour change: once a load has covered today, today's rates are fetched again after the new WebRateProviderOptions.RefreshInterval, for the file feeds and the pair providers alike, where they were never fetched again; a non-positive interval fails options validation (#1176, #1241). A day the source returned stays final, so Yahoo's, XE's and OANDA's provisional rate for today is kept, as before.
  • A disk-cache hit keeps the time it was written, so the cache's age counts from then, where it was stamped with the time it was read (#1177), and an IMF report cached while its month was open is fetched again once the month closes (#1240).
  • Experimental providers: the Yahoo, XE, OANDA and OFX parsers pass over a value of the wrong JSON kind as a malformed point, where InvalidOperationException escaped (#1175, #1239); IMF's log event ids no longer repeat RBA's (#1238); RBA and IMF download up to four eras or months at a time, and RBA's PreloadAsync finishes the eras it has started before rethrowing the first failure (#1188).
  • The providers' shared loading code (coalesced loads, coverage, the disk caches, JSON rate values) lives once in Bodu.Financial.ExchangeRates (#1188). New public API, all additive: WebRateProviderOptions.RefreshInterval, DateRangeCoverage.TryGetUncoveredSpan, the protected WebRateProvider.LoadCoalescedAsync, and the stored-at TryGet overloads of IByteCache<TKey> and FileSystemByteCache<TKey>.

Bodu.IO.Compound

  • Behaviour change: property-set ANSI strings are read and written in the section's code page, Windows-1252 and Shift-JIS among them, through the framework's CodePagesEncodingProvider (queried, not registered process-wide), where every code page but UTF-16 and UTF-8 was treated as Latin-1. SummaryInformation, DocumentSummaryInformation and ExcelWorkbookProperties read such text correctly. Code page 0 still reads as Latin-1, so a file decodes the same on every machine (#1085).
  • Behaviour change: once a compound file is disposed, every storage member that opens a child or stages an edit throws ObjectDisposedException, and a writable stream's Write, SetLength and Flush throw too (its CanWrite is false), where a writable file's storages kept staging edits that could never be committed. On a read-only file the TryOpen members throw rather than return false (#1146).
  • OpenStream with FileMode.CreateNew over an existing stream names the stream in its IOException, where the message called the mode unsupported (#1192).

Bodu.Globalization.Calendar

  • Behaviour change (Bodu.Globalization.Calendar.Caching): AddCachedNotableDateService decorates the most recent unkeyed INotableDateService registration and leaves keyed ones in place, undecorated, where a keyed registration made after the unkeyed one was taken as the service to wrap, removed, and left the unkeyed service failing to resolve; with only keyed registrations it throws InvalidOperationException (#1325).
  • Behaviour change: an adjustment policy's nonWorking flag applies to the observed occurrence, and that occurrence claims its day, where the flag had no effect (#1096).
  • Behaviour change: an adjustment scope that names a concept's rule (<Rule notableDateRef="…" ruleRef="…" />) applies to that rule only, where it applied to every rule with that identifier in any concept. A rule pack holding such a scope is written at .bcal format version 2, which earlier readers reject rather than misread; packs without one are unchanged (#1097).
  • Behaviour change: an out-of-range strategy value and a malformed imported document are load diagnostics (BODU-CAL-STRATEGY, BODU-CAL-SYNTAX): Load throws NotableDateValidationException and TryLoad returns false, where ArgumentOutOfRangeException or FormatException escaped both (#1098).
  • Behaviour change: a rule with no occurrence source, or more than one, is an error (BODU-CAL-RULE-SOURCE), where a JSON rule with none fell on 1 January and one with both used its recurrence (#1100).
  • DuplicatePolicy, SpanCollisionPolicy and ObservedDateRangePolicy are read and kept but not yet applied; the documentation now says so (#1119 tracks applying them).
  • Behaviour change: CachingNotableDateService returns every occurrence that overlaps the requested range, as the wrapped service does, where it dropped a multi-day occurrence that began before the range and returned one that crossed a new year twice (#1134).
  • Behaviour change: NotableDateDocumentBuilder.ToXml() declares encoding="utf-8", so the file Save writes loads with XDocument.Load and XmlReader, where its declaration said utf-16 over UTF-8 bytes (#1135).
  • Behaviour change: a PatchRule that names both a strategy and a recurrence, or an occurrence source with no kind, is a BODU-CAL-RULE-SOURCE error in XML and JSON, and an untilDate end strategy with no kind, or with two, is BODU-CAL-DURATION-NOEND or BODU-CAL-DURATION-CONFLICT, where these documents loaded (#1136).
  • Behaviour change: a malformed workingDays value is the new BODU-CAL-WORKING-DAYS diagnostic: Load throws NotableDateValidationException, where FormatException escaped, and TryLoad reports that code (#1137).
  • The caching registrations' option-validation messages and the binary pack writer's NotSupportedException message come from resources and read as sentences (#1138); NotableDateCachingOptions.Validate names CacheMissLogLevel when that is the undefined level (#1139).
  • Behaviour change: under HighestPriorityOnly, CategoryPriority and Custom, a same-day collision is settled over every occurrence emitted on that date, whatever the queried window, so a window's result is the wider resolution restricted to it. An occurrence that loses its date's collision no longer appears in a window that cuts only its later days. A custom INotableDateCollisionResolver receives the whole group for its date, and INotableDateProvider.GetNotableDates may be asked for a range that starts up to one span before the window. KeepAll is unchanged (#1165).
  • Behaviour change: an unrecognized or numeric JSON enumeration value is the new BODU-CAL-ENUM diagnostic, where it loaded the default; member names still match ignoring case (#1166).
  • Behaviour change: workingDays accepts only its seven-digit binary form in both loaders, where JSON also loaded a letter mask such as _MTWTF_ (#1167).
  • Behaviour change: a present JSON integer, Boolean, anchorDate or trigger month that does not parse fails the load with BODU-CAL-VALUE, naming the field and the value, where it loaded the field's default; an XML trigger month does the same. A year-list number in quotes is accepted (#1190).
  • Behaviour change: a JSON list item that is not a string fails the load, with BODU-CAL-ENUM in a list of names and BODU-CAL-VALUE in a list of territories, references, adjustments or tags and in a scope's ruleRefs, where it was dropped (#1203).
  • Behaviour change: a fixed or weekdayNearDate day outside 1-31 fails with BODU-CAL-STRATEGY in both formats, where it was clamped to 1 or 31 (#1204).
  • Behaviour change (Builder): NotableDateDocumentBuilder reads enumeration values, workingDays, integers, Booleans and the trigger month with the loader's rules, and throws FormatException carrying the loader's message for a value the loader rejects, where it read a default, dropped the value or read any Boolean other than true or 1 as false; values the loader accepts, such as JSON "5" or "false", are accepted (#1189, #1202).
  • Behaviour change (Builder): FromJson reads every field LoadJson reads (imports, trigger and action details, handler parameters, scope rule references and year bounds, a PatchRule's applicability, strategy, recurrence, duration, tags and adjustments), and throws NotSupportedException for the four fields its model cannot hold; ToXml and Build emit a PatchRule's recurrence and duration end, and ToJson throws NotSupportedException for them (#1205).
  • Behaviour change: the duplicate, span collision and observed-date range policies that ResolutionPolicy stored now take effect. DuplicatePolicy.KeepFirst, KeepLast and Merge settle a repeated notable date in the loaded document (a colliding AddRule stays BODU-CAL-DUP-RULE); a span policy other than KeepAll settles chains of overlapping multi-day occurrences, widening the years it gathers by up to ten while a chain stays open; and ActualOccurrenceControlsInclusion and BothOccurrencesControlInclusion include an occurrence by its actual span. A resource with the default policies resolves exactly as before, and a rule pack that sets a non-default span or observed-date policy is written at .bcal range-policy version 3. CachingNotableDateService clips cached years by the wrapped service's observed-date policy (#1119).
  • Behaviour change: a JSON scalar property that holds an object or array fails the load with BODU-CAL-VALUE, where it was read as absent (#1217).
  • Behaviour change (Builder): FromJson reads text fields and list items as LoadJson reads them (a number or Boolean in a text field is its JSON text; a non-string list item throws the loader's message), matches strategy and recurrence kinds ignoring case, and rejects a PatchRule occurrence source that names no kind or two, as LoadJson does (#1216, #1218).
  • Behaviour change: the nth-weekday strategies (DayOfWeekInMonthStrategy, RelativeWeekdayInMonthStrategy, MonthlyWeekdayRecurrenceStrategy) yield no date for an undefined WeekOrdinal, or for a fifth weekday that December 9999 lacks, where they returned another year's date or threw ArgumentOutOfRangeException (#1215).
  • Behaviour change: an override's operation is matched ignoring case; an unknown or absent operation is BODU-CAL-ENUM, and an AddRule without its rule is BODU-CAL-SCHEMA, where JSON read either as a PatchRule (#1226).
  • Behaviour change: LoadJson holds identifiers, references, the resource identifier and territory codes to the XSD's patterns (BODU-CAL-SCHEMA), and holds required values, non-empty rules and weekly days, and integers to the XSD's ranges (#1229, #1236). A JSON object or list property of the wrong kind, and a non-object item in a list of objects, are BODU-CAL-VALUE, where they were read as absent or skipped; such messages name the JSON kind rather than quoting the value (#1230, #1234, #1235).
  • Behaviour change (Builder): FromJson matches property names, checks each rule's and untilDate's occurrence source, parses strategy and recurrence values, and applies the required values and ranges as LoadJson does, throwing FormatException with the loader's message, where it accepted documents LoadJson rejects, filled in missing required values or wrote values the loader would read differently (#1227, #1228, #1233, #1236).
  • The JSON schema (NotableDates.schema.json) describes the JSON form LoadJson reads, no longer requires notableDates, and requires an override's rule or ruleRef according to its operation (#1231, #1247).
  • Behaviour change (Builder): NotableDateRuleBuilder.Fixed(int month, ...) on a rule in a calendar other than Gregorian writes the month as its number, which the loader reads, and accepts month 13, where it wrote an English month name the loader rejected; FromJson checks identifiers, references and territory codes against the loader's patterns (#1256, #1258).
  • A fixed month that does not parse in a calendar other than Gregorian names the forms that calendar takes, and a scope rule reference without its ruleRef is reported once in JSON, as in XML (#1257, #1261).
  • The JSON schema describes every member and value LoadJson reads (imports, every calendar, trigger and action, handler parameters, the scope years, the trigger and action refinements, an override's applicability, tags and adjustments) and states the loader's integer bounds (#1259, #1260).
  • Behaviour change: a Fixed rule's month is held to the months its calendar has: 1 to 13 in the Hebrew calendar and in the Chinese lunisolar calendar when the leap month counts, 1 to 12 otherwise, and the Hebrew month names are read in the Hebrew calendar alone. A Hijri or Persian rule naming Nisan, or month 13 in a twelve-month calendar, is now BODU-CAL-MONTH, where it loaded and resolved to another month or never occurred; NotableDateRuleBuilder.Fixed(int month, ...) applies the same bound (#1270).
  • The JSON schema refuses a malformed anchorDate such as 2024-1-1 (#1271).
  • Behaviour change: FixedDateStrategy's constructor applies the loader's month rules: it throws ArgumentOutOfRangeException for an undefined calendar or a month past the calendar's count, and ArgumentException for a Hebrew month alias outside the Hebrew calendar or outside the eight aliases; NotableDateBinaryResource.Read reports a pack holding such a strategy as NotableDateBinaryFormatException (#1275). Hebrew month names match ignoring case in Load, LoadJson and the builder's FromJson, which writes the canonical spelling (#1276).
  • Behaviour change: the strategy constructors apply the loader's rules: DayOfWeekInMonthStrategy, WeekdayNearDateStrategy and RelativeWeekdayInMonthStrategy reject undefined enum values, the four rule-reference strategies reject references that are not identifiers, NthWeekdayFromRuleStrategy and WorkingDayInMonthStrategy reject ordinals outside -366 to 366, and FixedDateStrategy rejects a Gregorian day its month never has; a pack holding such a strategy fails to read (#1280-#1283). NotableDateBinaryResource.Write runs the loader's identifier, duplicate and cross-reference checks and throws NotableDateValidationException before writing a resource the loader would reject; the new Write(resource, stream, algorithms) overload accepts a custom registry's algorithm keys (#1284). Write also refuses a number outside the range the loader accepts, such as a fromYear above 65535 or a trigger day of 32, with the loader's BODU-CAL-SCHEMA diagnostic (#1294), and an enumeration value its type does not define, which it wrote as its low byte, with BODU-CAL-ENUM (#1295).

Bodu.Globalization.Recurrence

  • Behaviour change: RecurrenceRule.Parse, RecurrenceSet.Parse and CronExpression.Parse throw FormatException carrying the defect that TryParse names, where the message was a generic one (#1101).
  • AnchoredInterval.GetOccurrences(anchor, from, to) computes the first occurrence in the window directly, so a window far from the anchor starts at once, where it walked every occurrence from the anchor (#1102).
  • Behaviour change: the DateTimeOffset overloads of RecurrenceRule, RecurrenceSet and AnchoredInterval return null, or end the sequence, at the last occurrence whose UTC instant a DateTimeOffset can represent, where they threw ArgumentOutOfRangeException, as CronExpression already does (#1185).

Bodu.Numerics and Bodu.Numerics.Serialization.Json

  • Behaviour change: Fraction<T>.ToDecimal, the explicit conversion to decimal, TryToDecimal and decimal.CreateChecked succeed whenever the value fits decimal, rounding half to even, where they overflowed (or returned false) when a component was wider than 96 bits (#1103).
  • Behaviour change: Interval<T>.AtLeast, GreaterThan, AtMost and LessThan throw ArgumentException for a NaN endpoint, as the bounded factories do, where they built an interval holding NaN (#1104).
  • Behaviour change: an infinite endpoint beside a closed bracket ([0, Infinity]) parses as a bounded endpoint, and the Strict and Lenient JSON policies write an infinite endpoint as the string "Infinity" or "-Infinity", so such an interval round-trips through text and JSON, where parsing rejected it and serializing threw (#1105).
  • Behaviour change: an unbounded side never holds its infinity, and nothing holds NaN: Interval<double>.All.Contains(double.NaN) and AtLeast(0).Contains(double.PositiveInfinity) are false, where they were true. The interval operations agree with that membership, so All no longer contains or overlaps [0, Infinity], AtLeast(0) intersected with [0, Infinity] is [0, +∞), and Complement no longer emits degenerate pieces. Where an unbounded side meets an open endpoint at the same infinity, the results are unchanged (#1154, #1197).
  • Behaviour change: AtLeast(+Infinity), GreaterThan(+Infinity), AtMost(-Infinity) and LessThan(-Infinity) return Interval<T>.Empty, as inverted bounds already do, where they built an interval that held no value but did not report empty; the bracket text and JSON for those shapes read as the empty interval (#1198).
  • Behaviour change: the interval JSON converters throw JsonException for a NaN endpoint on a bounded side, where ArgumentException escaped (#1155).
  • Behaviour change: Fraction<T> parsing accepts the culture's own negative and positive signs (U+2212 under sv-SE, for one) as well as ASCII - and +, so a negative fraction formatted under any culture parses back; text that parsed before gives the same value (#1156).

Bodu.Text.Encoding

  • Behaviour change: every encoding's IsValid, TryGetDecodedLength and GetDecodedLength accept exactly what its Decode accepts, where they disagreed for some padding, canonical-form and overflow cases (#1107). GetDecodedLength throws FormatException with the decoder's own message.
  • Behaviour change: the Base64 MIME EncodeToUtf8 and TryEncodeToUtf8 methods insert the CRLF line breaks that Encode writes, where their output had none (#1108).
  • Behaviour change: BinaryEncodings.Get("base58-flickr") returns an encoding with Flickr's alphabet, where it returned the Bitcoin one; Base58Variant.Flickr and BinaryEncodings.Base58Flickr are new (#1118).
  • Behaviour change: Base58.GetMaxDecodedLength(n) returns n, an upper bound that holds for input made only of '1', and Base58Check.GetMaxDecodedLength returns max(0, n - 4), where both returned a bound too small for leading zero bytes (so decoding into the documented buffer failed) and overflowed for a large n (#1158).
  • Behaviour change: strictly padded Base64 input shorter than one quantum, such as QQ=, throws the documented FormatException, where it threw OverflowException (#1159).

Bodu.IO.Hashing

  • CrcStandard.FromName and TryFromName resolve the 74 aliases the RevEng catalogue publishes for 39 standards (PKZIP, for one), where they knew only canonical names; names still compare ordinally (#1120).
  • The CrcStandard.Get and FromName exception messages come from the package's resources, so they can be localized; their text is unchanged (#1208).

Bodu.Text.Yaml, Bodu.Text.Toml and Bodu.Text.Bencode

  • Behaviour change: YamlSerializer writes a Uri as its original string and reads absolute or relative URI text back, where it wrote a mapping of the Uri's properties, threw for a relative Uri, and could not read one back (#1106).
  • Behaviour change: a YAML node belongs to at most one container: adding, inserting or assigning a node that is already in a container throws InvalidOperationException, where it was accepted and the node appeared in two places (#1157).
  • Behaviour change: a TOML or Bencode node also belongs to one place: adding, inserting, assigning or constructing with a node that is already in the same container throws InvalidOperationException, where it was accepted twice; assigning a node over itself changes nothing (#1206).
  • Behaviour change: adding a TOML, Bencode or YAML container to itself or to one of its descendants throws InvalidOperationException, as System.Text.Json.Nodes does, where it built a cycle that hung Root and overflowed the stack when written (#1207).

Bodu.Text.Delimited, Bodu.Text.DotEnv and Bodu.Text.Ini

  • Behaviour change: NoHeader deserialization into a class binds its fields by position, in the order Serialize writes them, as the generated factories do, where every record came back with no field set (#1109).
  • Behaviour change: DateOnly, TimeOnly, Int128, UInt128, Half and BigInteger values read back in all three formats, where they were written but could not be read. DotEnv and INI write TimeOnly in its round-trip form, with seconds and fractions, where they wrote hours and minutes, and INI writes the six types as keys, where it wrote them as sections (#1115).
  • Behaviour change: the Delimited reader counts a lone carriage return inside a quoted field as a line end, so LineNumber and the line in DelimitedFormatException count it (#1110); the writer quotes the document's first field when it begins with U+FEFF, which the reader would otherwise strip as a byte order mark (#1112); DelimitedElement's indexer and the root GetArrayLength check disposal and bounds, and GetProperty finds a ragged field by its column index (#1111); and DeserializeAsyncEnumerableAsync checks its arguments when it is called, not at the first MoveNextAsync (#1117).
  • Behaviour change: DotEnv and INI deserialization into an abstract type, or one without a public parameterless constructor, throws DotEnvSerializationException or IniSerializationException, where MissingMethodException escaped (#1114). Utf8DotEnvWriter rejects a name that would not read back, and writes a multi-line comment as one # line per line (#1113).
  • Behaviour change: DotEnv and INI map a member hidden with new once, where they mapped both declarations (#1178); honor the required keyword, and throw DotEnvSerializationException or IniSerializationException when two members map to one name (#1179); read back nint and nuint members (#1180); and write DateTime and DateTimeOffset in the round-trip "O" form, keeping fractions of a second, where they dropped them; text written by 1.0.0 still reads (#1181).
  • Behaviour change: DotEnv and INI treat a type that implements only IDictionary<string, TValue> as a dictionary in both directions, where Deserialize threw InvalidCastException (#1242).
  • Behaviour change: the Delimited reader reads invalid UTF-8 before a closing quote as U+FFFD, where the quoted field broke (#1182), and the source generator accepts every scalar type the reflection binders convert (#1183).
  • The three formats share their member discovery, binding, scalar conversion, line cursor and output code (Bodu.Text.Serialization/flat/), and read and write with fewer allocations (#1187). No public API changes.
  • Behaviour change: DotEnv and INI reject a dictionary whose keys are not strings (Hashtable, SortedList, Dictionary<int, string>) in both directions, where it was written as entries and read back as an empty instance (#1249).
  • Behaviour change: Delimited and DotEnv reject a type with a member that is not a scalar, whatever the member's value and the ignore condition, on write and, before reading any input, on read, where they wrote the value's ToString text, which does not read back, and read the type when the input had no field for the member; a member declared as object that holds a value that is not a scalar is rejected when written (#1251, #1277).
  • Behaviour change: Delimited reads the Key and Value columns it writes for a dictionary back into KeyValuePair<TKey, TValue> records, by header or under NoHeader, where every record came back with its defaults (#1278).
  • Behaviour change: INI checks a root type's shape once, in both directions, before it reads or writes anything: a type with a member INI cannot represent (object, a collection other than a string-keyed dictionary, or a section member that is not a scalar), whatever the member's accessors and whether or not a section is present or null, throws IniSerializationException, where writing and reading disagreed and some documents written could not be read back (#1289, #1290). For such a type, malformed text now reports that exception rather than IniFormatException.
  • Behaviour change: INI reads a nullable struct section, and a nullable struct root, into the structure, so the value reads back as written; an absent section leaves the member null (#1292).
  • Behaviour change: DotEnv reads a nullable struct root, and Delimited reads, writes and streams nullable struct records, KeyValuePair<,> among them, with the structure's keys or columns; a null record is left out on write (#1297).
  • Behaviour change: under WhenWritingDefault, INI and DotEnv leave a member out only when it holds its declared type's default, as System.Text.Json does: a nullable member holding 0, false or a default struct is now written and reads back as that value, and only a null one is left out (#1298).
  • Behaviour change: INI rejects, with IniSerializationException, a dictionary entry whose value is neither null nor a scalar, at the root or in a section, where it wrote the value's ToString text (#1267), and a root member declared as object, on write and on read, where it wrote the value as a section and read it back as a bare object (#1268). Under IgnoreCondition.Never a null dictionary entry is decided by the dictionary's value type: a string-keyed dictionary type writes an empty section at the root and throws in a section, a scalar or object writes an empty key, and any other type throws, where every such entry was written as an empty key (#1272, #1273).

Bodu.Extensions.Configuration.Text

  • Behaviour change: AddTomlFile and AddBencodeFile resolve a relative path through the builder's file provider, honouring SetBasePath and SetFileProvider (by default AppContext.BaseDirectory), as AddJsonFile does, where they read it relative to the current directory (#1116).
  • Behaviour change: AddTextConfigurationFile reads a file at an absolute path, where it looked for it under the base directory and failed, or built an empty provider for an optional file (#1148).

Bodu.Text.Configuration

  • ConfigurationParseException's documentation says what it carries when the library throws it, and its default message comes from the package's resources (#1149, #1193).

Faster

Bodu.Globalization.Recurrence: queries no longer replay the series from its start. RecurrenceRule's GetNextOccurrence, GetPreviousOccurrence and windowed GetOccurrences, and RecurrenceSet's, begin at the frequency period that holds the instant they are given, so their cost no longer grows with the age of the series. On a 4-vCPU Intel Xeon under .NET 8 and .NET 10, a daily rule anchored in 1990 answers a 2026 GetNextOccurrence or GetPreviousOccurrence in about half a microsecond rather than about a millisecond, a monthly last-weekday rule anchored in 1975 in 3 to 5 microseconds rather than about 0.9 milliseconds, and a set with a daily rule since 1990 and a run of exception dates in 2 to 4 microseconds rather than 1.1 to 1.4 milliseconds. The answers are unchanged. A rule with COUNT is still enumerated from its start, since the occurrences before a period decide how many it may still produce.

Bodu.Security.Cryptography's BLAKE2 and BLAKE3 kernels shorten each round's critical path, and Argon2's takes AVX-512's rotation. The output is unchanged, bit for bit.

  • BLAKE2b and BLAKE2s (#743): the kernels fold their message loads, add each message word before row b, and rotate rows a, c and d rather than b between half-rounds. On an AMD EPYC 9V74 (Zen 4) with AVX-512 under .NET 8, timed side by side in one process, BLAKE2b-512 hashes 1 MiB at 1,064 MiB/s against 1.2.0's 763 (1.39 times as fast), and BLAKE2s-256 at 655 against 512 (1.28 times). There 1.2.0 had run BLAKE2s at 0.91 of 1.0.0's speed under both runtimes, and BLAKE2b at 0.94 under .NET 8; 1.3.0 runs BLAKE2b 1.30 times as fast as 1.0.0 under .NET 8 and 1.61 times under .NET 10, and BLAKE2s 1.17 times.
  • BLAKE2b with AVX-512 (#755): its 256-bit kernel swaps each word's halves with VPSHUFD for the rotation by 32 bits, 6% faster on Zen 4, and as fast or slightly faster on the Zen 5 and Intel Xeons measured.
  • BLAKE3 (#756): its single-block kernel, which compresses a message's last chunk and short messages, takes the same reordering. The four-input ARM64 kernel shares the reordered round and hashes 1 MiB and 16 MiB 1.06 times as fast on an Arm Neoverse N2.
  • Argon2 with AVX-512 (#760): on processors with AVX-512VL, Argon2's 256-bit kernel rotates by 63 bits with a single VPRORQ instead of an addition, a shift and an XOR. An Argon2id derivation with 19 MiB, two passes and one lane ran 1.03 to 1.07 times as fast on an Intel Xeon 6973P-C and on AMD's Zen 4 and Zen 5, under .NET 8 and .NET 10. Processors without AVX-512 run the AVX2 kernel as before.

Other changes

  • Documentation. The package READMEs, which nuget.org shows, and the documentation site name both target frameworks, net8.0 and net10.0, where they said net8.0, and the site gives the Microsoft.Extensions dependencies' release line for each (#768, #771). The recurrence samples describe BYHOUR, BYMINUTE and BYSECOND as expanding within the day, as they do, rather than as unsupported (#767). The symmetric stream ciphers' Encrypt and Decrypt overloads document what happens to the output when they fail (#753). The WeekDayNum remarks say that a rule that is neither monthly nor yearly keeps a BYDAY ordinal and ignores it (#782), and the Bodu namespace overview no longer calls WorkingDaysOfWeek a flags enum (#786). Broken links in the XML documentation are fixed across the packages, and package titles and descriptions use plain hyphens. The Bodu.Core README no longer carries an editing note or a stale claim about the collection contract tests (#787), and twelve package READMEs and three namespace overviews no longer introduce their package as a .NET 8 library (#791). Thirteen package descriptions name .NET 8 and .NET 10 where they named .NET 8 (#803); Bodu.Globalization.Recurrence's description and guides cover the sub-daily frequencies and the Quartz cron tokens (#804); nine XML comments and four API overviews that the code contradicted are corrected (#805, #807, #810), and the example on ToWorkingWeek no longer calls a NotableDateService constructor that does not exist (#808); and the Bodu.Formats.Excel.Binary README says that it reads BIFF5 as well as BIFF8, through Bodu.IO.Biff (#806). Its README, API overview and package description now say that BIFF5 is read from the format's documented record layouts and tested against synthesized workbooks, where they said "as written by Excel 5.0/95", which no fixture shows (#1029). The Bodu.Security.Cryptography README names the three Poly1305 AEADs the package ships, where it named a ChaCha20-Poly1305 type it does not, and says that X25519 and Ed25519 implement RFC 8410's PKCS#8 and SubjectPublicKeyInfo key formats, in DER and PEM, which it called unimplemented (#1024), and the Bodu.Text.Encoding README says that Base64Url is Base64 with the URL-safe variant, not a wrapper over the BCL's helpers (#1025). The documentation site gains a page on versioning, stability and support, with each package's version in the package matrix; a release-notes page that opens with upgrading to 1.3.0; cross-cutting topics on trimming and native AOT, thread safety, errors and exceptions, reading untrusted input, conformance and test vectors, and dependency injection and configuration; and guides to Bodu.IO.Biff. Each Stable README's banner states the Stable promise with its one exception, a deliberate break recorded in the release notes and in CompatibilitySuppressions.xml, as 1.3.0's WeekPattern change is, and the packed READMEs no longer carry the repository-only testing sections (#1127). The source-generator page, the rule-pack guide, an AddSqliteRateCache example, the thread-safety contracts of OutlookMessage and ExcelBinaryWorkbook, and the configuration line limits are corrected (#1327, #1328, #1329, #1330, #1331). The XML documentation of every package, and of the samples, is spelled in US English, as the repository's guidelines require, and a documentation check keeps it so (#1223).
  • Build and CI. CI compiles the sample apps and the documentation project with warnings as errors (#788, #790), and caps a test session at 20 minutes, as its settings file says, rather than 200 (#796). A Release build of the repository now treats warnings as errors, as CI does, and every .NET target compiles at warning level 8, both set once in Directory.Build.targets (#969). The Coverage workflow no longer restores a stale build of the in-repository XML documentation analyzer from its cache (#966). Every pull request now packs the solution as the release does, without the test assemblies' access to internals and with package validation, so a failure only the shipping build shows stops the pull request rather than the release (#1043). The release-note catalogues are checked out with LF line endings on every platform, so their tests pass on Windows (#1045). The analyzers now see the hand-written Bodu.Core date tests and the shipped contract-test source they skipped, and CI builds the currency catalogue generator (#1243, #1244, #1245). The XML-documentation analyzer skips exactly the files Roslyn treats as generated, and builds use the analyzer packed from source rather than a committed copy (#1262, #1263). The coverage gate fails a collected package that has no floor, reports the shared Outlook decode layer once as shared source, collects each project on one framework rather than keeping whichever of its two result files was newer, and keeps the Experimental tier in its matrix (#1303, #1313, #1314, #1323). A pull request that touches shared build configuration skips coverage collection however long its diff is, and coverage no longer tries to collect the Bodu.CodeStyle solution's tests (#1336, #1337). The build-test job's time limit no longer cancels passing runs on a slow runner (#1169), and the session-start hook for Claude Code on the web installs the SDK the Bodu.CodeStyle solution pins (#1264). CI builds and tests every project, sample and tool on Windows as well as on the two Ubuntu images, and the release's test gate, the Bodu.CodeStyle workflow and the native AOT smoke test run there too (#1339). The CachedRates and calendar caching samples dispose their SQLite caches, which failed them on Windows, and CI runs every sample rather than stopping at the first that fails (#1343, #1344). The coverage aggregate no longer fails a pull request whose scope job did not run, such as a draft (#1341).
  • Testing. CronExpression is held to the test suites of 21 other cron libraries, restated in Vixie's dialect, and to the scenarios of the fixes in their release notes: 5,723 rows, which found #798 and #799. The eleven format readers, Stable and Preview, are held the same way to 5,374 fixes listed in other projects' release notes, as 6,863 catalogue rows read by the new shared test reader Bodu.Test.Corpus (#817): 3,315 fixes and 4,572 rows for the text formats, 1,120 and 1,264 for the compound-file and .xls readers, and 939 and 1,027 for the Outlook readers; each package's governance tests pin its catalogue's counts and check that its embedded copy matches corpus/. A catalogue checked out with Windows line endings fails with a message that names that cause (#1346), each timed catalogue row in the Excel.Binary and TOML tests is a test of its own, so one slow row no longer cancels the rows after it (#1348), and a check fails any test that asks MSTest to list its rows separately with rows MSTest cannot serialize, which it would silently run as one test (#1352). The root .runsettings, which a bare dotnet test uses, no longer runs the Census tests, which rewrite the committed kat-census.txt (#1349).
  • Internal cleanups. Analyzer and style fixes across the packages, with no change in behaviour.

Promoted to Stable

These ten packages shipped on the preview stream, last at 0.7.1, and move to 1.3.0 with this release. From 1.3.0 their public API keeps the Stable promise; the changes below, breaking ones included, are against 0.7.1.

The Outlook readers and Bodu.IO.Pst

The .msg reader, and the MAPI value model and compressed-RTF decoder it shares with the .pst reader, were held to the release-note fixes of five other .msg libraries, as the Stable format libraries were (see the format libraries): 418 fixes, as 447 catalogue rows in corpus/msg/. The .pst container and mail-store readers were held to those of four .pst readers (libpff, java-libpst, libpst and XstReader): 521 fixes, as 583 rows in corpus/pst/.

  • Both readers: a string ends at its first NUL, as MS-OXCDATA 2.11.1 defines both string types, in property values, multi-valued elements, named-property names and the HTML body (#978). An uncompressed (MELA) RTF body is every byte after its header, whatever its CRC and declared sizes say, as MS-OXRTFCP 2.2.3.1 directs (#976). Code page 65000 decodes as UTF-7, and a multi-valued AppTime property decodes to its dates (#980, #982).

  • Both readers: an HTML body decodes through the code page its internet code page names, UTF-16 and UTF-32 included, while a code-page string still refuses UTF-16 and now refuses UTF-32, since 8-bit text cannot be in either. An HTML body that begins with its encoding's byte order mark has the mark dropped, and a UTF-7 signature is dropped from HTML bodies and code-page strings (#998, #999, #1002, #1006).

  • Bodu.Formats.Outlook: OutlookAttachmentMethod.ByWebReference (7) is new. Both readers report it, and OpenContentStream throws NotSupportedException for it, since a web service holds the content (#983).

  • Bodu.Formats.Outlook.Msg: a property record of 16 zero bytes is skipped as padding (#979). Strict rejects a named-property storage that lacks one of its three streams (#977), and below Strict a recipient or attachment storage with no property stream is omitted, where it failed the message (#981). A malformed compound file is reported as malformed rather than as "not an OLE2 compound file" (#984, #1000), and the documentation names the exceptions OpenRead(path) throws, the code pages the encoding resolver skips, and the wPropIdx rule (#991).

  • Bodu.IO.Pst: an OST is recognized and rejected with PstUnsupportedFormatException, where it failed as a malformed header, and IsPstFile sniffs only the !BDN magic, so it answers true for an OST (#986). Every header version of 23 or more except 36 is read as Unicode, as MS-PST 2.2.2.6 defines, so a file written by an Outlook that supports Windows Information Protection (version 37) now opens (#987). A B-tree page whose keys fall outside the range its parent entry gives it, and a heap-on-node allocation that overlaps its block's header or page map, are rejected at every validation level, where the first was read twice and the second read from the header (#988, #989).

  • Bodu.Formats.Outlook.Pst: OutlookMailAttachment.OpenContentStream throws NotSupportedException for every method but ByValue, as the .msg reader does, where None and the reference methods served a payload when there was one (#1001). The documentation says which faults the tolerant levels omit and which fail at every level, that the stream overloads read from the stream's current position, and which exceptions OpenRead, OpenContentStream and OpenMessage throw; the concepts page states the NUL rule (#990, #1030, #1031).

  • New: salvage reads of damaged .pst stores (#1047). PstFileOptions.OmitUnresolvableValues and OutlookMailStoreReaderOptions.OmitUnresolvableValues, both false by default, leave out a property value or table cell whose reference into the container does not resolve (a heap item or subnode the node does not hold, or a fixed-width value shorter than its type) instead of failing the whole folder, message, attachment or recipient list, so a damaged object still yields the rest of its values, as libpff, libpst and java-libpst do. PstPropertyContext.OmittedPropertyIds and PstTableRow.OmittedPropertyIds list what the container left out, and the new OutlookMailStoreReaderOptions.ValueOmittedCallback receives an OutlookOmittedValue (node, row, property and OutlookOmittedValueReason) for every value the mail-store reader omits, unresolvable or undecodable, so a caller can tell a salvaged object from a clean one. The default behaviour does not change.

  • Breaking change (Bodu.IO.Pst): PstNode.Id and ParentId are renamed NodeId and ParentNodeId, the names PstNodeInfo and PstNodeId use, and the PstFileException(message, innerException, error) overload, which took the same arguments as (message, error, innerException), is removed (#1311).

  • Behaviour change (Bodu.Formats.Outlook.Pst): a fault in the .pst container now reaches the caller as OutlookPstFormatException, with the PstFileException (and its PstFileError category) as InnerException, when opening a file that is not a PST, an OST, or one with an unsupported encoding, and on every later read, so one catch (OutlookFormatException) covers both the .msg and the .pst reader (#1305).

The exchange-rate providers over published APIs

The BoE, ECB, RBA, Fixer, exchangerate.host and FRED providers.

  • Bodu.Financial.ExchangeRates.Boe: BoeRateProvider serves a range that two adjacent or overlapping loads already cover, where it downloaded the feed again (#968).
  • Bodu.Financial.ExchangeRates.Rba: a workbook whose data sheet is named Data in another case, such as data, is read, where the provider threw KeyNotFoundException (#1021). A row whose date serial names no date (NaN, an infinity, or a serial outside the range of OLE Automation dates) is skipped, and a sheet with no row whose serial names a date fails as malformed, where an out-of-range serial threw ArgumentException and a sheet whose only serials were NaN or infinite loaded as an era with no rates (#1034). The provider's tests now read workbooks they generate in the layout of the RBA's statistical table F11.1, era by era, where they read eleven of the RBA's own workbooks, whose terms of redistribution the repository did not record (#1008).
  • Breaking change (Bodu.Financial.ExchangeRates.Boe, .Ecb and .Rba): each options type derives from WebRateProviderOptions (#1312). The BoE's Endpoint.BaseUrl, QueryPath, HttpTimeout and UserAgent become BaseAddress, QueryPath, HttpTimeout and UserAgent, and OnDemandWindowDays (an integer, 10) becomes DefaultLookback (ten days); the ECB's Endpoint:BaseUrl, HttpTimeout and UserAgent become BaseAddress, HttpTimeout and UserAgent; the RBA's BaseUrl and CurrentEraRefreshInterval become BaseAddress and RefreshInterval. BoeEndpointOptions and EcbEndpointOptions are removed. The configuration keys Add<Source>ExchangeRates binds follow the members, and every option keeps its default. All three gain MaxResponseContentBufferSize, the BoE gains CurrencyAliases, and the BoE and RBA gain SynchronousNetworkFetchLogLevel.
  • Behaviour change (.Ecb and .Rba): an on-demand lookup honours the inherited seven-day DefaultLookback, so a date early in an RBA era also loads the previous era, and an ECB lookup near the 90-day boundary loads the full-history file (#1312).
  • Breaking change (.Boe, .Ecb and .Rba): FileSystemBoeResponseCache, FileSystemEcbFeedCache and FileSystemRbaWorkbookCache are internal. No public API accepted them; the disk cache is still configured through EnableDiskCache and CacheDirectory (#1310).
  • Behaviour change (.ExchangeRateHost): the default TimeSeriesPath is timeframe, the range endpoint the exchangerate.host API documents, where it was timeseries (#1306).
  • Behaviour change (Bodu.Financial.ExchangeRates.Fixer, .ExchangeRateHost and .Fred): a provider that builds its own HttpClient applies the configured MaxResponseContentBufferSize, where it dropped it and kept the 64 MiB default (#1090).
  • The FRED README states the notice FRED's API terms require, and the exchangerate.host README states that multi-day queries need a paid plan (#1309).

Experimental packages (0.7.2)

The IMF, Yahoo and OFX providers read feeds their operators do not publish as APIs, so they are retiered from Preview to Experimental: a feed can change or start refusing requests without notice, and the provider then stops working with no change on the Bodu side (#1308). They move from 0.7.1 to 0.7.2 and depend on the 1.3.0 Stable packages. Each README says which feed it reads; pair one with a Stable provider.

  • Bodu.Financial.ExchangeRates.Imf: the file cache throws ArgumentNullException for a null key, where it threw NullReferenceException, and a provider that builds its own HttpClient applies the configured MaxResponseContentBufferSize, where it kept the 64 MiB default (#1090).
  • Behaviour change (Bodu.Financial.ExchangeRates.Yahoo): a daily bar is dated in the exchange's time zone (exchangeTimezoneName, then gmtoffset, then UTC), where every bar was dated in UTC, so a London summer bar stamped 23:00 UTC is dated the next day (#1307). The request window opens a day before the start date, so the start date's bar is loaded for an exchange east of UTC (#1318), and a range ending on DateOnly.MaxValue is requested rather than throwing ArgumentOutOfRangeException (#1321).
  • Behaviour change (Bodu.Financial.ExchangeRates.Imf): an on-demand lookup honours DefaultLookback (seven days), so a date early in a month also loads the previous month's report, which holds the nearest earlier rate when the new month has none yet (#1317).