Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
History note: Sections at and below
[0.5.0]were reformatted on 2026-05-17 for one-time CONVENTIONS v0.5 §CHANGELOG conformance: forbidden sub-headers folded into the six Keep a Changelog headers, non-user-facing content removed (build hygiene, internal refactors, test counts, governance churn), bullets normalized to past-tense active voice with code-formatted API leads. The nuget.org Release Notes tab and GitHub Releases for each shipped version remain unchanged. Sections from[0.6.0]onward are frozen per Rule 7.
Unreleased
0.6.3 - 2026-06-06: release notes sourced from the CHANGELOG
Tooling release. No library API or behavior change.
Changed
- The release workflow now publishes the matching
CHANGELOG.mdsection as the GitHub release body (body_path), so release notes carry the full hand-written detail instead of GitHub's auto-generated commit summary.
0.6.2 - 2026-06-05: documentation refresh
Documentation-only release. No API or behavior change.
Changed
- Refreshed the README (plain-ASCII punctuation) and rewrote the shared
CONVENTIONS.md: removed the version-history preamble so it reads as a conventions document, not a changelog.
0.6.1 - 2026-06-04
Changed
- README roadmap label corrected: the "Possible v0.6.0+ (longer horizon, no commitment)" backlog heading was renamed to "Possible v0.7.0+" now that v0.6.0 has shipped.
- README "Shipped in v0.6.0" section added: documents the v0.6.0 surfaces (
WithoutException(), the typedWithProperty<T>overloads, the typedWithScopeProperty<T>overloads) and the removal of the obsolete single-argWithExceptionMessage(string), matching the existing "Shipped in v0.5.0 / v0.4.0 / v0.3.0" entries. - README family roster completed: the package-family enumerations in the root and adapter READMEs were brought to the full seven-package roster.
0.6.0 - 2026-06-02
BREAKING
- The single-arg
WithExceptionMessage(string substring)overload (onLogFilterand on theHasLoggedAssertion/HasNotLoggedAssertion/HasLoggedSequenceAssertionchain),[Obsolete]since v0.4.0, has been removed. Append, StringComparison.Ordinalto existing call sites to keep the previous behavior, or pass a different comparison for case-insensitive / culture-aware matching. See### Removedbelow.
Added
LogFilter.WithoutException()and the matchingWithoutException()chain method onHasLoggedAssertion/HasNotLoggedAssertion/HasLoggedSequenceAssertion: matched records whoseExceptionisnull. The complement ofWithException(). Lets a test assert the deliberate absence of an exception (for example a record logged at warning or error level with no exception object attached) without the.Where(r => r.Exception is null)escape hatch.LogFilter.WithScopeProperty<T>(string key, T value)andLogFilter.WithScopeProperty<T>(string key, Func<T, bool> predicate)plus the matching chain methods: typed scope-property filters. Scope values keep their runtime type, so the value form compares typed-to-typed viaEqualityComparer<T>.Defaultand the predicate form receives the typed value; a scope value whose runtime type is notTnever matches. Removes the object-boxing boilerplate of the existing object-typed overloads when the scope value is a known type such asGuidorint.LogFilter.WithProperty<T>(string key, T value)andLogFilter.WithProperty<T>(string key, Func<T, bool> predicate)(where T : IParsable<T>) plus the matching chain methods: typed structured-state filters.FakeLogRecordstores structured-state values as their formatted strings, so the stored string is parsed back toTviaIParsable<T>.TryParseusingCultureInfo.InvariantCulturebefore comparing (value form) or applying the predicate; an absent or non-parsable value never matches. Removes the manualint.TryParse(...)boilerplate at the call site. TheIParsable<T>constraint keeps the round-trip reflection-free and AOT-safe.
Changed
TUnit/TUnit.Assertions/TUnit.Coredependency bumped1.44.0→1.48.6. Taken for family lockstep; the sibling adapters are on the same pin.
Removed
LogFilter.WithExceptionMessage(string substring)and the matching single-argWithExceptionMessage(string)chain method onHasLoggedAssertion/HasNotLoggedAssertion/HasLoggedSequenceAssertion: the implicit-Ordinaloverloads,[Obsolete]since v0.4.0, are gone. The explicit-comparison overloadWithExceptionMessage(string substring, StringComparison comparison)is unchanged. Migrate by appending, StringComparison.Ordinalto existing call sites. Removing a public member is a source-and-binary breaking change; see### BREAKINGabove.
0.5.0 - 2026-05-14
Added
LogAssertions.Render.LogSnapshotRenderer.Render(FakeLogCollector, LogSnapshotOptions?): rendered every captured record as deterministic multi-line text suitable for snapshot comparison. Each record renders as a[NN] level category "message"header followed by optional indentedstate:,scope:, andexception:detail lines; records are separated by a single blank line; an empty collector renders asstring.Empty. Lines are terminated with the literal LF byte (neverEnvironment.NewLine) so a baseline committed on one OS stays valid for test runs on every other. Output is a stable, pin-able contract, unlikeLogAssertionRenderingwhose output is explicitly not stable. The renderer emits captured text verbatim; volatile values are the snapshot layer's concern, composed viaSnapshotAssertions.Scrubbersat theMatchesSnapshot()call site.LogAssertions.Render.LogSnapshotOptions: controlled rendering viaLevelStyle(Abbreviation/Full),CategoryStyle(LeafOnly/Full),ScopeStyle(Include/Exclude),ExceptionStyle(TypeAndMessage/StackTracePlaceholder/Full).LogSnapshotOptions.Defaultexposed the all-default instance.LogAssertions.Render.LogLevelStyle,CategoryStyle,ScopeStyle,ExceptionStyle: enums backing the options above.
Changed
- Dependency refresh to bring pins back into lockstep with the rest of the assertion family (
TimeAssertionsv0.4.0 moved the baseline forward on 2026-05-13):DotNetProjectFile.Analyzers1.13.1 → 1.14.0Meziantou.Analyzer3.0.78 → 3.0.84Microsoft.Extensions.Diagnostics.Testing10.5.0 → 10.6.0Microsoft.SourceLink.GitHub10.0.203 → 10.0.300
README.md: added a "Pin the full log sequence as a snapshot" cookbook section showing the two-lineAssert.That(LogSnapshotRenderer.Render(collector)).MatchesSnapshot()composition and theLogSnapshotOptionssurface.
0.4.1 - 2026-05-12
Changed
- Dependency refresh to the family-lockstep versions:
TUnit/TUnit.Assertions/TUnit.Core1.43.11 → 1.44.0Microsoft.CodeAnalysis.BannedApiAnalyzers3.3.4 → 4.14.0Meziantou.Analyzer3.0.72 → 3.0.78SnapshotAssertions.TUnit0.2.0 → 0.3.0
- Production-code source style ratcheted via
MeziantouAnalysisMode=all-warningsforsrc/projects (path-conditional via the normalizedReplace('\\','/').Contains('/tests/')predicate). Test projects retain Meziantou defaults. Findings surfaced and fixed at source: CRTP self-cast pattern centralized via anUnsafe.As<TSelf>(this)helper property; discrete-value equality comparisons modernized to pattern-matching form (is 0,is SequenceStepKind.Simple); trailing-nullconstructor arguments given explicitAnyOrderSubSteps:named-argument form; culture-sensitive string concatenations switched tostring.Create(CultureInfo.InvariantCulture, ...). BannedSymbols.txt: collapsed bare#comment lines into adjacent text-bearing lines so the file parses cleanly under the stricterBannedApiAnalyzers4.x grammar.
0.4.0 - 2026-05-07
Added
LogFilter.WithInnerException<TInner>()andLogFilter.WithInnerExceptionMessage(string substring, StringComparison comparison): matched records whoseException.InnerExceptionis assignable toTInner(or whose inner-exception message contains a substring under the supplied comparison). Walks one level only; designed for the gRPC / RPC pattern where a transport exception wraps the underlying domain exception once.LogFilter.WithExceptionMessage(string substring, StringComparison comparison)overload: explicit-comparison variant of the legacyWithExceptionMessage(string). The single-arg overload is now[Obsolete]and removed in v0.6.0; see### Deprecatedbelow.LogFilter.WithScopeProperties(IDictionary<string, object?>): subset match across all active scopes for the record. Each key/value pair must match in some scope; different pairs may match in different scopes. The dictionary is snapshotted on construction; mutating the input afterwards does not affect the filter.DumpVerbosityenum +DumpTo(TextWriter, DumpVerbosity)overload: three levels:Compact(headlines only),Default(existing one-liner detail),Verbose(Defaultplus full exceptionToString()including stack trace). The no-arg overload is unchanged (Default).HasLoggedAssertion.WithInnerException<TInner>()andWithInnerExceptionMessage(string substring, StringComparison comparison): chain methods onHasLoggedAssertion/HasNotLoggedAssertion/HasLoggedSequenceAssertion. Compose with the existingWithException<T>()filter via the chain (noLogFilter.All(...)boilerplate needed).HasLoggedAssertion.WithExceptionMessage(string substring, StringComparison comparison): chain-method overload on the same assertion classes; paired with the same overload onLogFilter.HasLoggedAssertion.WithScopeProperties(IDictionary<string, object?>): chain method on the same assertion classes.FakeLogCollectorTUnitInspectionExtensions.DumpToTestOutput(DumpVerbosity)overload: the no-arg overload now delegates to this withDumpVerbosity.Default.HasLoggedSequence.ThenAnyOrder(params Action<HasLoggedSequenceAssertion>[]): concurrent step group. All sub-steps must match somewhere in the remaining records, but the order among them is unconstrained. Records that match no sub-step are skipped. Sub-steps are matched via backtracking, so an order-independent valid assignment is found if one exists: broad filters never starve more specific filters. Sub-step configurators must add filters only; calls toThen()or nestedThenAnyOrder()from within a configurator throwInvalidOperationException. TheThen()chain still works for strictly-ordered next steps before / after aThenAnyOrdergroup.Microsoft.CodeAnalysis.BannedApiAnalyzerswired into bothsrc/projects with a sharedBannedSymbols.txtat the repo root, codifying the no-reflection convention fromCONVENTIONS.mdas a build-time error (RS0030 +TreatWarningsAsErrors=true). Test projects do not reference the banned-symbols file so reflection-using helpers (e.g.PublicApiGenerator) stay usable in tests.
Changed
- Dependency refresh to latest stable for every direct and analyzer dependency:
TUnit/TUnit.Assertions/TUnit.Core1.43.2 → 1.43.11Microsoft.Extensions.Diagnostics.Testing10.0.0 → 10.5.0PublicApiGenerator11.4.6 → 11.5.4Microsoft.Sbom.Targets3.0.1 → 4.1.5Microsoft.SourceLink.GitHub8.0.0 → 10.0.203DotNetProjectFile.Analyzers1.12.2 → 1.13.1Meziantou.Analyzer2.0.219 → 3.0.72Microsoft.VisualStudio.Threading.Analyzers17.13.61 → 17.14.15Roslynator.Analyzers4.13.1 → 4.15.0SonarAnalyzer.CSharp10.24.0.138807 → 10.25.0.139117
CONVENTIONS.mdupgraded to v0.2: codified family-wide rules (trailingCancellationToken ct = defaulton every new async API,Task.Delay(TimeSpan, TimeProvider, ct)for polling loops, the 100/200/400/800/1000ms exponential schedule for time-based polls, the# <Package> snapshot v<N>header convention forToSnapshotString(), TFM policy, and the "Verify is not promoted" stance).README.md: added a "Pair with" section cross-referencingTimeAssertions.TUnitandSnapshotAssertions.TUnit; added the> **Scope:** Test projects only. Not intended for production code.blockquote at the top of the per-package short README.
Deprecated
LogFilter.WithExceptionMessage(string substring)and the corresponding chain method onHasLoggedAssertion: single-arg overloads now carry[Obsolete(error: false)]. They default toStringComparison.Ordinaland delegate to the new explicit-comparison overload. Will be removed in v0.6.0 (two-minor cycle). Migrate by appending, StringComparison.Ordinalto existing call sites, or pick a different comparison if you want case-insensitive / culture-aware behavior.
0.3.0 - 2026-05-02
Added
HasLoggedAssertion.GetMatch()/GetMatches(): value-returning terminators on theHasLogged()chain. Eliminated the duplicatecollector.Filter(...)call after asserting: hand the matched record(s) directly to a follow-up TUnit assertion.GetMatch()requires the chain's count expectation to constrain the match count to exactly one (typically viaOnce()orExactly(1), but any terminator with both bounds equal to one is accepted, includingBetween(1, 1)); throwsInvalidOperationExceptionupfront for any other expectation.GetMatches()accepts any terminator and returns the matched records as a defensiveIReadOnlyList<FakeLogRecord>snapshot captured at evaluation time.FakeLogCollectorTUnitInspectionExtensions.DumpToTestOutput(): TUnit-aware variant of the framework-agnosticDumpTo(TextWriter)that routes captured records toTestContext.Current.Output.StandardOutput. Eliminated theusing var sw = new StringWriter(); collector.DumpTo(sw); Console.WriteLine(sw)boilerplate during test development. ThrowsInvalidOperationExceptionwith a clear diagnostic if called outside a TUnit test context.
Changed
TUnitdependency bumped1.41.0→1.43.2(latest stable). AddsTUnit.Coreas a directPackageReferencetoLogAssertions.TUnit(needed forTestContext.CurrentinDumpToTestOutput). The publishedLogAssertions.TUnit.nupkgtherefore declaresTUnit.Core 1.43.2andTUnit.Assertions 1.43.2as direct dependencies; consumers who pin a lower TUnit version will see the standard NuGet upgrade prompt on install.README.md: added a "TUnit-native conveniences" section documentingBecause("reason")(the correct reason-annotation API;.WithMessage(...)is a separate predicate-on-failure-message feature for negative-testing patterns),Assert.Multiple()aggregating failures from our chains,[NotInParallel]guidance (FakeLogCollectoris instance-scoped: tests usingLogAssertions.TUnitdo NOT need[NotInParallel]for the collector itself),Should()interop (deferred pending the upstreamTUnit.Assertions.Shouldpackage reaching stable), and a Troubleshooting section covering shorthand resolution failures on pre-0.2.2 versions,LogCollectorBuildernot found (missingusing LogAssertions;), andIDE0005noise on per-file imports under strict-analysis projects.README.md: corrected v0.2.4 typos in the "Migrating from manual assertions" section:InScope("RequestId", id)→WithScopeProperty("RequestId", id),WithExceptionOfType<TimeoutException>()→WithException<TimeoutException>().
0.2.4 - 2026-05-02
Changed
README.md:GlobalUsings.csrecommendation extended withglobal using System;so the first.Containing(text, StringComparison.X)call in a project that does not enable<ImplicitUsings>compiles without an extra per-file import.README.md: Quick Start gained a "Lifetime / disposal" paragraph clarifying what theIDisposablereturned fromLogCollectorBuilder.Create()owns. Disposing thefactorystops new records from being captured but the records already gathered into thecollectorsnapshot remain valid: the collector can be queried after theusingblock exits. Both block-form (using (factory) { ... }) and declaration-form (using ILoggerFactory factory = ...) are explicitly endorsed.README.md: new "Migrating from manual assertions" section with a side-by-side before / after pairing a manualcollector.GetSnapshot()+ LINQ filter + multiple separate assertions against one fluent chain, calling out the two non-obvious wins on top of readability (failure message renders the full captured-records snapshot; the same chain extends to scopes, structured properties, exception types, and combinator nodes without restructuring the test).
0.2.3 - 2026-05-01
Changed
README.md: new "Namespaces" section explaining the asymmetricusingsituation: the assertion entry points (HasLogged,HasLoggedOnce,AssertAllAsync, ...) live inTUnit.Assertions.Extensionsand auto-import via TUnit's mechanism, whileLogCollectorBuilder,LogFilter,ILogRecordFilter, and theFakeLogCollectorinspection extensions live inLogAssertionsand require an explicitusing LogAssertions;. Includes a recommendedGlobalUsings.cssnippet that consolidates both for any consuming test project.README.md:AssertAllAsynccookbook example updated to use the v0.2.1 sync overload (c => c.HasLogged()...) as the primary form. The verboseasync c => await c.HasLogged()...form is mentioned as a fallback for cases that mix non-assertion async work between checks.README.md:.And/.Orsection rewritten. Confirms.Andis useful for chaining a positive + negative invariant in one expression, and explicitly says.Oris rarely useful for log assertions (considerMatchingAny(...)over filters instead). RecommendsAssertAllAsyncfor three-or-more conditions.README.md:Never()vsHasNotLogged()clarification added to the terminators section: preferHasNotLogged()when "this should not happen" is the test's primary intent; use.Never()when an existing positive filter chain ends up needing a zero-count expectation.
0.2.2 - 2026-05-01
Changed
- BREAKING:
HasLoggedShorthandExtensions(theHasLoggedOnce,HasLoggedExactly,HasLoggedAtLeast,HasLoggedAtMost,HasLoggedBetween,HasLoggedNothing,HasLoggedWarningOrAbove,HasLoggedErrorOrAboveentry points) moved from namespaceLogAssertions.TUnitto namespaceTUnit.Assertions.Extensions. The shipped assembly (LogAssertions.TUnit.dll), package ID, and method signatures are unchanged. Only the public namespace of the static class moved. Consumers that used the shorthands as the extension methods they were meant to be require no code change: the methods now resolve via TUnit's auto-importedTUnit.Assertions.Extensionsnamespace, so any file that already callsHasLogged()will also pick upHasLoggedOnce()etc. without changes. Migration for fully-qualified call sites: replaceLogAssertions.TUnit.HasLoggedShorthandExtensions.HasLoggedOnce(source)withTUnit.Assertions.Extensions.HasLoggedShorthandExtensions.HasLoggedOnce(source).
0.2.1 - 2026-05-01
Added
AssertAllExtensions.AssertAllAsyncnew overload acceptingFunc<IAssertionSource<TActual>, Assertion<FakeLogCollector>>(sync return) instead ofFunc<IAssertionSource<TActual>, Task>(awaited delegate). Drops theasync/awaitboilerplate from every entry. The compiler picks the right overload based on the lambda's return type:Assertion<T>for the new sync form,Taskfor the existing async form. Failure-aggregation semantics are identical between the two overloads. The existing overload remains for cases where the lambda needs to mix in non-assertion async work.
Changed
PackageValidationBaselineVersionpinned to0.2.0on both packages (was0.1.0forLogAssertions.TUnit; not previously set onLogAssertionsbecause 0.2.0 was its first release). ApiCompat baseline checks now compare against the previous shipped version of each package.
0.2.0 - 2026-05-01
Added
LogAssertionsnew framework-agnostic core package, first published at 0.2.0. ContainsILogRecordFilter, theLogFilterfactory, the four internal filter primitives (PredicateFilter,AndFilter,OrFilter,NotFilter),LogAssertionRendering(failure-snapshot rendering),LogCollectorBuilder.Create, and theFakeLogCollectorinspection extensions (Filter,CountMatching,DumpTo).LogAssertions.TUnitnow declares a transitive dependency onLogAssertions. Existing v0.1.0 consumers need no migration:LogAssertions.TUnitis still the package to install;LogAssertionscomes transitively.ILogRecordFilterinterface +LogFilterstatic factory: composable filter primitives. Every built-in chain method (AtLevel,Containing, etc.) creates one of these internally; users can build reusable filters and inject them via the newWithFilter(ILogRecordFilter)chain method.WithFilter,MatchingAny,MatchingAll,Not: combinator chain methods acceptingILogRecordFilter(orparams ILogRecordFilter[]). TheLogFilterfactory exposes the same asLogFilter.All,LogFilter.Any,LogFilter.Not.AtAnyLevel(params LogLevel[]): matched any level in the supplied set.Matching(Regex): regex match against the formatted message.ContainingAll(StringComparison, params string[])andContainingAny(StringComparison, params string[]).WithException()(parameterless: any non-null exception) andWithException(Func<Exception, bool>)(predicate).WithEventIdInRange(int min, int max): inclusive range match.WithLoggerName(string): alias forWithCategory.NotContaining(string, StringComparison),NotAtLevel(LogLevel),ExcludingCategory(string),ExcludingLevel(LogLevel): negation shortcuts.When(bool condition, Action<TSelf> apply): conditional sub-chain configuration for parameterised tests.HasLoggedOnce(),HasLoggedExactly(int),HasLoggedAtLeast(int),HasLoggedAtMost(int),HasLoggedBetween(int, int),HasLoggedNothing(),HasLoggedWarningOrAbove(),HasLoggedErrorOrAbove(): top-level shorthand entry points wrapping the equivalentHasLogged()...X()chain.AssertAllExtensions.AssertAllAsync(...): batch terminator that runs N independent assertions against the same collector and aggregates failures into a singleAssertionException(analogous to TUnit'sAssert.Multiple, scoped to log assertions).LogCollectorBuilder.Create(LogLevel): one-line factory returning a wired(ILoggerFactory, FakeLogCollector)tuple. Replaced the 3-4 lines of boilerplate every test would otherwise need.FakeLogCollector.Filter(params ILogRecordFilter[]),CountMatching(params ILogRecordFilter[]),DumpTo(TextWriter): inspection extensions.LogAssertionRendering: public static class exposing the failure-snapshot rendering helpers (AppendCapturedRecords,LevelAbbreviation) so users can build their own diagnostic surfaces with the same format. Format itself is not stable (see README "Stability promise").
Changed
- BREAKING:
LogAssertionBase<TSelf>annotated[EditorBrowsable(Never)]and documented as not for external derivation. The CRTP + sealed-derived-classes pattern requires the type to remainpublic(C# does not allow public classes to inherit from internal classes), so this is the strongest signal we can send while keeping the source generator working. The README "Stability promise" section spells out which surfaces are stable vs implementation detail.
Removed
- BREAKING:
LogAssertionBase<TSelf>.AddPredicate(Func<FakeLogRecord, bool>, string): removed. Replaced byprotected virtual void AddFilter(ILogRecordFilter)as part of the internal refactor. Only matters for consumers that were deriving fromLogAssertionBase(which was already documented as unsupported).
0.1.0 - 2026-05-01
Added
HasLogged(): positive assertion entry point onFakeLogCollector, defaults to "at least 1 matching record".HasNotLogged(): negative assertion entry point onFakeLogCollector, fixed at "zero matching records".HasLoggedSequence(): order-preserving sequence-matching entry point withThen()step terminator.AtLevel(LogLevel),AtLevelOrAbove(LogLevel),AtLevelOrBelow(LogLevel): level filters chained on the assertion entry points.Containing(string, StringComparison)(comparison explicit by design),WithMessage(Func<string, bool>),WithMessageTemplate(string)(matches the pre-substitution template viaMicrosoft.Extensions.Logging's{OriginalFormat}entry): message filters.WithException<TException>(),WithExceptionMessage(string): exception filters. (WithExceptionMessage(string, StringComparison)overload added in v0.4.0; legacy single-arg overload[Obsolete]until v0.6.0.)WithProperty(string key, string? value)(ordinal),WithProperty(string key, Func<string?, bool> predicate)(predicate over formatted value): structured-state filters.WithScope<TScope>()(by scope type),WithScopeProperty(string key, object? value)(object.Equalson scope-property value),WithScopeProperty(string key, Func<object?, bool> predicate): scope filters. Recognizes dictionary scopes andLoggerMessage.DefineScopescopes; anonymous-object scopes intentionally not supported (would require reflection, breaking AOT).WithCategory(string),WithEventId(int),WithEventName(string): identity filters.Where(Func<FakeLogRecord, bool>): escape-hatch filter.Once(),Exactly(int),AtLeast(int),AtMost(int),Between(int, int),Never(): six terminators onHasLogged().- Failure-message snapshot rendering with 4-character level abbreviations matching the
Microsoft.Extensions.Loggingconsole formatter (trce,dbug,info,warn,fail,crit,none), indentedprops:line listing each record's structured properties (excluding the{OriginalFormat}entry, already implied by the message line), indentedscope:line rendering each active scope's content askey=valuepairs (orToString()for opaque scopes), and indentedexception:line with type name and message. .And/.Orchaining via TUnit'sAssertion<T>base class.