Repository navigation
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:
WeekPatternis nowDayOfWeekSet. Bodu.Core replacesWeekPatternwithDayOfWeekSet, and every member of Bodu.Core, Bodu.Globalization.Calendar and Bodu.Globalization.Calendar.Builder that took or returned aWeekPatterntakes or returns aDayOfWeekSet. Code that namesWeekPattern, 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 aWeekPatternwrote, including theworkingDaysattributes of calendar documents, parses unchanged, and compiled.bcalpacks 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.0andnet10.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
.xlsreaders, 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
nullpassed toBencodeElement.GetPropertyorTryGetPropertyno longer compiles: the new UTF-8 overloads make the call ambiguous, as it is forJsonElement.GetProperty. Cast it tostring. - Trimming and native AOT.
BencodeSerializer's public methods now carry[RequiresUnreferencedCode]and[RequiresDynamicCode], andYamlSerializer'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.MsgandBodu.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
WeekPatternor 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,MinuteSetandSecondSetare immutable sets of months (1 to 12), days of the month (1 to 31), hours (0 to 23), minutes and seconds (0 to 59), andDayOfWeekSet, which replacesWeekPattern, is the same overDayOfWeek.Containsis a range check and a bit test and returnsfalseoutside the range;WithandWithoutreturn new sets;|,&,^and~combine them; andforeachyields the values in ascending order without allocating.- Text. The numeric sets write a list of values and ranges such as
1-3,12by default, every value with formatL, or binary text, one digit per value, with formatB;MonthSetalso writes a mask of the months' initials such asJFM________Dwith formatJ.ParseandTryParsetell the forms apart, andParseExactandTryParseExactread the one you name. - Spans and UTF-8.
TryFormatwrites any of the forms into characters or UTF-8 bytes without allocating,ParseandTryParsealso read spans of characters or UTF-8 bytes, andParseExactandTryParseExactread spans of characters (#813). - One interface. All six implement
ICalendarValueSet<TSelf, TValue>, so a generic method can take any of them:Empty,All,FromUInt64,ParseExactandTryParseExactare static abstract members, withCount,Contains,With,Without,ToUInt64,ToString(string), the operators, andIParsable<TSelf>,ISpanParsable<TSelf>,IUtf8SpanParsable<TSelf>,IFormattable,ISpanFormattableandIUtf8SpanFormattable. - One layout. Each set is a single 64-bit value.
ToUInt64returns its bits, bitnfor the value at offsetnof the range, andFromUInt64takes them back, throwingArgumentOutOfRangeExceptionfor a bit outside the range.
- Text. The numeric sets write a list of values and ranges such as
- Bodu.Core:
ThrowHelper.ThrowIfBitsOutsideMask<T>throwsArgumentOutOfRangeExceptionwhen a value sets a bit its mask does not, for anyIBinaryInteger<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)onDateOnlyandDateTimetells whether a date is the first to fifth, or the last, occurrence of a weekday in its month, returningfalsefor 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'sWtoken does.DateTime.TryToDateTimeOffset(TimeSpan, out DateTimeOffset)is the non-throwing counterpart ofToDateTimeOffset.CronExpressionnow 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.
RecurrenceRuleandRecurrenceSetanswerGetOccurrences,GetNextOccurrenceandGetPreviousOccurrenceforFREQ=HOURLY,MINUTELYandSECONDLY, which until now parsed and round-tripped but threwNotSupportedExceptionwhen queried.BYMINUTEandBYSECONDexpand an hourly period andBYSECONDa 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, soFREQ=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.
CronExpressionparsesL(the last day of the month),L-n(n days before it),nW(the weekday nearest day n),LWandL-nWin the day-of-month field,dL(the month's last weekday d) andd#k(its k-th) in the day-of-week field, and?for*in either, all of which it rejected withNotSupportedExceptionbefore. 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, soFRI#3is 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 asL-31or1,15W, fails withFormatExceptionlike any malformed field. - Bodu.Globalization.Recurrence: the rule builder takes the calendar value sets.
RecurrenceRuleBuildergainsBySecond(SecondSet),ByMinute(MinuteSet),ByHour(HourSet),ByDay(DayOfWeekSet),ByMonthDay(DayOfMonthSet)andByMonth(MonthSet), one overload per part, each writing the set's values in ascending order (BYDAYMonday first, as RFC 5545 lists the days). An empty set throwsArgumentExceptionnaming the part. Theintoverloads stay, for a month day counted from the end and for second 60. - Bodu.Globalization.Calendar: JSON documents take an algorithm registry.
NotableDateResourceLoader.LoadJsonandTryLoadJsongain overloads that take anINotableDateAlgorithmRegistry, 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 withBODU-CAL-ALGORITHM(#1099). - Bodu.Text.Bencode: dictionary keys by their bytes.
BencodeProperty.GetNameBytes()andNameEquals(ReadOnlySpan<byte>), andBencodeElement.GetProperty(ReadOnlySpan<byte>)andTryGetProperty(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.LowercaseKeyslowercases keys with the invariant culture. TheEditorConfigCompatibleprofile sets it, as the EditorConfig specification requires (#866). - Bodu.Text.Delimited:
CommentCharonDelimitedWriterOptionsandDelimitedSerializerOptionsnames 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>throwsArgumentOutOfRangeExceptionrather thanArgumentException(#739). The new type derives from the old, socatch (ArgumentException)still catches it; only a check for the exact type sees the change. - Bodu.Globalization.Calendar.Plugins:
AssemblyNamemay be null.PluginMissingAttributeException.AssemblyNameandPluginNotTrustedException.AssemblyNameare now annotatedstring?(#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, asComputeHash(Stream)does, so data appended before the call no longer enters the digest (#741). - Bodu.Globalization.Recurrence:
RecurrenceRuleBuilder.ByDayandWithWeekStartthrowArgumentOutOfRangeExceptionfor a weekday that is not defined, andByDayfor 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.TryParseandRecurrenceSet.TryParsereturnfalse,ParsethrowsFormatException, and the builder's signed parts throwArgumentOutOfRangeException, where each threwOverflowException(#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 theAddWorkingDaysextensions throwArgumentOutOfRangeException, as they do for its neighbour; each threwOverflowException(#784). - Bodu.Globalization.Recurrence:
WeekDayNum.ToString()returns theBYDAYtoken it stands for (MO,-1FR), where it returned the record's generated text, andRecurrenceRulewritesBYDAYthrough it (#794). - Bodu.Globalization.Recurrence:
CronExpression.GetNextOccurrenceandGetPreviousOccurrencereturnnullnear either end of the calendar, where a search that would step past 9999-12-31 or before 0001-01-01 threwArgumentOutOfRangeException, and theDateTimeOffsetoverloads returnnullfor an occurrence whose UTC instant falls outsideDateTime'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 thannull:0 0 * 2 MON#5, the 29th of February when it is a Monday, fires next on 29 February 2044 after 2016.nullnow means that no occurrence follows (or precedes) anywhere in the calendar (#799). - Bodu.Security.Cryptography:
ICryptoTransformExtensions.Transform(Stream, Stream, int)andTransformAsyncno 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 threwNotSupportedExceptionon the firstFraction<T>it serialized, because each converter factory closes its converter withMakeGenericType. The factories andAddNumericsJsonConvertersnow declare[RequiresDynamicCode], and the documentation and the NativeAOT smoke test register the closed converters, such asFractionJsonConverter<T>, instead (#814). - Bodu.Text.Bencode and Bodu.Financial.Serialization.Json:
BencodeSerializerand theMoney<TCurrency>converter factory gave a trimmed or native-AOT application no warning, and could fail at run time.BencodeSerializer's public methods andBencodeSerializerOptions.GetConverternow declare[RequiresUnreferencedCode]and[RequiresDynamicCode], and the factory,AddFinancialJsonConvertersandAddFinancialJsondeclare[RequiresDynamicCode]; the Financial JSON guide shows the native-AOT registration (#809). - Bodu.Text.Yaml:
YamlSerializergave a native-AOT application no warning, although it closes its enumeration, integer, collection, dictionary and nullable converters withMakeGenericTypeat run time. Its public methods andYamlSerializerOptions.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:
Utf8BencodeReaderrejects empty input, asBencodeDocumentandBencodeNodealready did, soBencodeSerializerreports empty input asBencodeFormatExceptionwhere it threwBencodeSerializationException(#822). - Behaviour change: serializing a null root value throws
BencodeSerializationExceptionfrom every overload exceptSerializeToNode, which returnsnull. Astringroot threwArgumentNullException, aNullable<T>rootInvalidOperationException, 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.ParsethrowsBencodeFormatException, and the serializer's string-keyed and char-keyed dictionaries and extension data throwBencodeSerializationException. Such keys were merged silently. The reader andBencodeDocumentread any key exactly, and the new byte members reach it (#818, #819). - Behaviour change: a case-insensitive
BencodeNode.Parsethat meets two keys differing only in case throws, where the second replaced the first (#861). Utf8BencodeReader.ValueSpanholds 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.MaxValuebound to a signed or narrower member,Int128included, throwsBencodeSerializationException, where it threwBencodeFormatException, which is reserved for bytes that are not Bencode. Every integer that does not fit its member reports itsBytesOffset, and the converter guide listsnuintwithulong, on the unsigned surface it reads and writes through (#1005, #1011, #1013). - A failed add, insert or construction of a
BencodeObjectorBencodeArrayleaves 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.Populatefills a member of a class or struct type through the instance it already holds, asSystem.Text.Jsondoes, 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'sNotSupportedExceptionthrown (#827, #828, #829). - Behaviour change: a delegate,
MemberInfo(such asType) or pointer member fails withNotSupportedExceptionnaming 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
newmaps 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
Pathnames the member:Nameat the root,Inner.Namebelow 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,300bound as 44 and-1as 255. A value of an enum based onulongabovelong.MaxValueis written as each format writes such aulongmember, where every format threwOverflowException: TOML, whose integers are signed 64-bit, now throwsTomlSerializationException, and YAML's number enum converter reads the value back (#996, #1003, #1004). - Behaviour change: a string enum converter created with
allowIntegerValues: falsereads and writes names only, as System.Text.Json'sJsonStringEnumConverterdoes 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-tagged2and 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:
YamlStringEnumConverterwrites member names whateverWriteEnumsAsStringssays, 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
JsonStringEnumConverterdoes 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 underSnakeCaseLowerCanRead | CanExecuteis 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 bound2.0as 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 aYamlSerializationExceptionthat names the key, where the message named only the type.
Bodu.Text.Toml
- Behaviour change: a finite float outside the range of a
floatorHalfmember is aTomlSerializationException, where it became an infinity.Utf8TomlReader.TryGetSingleandTomlValue.TryGetValue<float>returnfalsefor such a value, andGetSinglethrowsFormatException.inf,-infandnanread as before (#831, #910). - Behaviour change: a case-insensitive
TomlNode.Parsethat 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
TomlObjectorTomlArrayleaves 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
GetDoubleand 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, so0x10and16are 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!!floator not (#883, #895, #901, #970, #974, #994). - Behaviour change:
MaxDepthcounts sequences and mappings, as its documentation says, where it counted every node, and exceeding it while deserializing is aYamlSerializationException(#890, #899). - Behaviour change: a finite value outside a
floatmember'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:30and<<, 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, andSkipRecordskips 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
ArgumentExceptionotherwise, 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
nullto a nullable value-type member, as an empty field does, exceptchar?, which reads the space (#853). - Behaviour change:
DateTimeandDateTimeOffsetmembers are written in the round-tripOformat, andDateOnly,TimeOnlyandTimeSpanin 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, so1, 2, "test"readstest(#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 underTakeLast(#911). DeserializeAsyncEnumerableAsyncyields 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
SerializeandSerializeAsyncoverload (#855), and a member hidden withnewmaps 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 astringroot, raiseDelimitedSerializationExceptionfrom every reflection overload before anything is read or written. They were written as blank rows or as the type's own properties, such as aLengthcolumn forList<string>, and read as default values. A record type with no public parameterless constructor raisesDelimitedSerializationExceptionon read, where the runtime'sMissingMethodExceptionescaped (#1050). - A conversion error from a record or section factory reaches the caller as
DelimitedSerializationExceptionorIniSerializationException, 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, soKEY= # notereads as an empty value;KEY=#noteandURL=http://host/#topkeep their#(#833). - Behaviour change: only whitespace and a
#comment may follow a closing quote; other text throwsDotEnvFormatException, 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,\vand\$, and a backslash before a line break continues the value; any other escape keeps its backslash (#836). - Behaviour change:
DotEnvDocument'sGetPropertyandTryGetPropertyreturn 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.
LineNumberis 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,UriorBigInteger, or the nullable form of one, raisesDotEnvSerializationExceptionon write and on read, where its public properties were written as keys and reading it returned its default value (#1053). - The reader options
DisallowExportPrefix,DisallowInlineCommentsandSkipComments, and INI'sSkipComments, are documented by whattruedoes: their summaries, andDisallowExportPrefix'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]namesfoo]bar. Any other text after a header throwsIniFormatException, where it was dropped (#845). - Behaviour change:
Utf8IniWriter, andIniObject.WriteTothrough it, throwArgumentExceptionfor 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.IniSerializerreports the same asIniSerializationException, naming the section and key. A comment with line breaks is written as one comment line per line (#841, #842, #843). LineNumberis 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,HalforBigIntegerroot, or the nullable form of one, raisesIniSerializationExceptionon 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:
EditorConfigCompatiblelowercases keys with the invariant culture, and keeps dotted keys whole through theIdentitymapping its documentation named.Identitykeeps a key exactly as written, where it rejoined it on the first separator, soa:bbecamea.b(#866, #869, #916). - Behaviour change:
ConfigurationDocument.Load(path)resolves globs against the full path of the file's directory when noPathRootis set, as the documentation said it did. Each entry resolved from a document loaded by path reports that path inSourceLocation.Path, which was alwaysnull(#915, #922). - The preamble's
rootpair is never resolved as a property, and each segment of a key is trimmed, soa . bisa:b(#870, #921). Parse,TryParse,ParseWithDiagnosticsandLoad(TextReader)ignore a leading U+FEFF, asLoaddid for a stream (#865).- The documentation no longer says that the
Normalizedwrite preset sorts, marksInvalidEscapeas reserved, corrects four passages that the code contradicted, and documents the registration methodsBodu.Extensions.Configuration.Texthas (#871, #872, #873, #874).
Bodu.IO.Compound
- Behaviour change:
CompoundValidationLevel.Strictrejects 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, andCompatibleprunes 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.
Minimalzero-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.Openreads a container from the stream's current position, not from offset 0, under every read strategy (#958), and three failures report theCompoundFileErrorcategory their documentation names (#949).- Behaviour change: a writable storage throws the documented
ArgumentExceptionfor a name already taken andKeyNotFoundExceptionfor a missing one, where it threwCompoundFileSerializationException. TheCreateStreamoverload 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 undefinedCompoundFileVersionis rejected at every entry point, where it was written as version 3 (#953, #954, #955). CommitandCommitAsyncplan 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:
OlePropertySetreads 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). OlePropertySetreads property identifiers unsigned, so PID_LOCALE and PID_BEHAVIOR read, where they threwOverflowExceptionand keptExcelBinaryWorkbookfrom opening a workbook whose document summary carries PID_LOCALE. An oversized length is reported as malformed, where it too threwOverflowException(#961).OlePropertySetreads vector strings whether or not they are padded, as [MS-OSHARED]'s document parts and heading pairs are not, and aVT_LPWSTRwhose writer aligned the string rather than the property. A property whose type word carriesVT_ARRAY,VT_BYREFor 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
EOFrecord that matches its ownBOF, 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
EOFrecord 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'sBOF. A worksheet's used range comes from its ownDIMENSIONSrecord, 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:
BiffSstReaderends the table where its records end. It also passes over aCONTINUErecord that holds only a continued string's flags byte, and decodes a surrogate pair split across aCONTINUEboundary to its character, where each half became U+FFFD (#935, #937, #1012). - A formula's text result is read across its
CONTINUErecords, where it failed, and after theARRAY,TABLEorSHRFMLArecord of an array, table or shared formula, where it read as an empty string (#929, #930). - Behaviour change:
IsDateFormattedistruefor the Thai built-in formats 71 to 81, and aFORMATrecord 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 asTRUE. 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
BOFrecord declares an unsupported version raisesExcelBinaryUnsupportedException, where the codec'sBiffUnsupportedVersionExceptionescaped. The documentation says when a malformed record fails, at open for the workbook globals and when the sheet is read for a sheet, and listsRSTRINGamong 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 withSystem.Globalization.ISOWeekin 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)throwsArgumentExceptionfor a multidimensional array, where it threwRankException(#1063);BufferConverter.ToArraythrowsArgumentOutOfRangeExceptionfor a negative count, whereOverflowExceptionescaped (#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 threwIndexOutOfRangeException(#1068); andThrowIfSpanLengthNotPositiveMultipleOf(Span)validates its divisor, as itsReadOnlySpanoverload does, where 0 threwDivideByZeroException(#1074). - Quarter bounds from
FiscalWeekQuarterProviderkeep the receiver'sDateTimeKind(#1065); the defaultGetFiscalYearaccepts 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)returnsstartwhenafterequals it, as theDateTimeoverload does (#1069). - Behaviour change:
ToSafePathSegmentreplaces both/and\on every platform, where\survived on Linux and macOS (#1070). - Behaviour change:
ReverseDigitsand theRotateDigitsmethods throwOverflowExceptionwhen the result does not fit the type, where they returned a wrapped value (ushort60009 reversed gave 24470) (#1071). - Behaviour change:
XorShiftRandom.Next(0)returns 0, andNext(minValue, maxValue)withminValue > maxValuethrowsArgumentOutOfRangeException, asSystem.Randomdoes; it threw, and threwArgumentException, respectively (#1072). - Behaviour change:
NearestDateOfWeekandGetNearestDateOfWeekonDateTimeandDateOnlyreturn the occurrence in the other direction when the nearer one lies outside the range of the type, where they threwArgumentOutOfRangeExceptionwithin six days of either end (#1151). - Behaviour change:
WithExceptionFallbacksandWithReplacementFallbacksreturn 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 anEncodingsubclass that is not registered threwNotSupportedException(#1073). - Behaviour change: when the last writer queued on an
AsyncReaderWriterLockcancels, 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,PreviousDateOfWeekandPreviousOrSameDateOfWeekonDateTimeandDateOnlythrowArgumentOutOfRangeExceptionnamingdateTimeordatewhen the occurrence lies outside the type's range, where the exception namedticksorvalue(#1195). - The async primitives allocate less per contended wait (
AsyncSemaphoreandAsyncAutoResetEvent144 bytes where 528,AsyncLock144 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, andTryToDateTimeOffsetchecks its rules rather than catching an exception, so a failed conversion allocates nothing (#1186). - Behaviour change:
NthDateOfWeekInMonthandGetNthDateOfWeekInMonthonDateTimeandDateOnly, asked for a fifth Saturday, Sunday, Monday or Tuesday of December 9999, throw the documentedArgumentOutOfRangeExceptionnamingordinal, as in every other month, where the runtime's exception nameddayNumberorticks(#1237).
Bodu.Collections
- Behaviour change: the
DequeandCircularBufferconstructors that take a collection validate the capacity before anything else, andEvictingDictionaryrejects an undefinedEvictingDictionaryPolicywhen it is constructed, each throwing the documentedArgumentOutOfRangeException. Before, a capacity of -1 or 0 could succeed, or throwInvalidOperationException, and an undefined policy failed at the first eviction (#1075, #1076, #1077). - The
Dequecollection constructors document that the deque always grows and that its capacity becomes the source's length when that is larger (#1150). - The
CircularBuffer,Dequeand ring-backed collection constructors document their largest capacity (#1194).
Bodu.Security.Cryptography
- Behaviour change:
Shake.HashSizethrowsArgumentOutOfRangeExceptionfor a size that is not a positive multiple of 8, as its documentation and the constructor do, where it threwCryptographicException(#1059). - The padding modes' block-size
CryptographicExceptionstates 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.RoundTripOnlyaccepts only the round-trip shape its documentation describes (the uppercase ISO code, one space, an invariant number), read with the invariant culture whateverFormatProvidersays, and keeps a scale finer than the currency's minor units (USD 145.678912keeps six places), where it accepted padding, signs, symbols and the code last, and rounded to the minor units (#1081). - Behaviour change: converting a default
Moneywith a decimal rate throwsInvalidOperationException, asIsDefaultdocuments (#1082). - Behaviour change:
ToMoney(), the implicit conversion toMoneyandToCalculated()throwInvalidOperationExceptionfor aMoney<TCurrency>whose tag names no currency in the ISO 4217 catalogue, where they produced aMoneywith no currency;MoneyBag.Add<T>andSubtract<T>throw it too (#1094). - Behaviour change: reading a
MoneyBagfrom JSON honours the policy: Strict rejects a repeated ISO code and a repeatedbalancesproperty, and Lenient rejects a repeated code and accepts lowercase or padded codes (#1083). - Behaviour change: a
MoneyBagread under the Compact policy rejects a repeated ISO code as Strict and Lenient do, where it added the balances together (#1142). - Behaviour change:
MonetaryContext.ResolveScaleandRoundunderScalePolicy.CustomthrowArgumentOutOfRangeExceptionnamingCustomScalefor a scale outside 0 to 28, where a negative scale returned the value unrounded and one above 28 named adecimalsparameter (#1143). - Behaviour change:
MoneyFormatterBuilder.WithMinorUnits, and theMoneyFormatterconstructor for aMinorUnitsOverride, throwArgumentOutOfRangeExceptionoutside 0 to 28, where every laterFormatcall threwFormatException(#1144). - Behaviour change:
MonetaryContext.ResolveScaleandRoundthrowArgumentOutOfRangeExceptionnamingcurrencyMinorUnitsfor 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 withwith) throwsArgumentOutOfRangeExceptionforMinorUnitsoutside 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
CurrencyInfowith a negative cash-rounding increment, or one finer than its minor units, throwsArgumentOutOfRangeException, 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 threwOverflowException(#1172).
Bodu.Financial.ExchangeRates and its caching packages
- Behaviour change (Bodu.Financial.ExchangeRates.DependencyInjection): the named
HttpClientthat eachAdd<Source>ExchangeRatesregisters is capped atWebRateProviderOptions.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
CachingRateProviderandAggregatingRateProviderreturn the most recent rate on or before today, asIDatedRateProviderdocuments and the web providers do, where they required a rate dated today and so failed at weekends;CachingRateOptions.DefaultLookupOptionsandRateAggregationOptions.DefaultLookupOptionsdefault toPreviousWithin(100000)(#1091). - Behaviour change: in range reads,
AggregatingRateProviderpasses over a child that cannot carry the pair, where itsRateSeriesNotFoundExceptionescaped, and the average leaves out an empty candidate, where one empty child emptied the result (#1087, #1088). - Behaviour change: an inverted
ExchangeRateof zero throwsArgumentOutOfRangeException, where it threwDivideByZeroException(#1089). - Behaviour change:
AddDistributedRateCacheregisters 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 wasInformation(#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
InvalidOperationExceptionescaped (#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'sPreloadAsyncfinishes 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 protectedWebRateProvider.LoadCoalescedAsync, and the stored-atTryGetoverloads ofIByteCache<TKey>andFileSystemByteCache<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,DocumentSummaryInformationandExcelWorkbookPropertiesread 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'sWrite,SetLengthandFlushthrow too (itsCanWriteisfalse), where a writable file's storages kept staging edits that could never be committed. On a read-only file theTryOpenmembers throw rather than returnfalse(#1146). OpenStreamwithFileMode.CreateNewover an existing stream names the stream in itsIOException, where the message called the mode unsupported (#1192).
Bodu.Globalization.Calendar
- Behaviour change (Bodu.Globalization.Calendar.Caching):
AddCachedNotableDateServicedecorates the most recent unkeyedINotableDateServiceregistration 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 throwsInvalidOperationException(#1325). - Behaviour change: an adjustment policy's
nonWorkingflag 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.bcalformat 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):LoadthrowsNotableDateValidationExceptionandTryLoadreturnsfalse, whereArgumentOutOfRangeExceptionorFormatExceptionescaped 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,SpanCollisionPolicyandObservedDateRangePolicyare read and kept but not yet applied; the documentation now says so (#1119 tracks applying them).- Behaviour change:
CachingNotableDateServicereturns 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()declaresencoding="utf-8", so the fileSavewrites loads withXDocument.LoadandXmlReader, where its declaration saidutf-16over UTF-8 bytes (#1135). - Behaviour change: a
PatchRulethat names both a strategy and a recurrence, or an occurrence source with no kind, is aBODU-CAL-RULE-SOURCEerror in XML and JSON, and anuntilDateend strategy with no kind, or with two, isBODU-CAL-DURATION-NOENDorBODU-CAL-DURATION-CONFLICT, where these documents loaded (#1136). - Behaviour change: a malformed
workingDaysvalue is the newBODU-CAL-WORKING-DAYSdiagnostic:LoadthrowsNotableDateValidationException, whereFormatExceptionescaped, andTryLoadreports that code (#1137). - The caching registrations' option-validation messages and the binary pack writer's
NotSupportedExceptionmessage come from resources and read as sentences (#1138);NotableDateCachingOptions.ValidatenamesCacheMissLogLevelwhen that is the undefined level (#1139). - Behaviour change: under
HighestPriorityOnly,CategoryPriorityandCustom, 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 customINotableDateCollisionResolverreceives the whole group for its date, andINotableDateProvider.GetNotableDatesmay be asked for a range that starts up to one span before the window.KeepAllis unchanged (#1165). - Behaviour change: an unrecognized or numeric JSON enumeration value is the new
BODU-CAL-ENUMdiagnostic, where it loaded the default; member names still match ignoring case (#1166). - Behaviour change:
workingDaysaccepts 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,
anchorDateor trigger month that does not parse fails the load withBODU-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-ENUMin a list of names andBODU-CAL-VALUEin a list of territories, references, adjustments or tags and in a scope'sruleRefs, where it was dropped (#1203). - Behaviour change: a
fixedorweekdayNearDateday outside 1-31 fails withBODU-CAL-STRATEGYin both formats, where it was clamped to 1 or 31 (#1204). - Behaviour change (Builder):
NotableDateDocumentBuilderreads enumeration values,workingDays, integers, Booleans and the trigger month with the loader's rules, and throwsFormatExceptioncarrying the loader's message for a value the loader rejects, where it read a default, dropped the value or read any Boolean other thantrueor1asfalse; values the loader accepts, such as JSON"5"or"false", are accepted (#1189, #1202). - Behaviour change (Builder):
FromJsonreads every fieldLoadJsonreads (imports, trigger and action details, handler parameters, scope rule references and year bounds, a PatchRule's applicability, strategy, recurrence, duration, tags and adjustments), and throwsNotSupportedExceptionfor the four fields its model cannot hold;ToXmlandBuildemit a PatchRule's recurrence and duration end, andToJsonthrowsNotSupportedExceptionfor them (#1205). - Behaviour change: the duplicate, span collision and observed-date range policies that
ResolutionPolicystored now take effect.DuplicatePolicy.KeepFirst,KeepLastandMergesettle a repeated notable date in the loaded document (a collidingAddRulestaysBODU-CAL-DUP-RULE); a span policy other thanKeepAllsettles chains of overlapping multi-day occurrences, widening the years it gathers by up to ten while a chain stays open; andActualOccurrenceControlsInclusionandBothOccurrencesControlInclusioninclude 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.bcalrange-policy version 3.CachingNotableDateServiceclips 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):
FromJsonreads text fields and list items asLoadJsonreads 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 aPatchRuleoccurrence source that names no kind or two, asLoadJsondoes (#1216, #1218). - Behaviour change: the nth-weekday strategies (
DayOfWeekInMonthStrategy,RelativeWeekdayInMonthStrategy,MonthlyWeekdayRecurrenceStrategy) yield no date for an undefinedWeekOrdinal, or for a fifth weekday that December 9999 lacks, where they returned another year's date or threwArgumentOutOfRangeException(#1215). - Behaviour change: an override's operation is matched ignoring case; an unknown or absent operation is
BODU-CAL-ENUM, and anAddRulewithout its rule isBODU-CAL-SCHEMA, where JSON read either as aPatchRule(#1226). - Behaviour change:
LoadJsonholds identifiers, references, the resource identifier and territory codes to the XSD's patterns (BODU-CAL-SCHEMA), and holds required values, non-emptyrulesand weeklydays, 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, areBODU-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):
FromJsonmatches property names, checks each rule's anduntilDate's occurrence source, parses strategy and recurrence values, and applies the required values and ranges asLoadJsondoes, throwingFormatExceptionwith the loader's message, where it accepted documentsLoadJsonrejects, 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 formLoadJsonreads, no longer requiresnotableDates, and requires an override'sruleorruleRefaccording 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;FromJsonchecks 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
ruleRefis reported once in JSON, as in XML (#1257, #1261). - The JSON schema describes every member and value
LoadJsonreads (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
Fixedrule'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 namingNisan, or month 13 in a twelve-month calendar, is nowBODU-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
anchorDatesuch as2024-1-1(#1271). - Behaviour change:
FixedDateStrategy's constructor applies the loader's month rules: it throwsArgumentOutOfRangeExceptionfor an undefined calendar or a month past the calendar's count, andArgumentExceptionfor a Hebrew month alias outside the Hebrew calendar or outside the eight aliases;NotableDateBinaryResource.Readreports a pack holding such a strategy asNotableDateBinaryFormatException(#1275). Hebrew month names match ignoring case inLoad,LoadJsonand the builder'sFromJson, which writes the canonical spelling (#1276). - Behaviour change: the strategy constructors apply the loader's rules:
DayOfWeekInMonthStrategy,WeekdayNearDateStrategyandRelativeWeekdayInMonthStrategyreject undefined enum values, the four rule-reference strategies reject references that are not identifiers,NthWeekdayFromRuleStrategyandWorkingDayInMonthStrategyreject ordinals outside -366 to 366, andFixedDateStrategyrejects a Gregorian day its month never has; a pack holding such a strategy fails to read (#1280-#1283).NotableDateBinaryResource.Writeruns the loader's identifier, duplicate and cross-reference checks and throwsNotableDateValidationExceptionbefore writing a resource the loader would reject; the newWrite(resource, stream, algorithms)overload accepts a custom registry's algorithm keys (#1284).Writealso refuses a number outside the range the loader accepts, such as afromYearabove 65535 or a trigger day of 32, with the loader'sBODU-CAL-SCHEMAdiagnostic (#1294), and an enumeration value its type does not define, which it wrote as its low byte, withBODU-CAL-ENUM(#1295).
Bodu.Globalization.Recurrence
- Behaviour change:
RecurrenceRule.Parse,RecurrenceSet.ParseandCronExpression.ParsethrowFormatExceptioncarrying the defect thatTryParsenames, 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
DateTimeOffsetoverloads ofRecurrenceRule,RecurrenceSetandAnchoredIntervalreturnnull, or end the sequence, at the last occurrence whose UTC instant aDateTimeOffsetcan represent, where they threwArgumentOutOfRangeException, asCronExpressionalready does (#1185).
Bodu.Numerics and Bodu.Numerics.Serialization.Json
- Behaviour change:
Fraction<T>.ToDecimal, the explicit conversion todecimal,TryToDecimalanddecimal.CreateCheckedsucceed whenever the value fitsdecimal, rounding half to even, where they overflowed (or returnedfalse) when a component was wider than 96 bits (#1103). - Behaviour change:
Interval<T>.AtLeast,GreaterThan,AtMostandLessThanthrowArgumentExceptionfor 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)andAtLeast(0).Contains(double.PositiveInfinity)arefalse, where they weretrue. The interval operations agree with that membership, soAllno longer contains or overlaps[0, Infinity],AtLeast(0)intersected with[0, Infinity]is[0, +∞), andComplementno 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)andLessThan(-Infinity)returnInterval<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
JsonExceptionfor a NaN endpoint on a bounded side, whereArgumentExceptionescaped (#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,TryGetDecodedLengthandGetDecodedLengthaccept exactly what itsDecodeaccepts, where they disagreed for some padding, canonical-form and overflow cases (#1107).GetDecodedLengththrowsFormatExceptionwith the decoder's own message. - Behaviour change: the Base64 MIME
EncodeToUtf8andTryEncodeToUtf8methods insert the CRLF line breaks thatEncodewrites, 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.FlickrandBinaryEncodings.Base58Flickrare new (#1118). - Behaviour change:
Base58.GetMaxDecodedLength(n)returnsn, an upper bound that holds for input made only of'1', andBase58Check.GetMaxDecodedLengthreturnsmax(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 largen(#1158). - Behaviour change: strictly padded Base64 input shorter than one quantum, such as
QQ=, throws the documentedFormatException, where it threwOverflowException(#1159).
Bodu.IO.Hashing
CrcStandard.FromNameandTryFromNameresolve 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.GetandFromNameexception 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:
YamlSerializerwrites aUrias its original string and reads absolute or relative URI text back, where it wrote a mapping of theUri's properties, threw for a relativeUri, 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, asSystem.Text.Json.Nodesdoes, where it built a cycle that hungRootand overflowed the stack when written (#1207).
Bodu.Text.Delimited, Bodu.Text.DotEnv and Bodu.Text.Ini
- Behaviour change:
NoHeaderdeserialization into a class binds its fields by position, in the orderSerializewrites them, as the generated factories do, where every record came back with no field set (#1109). - Behaviour change:
DateOnly,TimeOnly,Int128,UInt128,HalfandBigIntegervalues read back in all three formats, where they were written but could not be read. DotEnv and INI writeTimeOnlyin 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
LineNumberand the line inDelimitedFormatExceptioncount 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 rootGetArrayLengthcheck disposal and bounds, andGetPropertyfinds a ragged field by its column index (#1111); andDeserializeAsyncEnumerableAsyncchecks its arguments when it is called, not at the firstMoveNextAsync(#1117). - Behaviour change: DotEnv and INI deserialization into an abstract type, or one without a public parameterless constructor, throws
DotEnvSerializationExceptionorIniSerializationException, whereMissingMethodExceptionescaped (#1114).Utf8DotEnvWriterrejects 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
newonce, where they mapped both declarations (#1178); honor therequiredkeyword, and throwDotEnvSerializationExceptionorIniSerializationExceptionwhen two members map to one name (#1179); read backnintandnuintmembers (#1180); and writeDateTimeandDateTimeOffsetin 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, whereDeserializethrewInvalidCastException(#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
ToStringtext, which does not read back, and read the type when the input had no field for the member; a member declared asobjectthat holds a value that is not a scalar is rejected when written (#1251, #1277). - Behaviour change: Delimited reads the
KeyandValuecolumns it writes for a dictionary back intoKeyValuePair<TKey, TValue>records, by header or underNoHeader, 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, throwsIniSerializationException, 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 thanIniFormatException. - 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; anullrecord 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, asSystem.Text.Jsondoes: a nullable member holding0,falseor a default struct is now written and reads back as that value, and only anullone is left out (#1298). - Behaviour change: INI rejects, with
IniSerializationException, a dictionary entry whose value is neithernullnor a scalar, at the root or in a section, where it wrote the value'sToStringtext (#1267), and a root member declared asobject, on write and on read, where it wrote the value as a section and read it back as a bareobject(#1268). UnderIgnoreCondition.Neveranulldictionary 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 orobjectwrites 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:
AddTomlFileandAddBencodeFileresolve a relative path through the builder's file provider, honouringSetBasePathandSetFileProvider(by defaultAppContext.BaseDirectory), asAddJsonFiledoes, where they read it relative to the current directory (#1116). - Behaviour change:
AddTextConfigurationFilereads 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
VPSHUFDfor 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
VPRORQinstead 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.0andnet10.0, where they saidnet8.0, and the site gives theMicrosoft.Extensionsdependencies' release line for each (#768, #771). The recurrence samples describeBYHOUR,BYMINUTEandBYSECONDas expanding within the day, as they do, rather than as unsupported (#767). The symmetric stream ciphers'EncryptandDecryptoverloads document what happens to the output when they fail (#753). TheWeekDayNumremarks say that a rule that is neither monthly nor yearly keeps aBYDAYordinal and ignores it (#782), and theBodunamespace overview no longer callsWorkingDaysOfWeeka 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 onToWorkingWeekno longer calls aNotableDateServiceconstructor 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 thatBase64UrlisBase64with 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 inCompatibilitySuppressions.xml, as 1.3.0'sWeekPatternchange is, and the packed READMEs no longer carry the repository-only testing sections (#1127). The source-generator page, the rule-pack guide, anAddSqliteRateCacheexample, the thread-safety contracts ofOutlookMessageandExcelBinaryWorkbook, 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.
CronExpressionis 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 readerBodu.Test.Corpus(#817): 3,315 fixes and 4,572 rows for the text formats, 1,120 and 1,264 for the compound-file and.xlsreaders, 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 matchescorpus/. 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 baredotnet testuses, no longer runs the Census tests, which rewrite the committedkat-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-valuedAppTimeproperty 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, andOpenContentStreamthrowsNotSupportedExceptionfor 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).
Strictrejects a named-property storage that lacks one of its three streams (#977), and belowStricta 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 exceptionsOpenRead(path)throws, the code pages the encoding resolver skips, and thewPropIdxrule (#991). -
Bodu.IO.Pst: an OST is recognized and rejected with
PstUnsupportedFormatException, where it failed as a malformed header, andIsPstFilesniffs only the!BDNmagic, so it answerstruefor 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.OpenContentStreamthrowsNotSupportedExceptionfor every method butByValue, as the.msgreader does, whereNoneand 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 exceptionsOpenRead,OpenContentStreamandOpenMessagethrow; the concepts page states the NUL rule (#990, #1030, #1031). -
New: salvage reads of damaged
.pststores (#1047).PstFileOptions.OmitUnresolvableValuesandOutlookMailStoreReaderOptions.OmitUnresolvableValues, bothfalseby 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.OmittedPropertyIdsandPstTableRow.OmittedPropertyIdslist what the container left out, and the newOutlookMailStoreReaderOptions.ValueOmittedCallbackreceives anOutlookOmittedValue(node, row, property andOutlookOmittedValueReason) 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.IdandParentIdare renamedNodeIdandParentNodeId, the namesPstNodeInfoandPstNodeIduse, and thePstFileException(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
.pstcontainer now reaches the caller asOutlookPstFormatException, with thePstFileException(and itsPstFileErrorcategory) asInnerException, when opening a file that is not a PST, an OST, or one with an unsupported encoding, and on every later read, so onecatch (OutlookFormatException)covers both the.msgand the.pstreader (#1305).
The exchange-rate providers over published APIs
The BoE, ECB, RBA, Fixer, exchangerate.host and FRED providers.
- Bodu.Financial.ExchangeRates.Boe:
BoeRateProviderserves 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
Datain another case, such asdata, is read, where the provider threwKeyNotFoundException(#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 threwArgumentExceptionand 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'sEndpoint.BaseUrl,QueryPath,HttpTimeoutandUserAgentbecomeBaseAddress,QueryPath,HttpTimeoutandUserAgent, andOnDemandWindowDays(an integer, 10) becomesDefaultLookback(ten days); the ECB'sEndpoint:BaseUrl,HttpTimeoutandUserAgentbecomeBaseAddress,HttpTimeoutandUserAgent; the RBA'sBaseUrlandCurrentEraRefreshIntervalbecomeBaseAddressandRefreshInterval.BoeEndpointOptionsandEcbEndpointOptionsare removed. The configuration keysAdd<Source>ExchangeRatesbinds follow the members, and every option keeps its default. All three gainMaxResponseContentBufferSize, the BoE gainsCurrencyAliases, and the BoE and RBA gainSynchronousNetworkFetchLogLevel. - 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,FileSystemEcbFeedCacheandFileSystemRbaWorkbookCacheare internal. No public API accepted them; the disk cache is still configured throughEnableDiskCacheandCacheDirectory(#1310). - Behaviour change (.ExchangeRateHost): the default
TimeSeriesPathistimeframe, the range endpoint the exchangerate.host API documents, where it wastimeseries(#1306). - Behaviour change (Bodu.Financial.ExchangeRates.Fixer, .ExchangeRateHost and .Fred): a provider that builds its own
HttpClientapplies the configuredMaxResponseContentBufferSize, 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
ArgumentNullExceptionfor a null key, where it threwNullReferenceException, and a provider that builds its ownHttpClientapplies the configuredMaxResponseContentBufferSize, 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, thengmtoffset, 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 onDateOnly.MaxValueis requested rather than throwingArgumentOutOfRangeException(#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).