Repository navigation
Nilog v1.0.3
Added
-
Typed six-, seven-, and eight-argument overloads — a new build-time source generator
(Nilog.SourceGenerators) emits zero-arrayWriteTrace/WriteDebug/WriteInformation/
WriteWarning/WriteError/WriteCriticalandNilogger.Logoverloads for 6–8 arguments,
lifting the typed ceiling from 5 to 8. They are generated directly intoNilog.dll, so
consumers pick them up with no extra reference. The disabled path allocates 0 bytes for
up to 8 typed arguments — asserted as exactly0Lby
HighArityTests.DisabledPath_SixTypedArgs_AllocatesZeroBytes/…EightTypedArgs…and
confirmed by BenchmarkDotNet (8-arg disabled: 0.82 ns / 0 B vs Microsoft 221 ns / 336 B).
6+ arguments previously fell back toparams object[]; that boundary is now 9+. -
Seven new
Nilog.Analyzersrules (1 → 8 total — full parity with the SerilogAnalyzer set):- NILOG002 — the
{Placeholder}count in a constant template does not match the number
of arguments supplied. - NILOG003 — the template is built with string concatenation (
"a" + b) or
string.Format(...), which defeats the template cache and loses named properties. - NILOG004 — the same named
{Placeholder}appears more than once (silently collides on
one structured-property key); numeric/positional{0} {0}reuse is intentionally allowed. - NILOG005 (Info) — positional
{0}placeholders instead of named{Name}ones. - NILOG006 — an
Exceptionpassed as a template value instead of the exception parameter,
which loses its type/message/stack as structured data. - NILOG007 — a malformed template: an unclosed
{or an empty{}placeholder. - NILOG008 (Info) — a placeholder name that is not PascalCase (
{userId}→{UserId}).
- NILOG002 — the
-
Code fix for NILOG001 — a one-click "Convert to a literal template with arguments"
refactoring that rewriteslogger.WriteInformation($"User {id}")into
logger.WriteInformation("User {id}", id), preserving:format/,alignmentclauses and
appending the extracted expressions at the correct trailing position for every call shape. -
Nilog.Analyzersnow ships as a standalone NuGet package (analyzers/dotnet/cs),
development-dependency only, so it never adds a runtime dependency to consumers. -
Real
FlushAsyncviaNilogger.RegisterFlush(Func<CancellationToken, Task>)/
UnregisterFlush(...). Buffering/batching sinks register how to drain themselves and
FlushAsyncawaits them all (every callback is attempted; failures surface together as an
AggregateException). With nothing registered it stays a zero-allocation no-op, so existing
callers are unaffected — turning the long-standing "FlushAsync is a no-op" limitation into a
working flush. -
LoggingEngineInteropTests— end-to-end tests that run Nilog through the real
Microsoft.Extensions.LoggingLoggerFactory+ILoggerProviderpipeline (the exact
contract every third-party engine integrates through), asserting the rendered message,
{OriginalFormat}, named properties, exceptions, and level-filtering all survive intact. -
Allocation regression gate — a consolidated
AllocationGateTestssuite (1–8 typed args
and the staticLogpath, all asserting0Lon the disabled path) plus a GitHub Actions
workflow (.github/workflows/ci.yml) that builds Release and runs the tests on every push/PR.
Changed
-
TemplateFormatter.Rendernow covers up to 8 arguments, so the generated 6–8 arg
overloads render through the same stack-allocatedSpan<char>path as 1–5 args instead of
building anobject?[]inToString(). Enabled-path effect vs Microsoft: 6-arg
264 B → 192 B (~27% less, ~44% faster) and 8-arg 336 B → 248 B (~26% less, ~50%
faster).Format(params object?[])remains only as the format-specifier/overflow fallback. -
Niloggeris now apartialclass so the generated overloads compile into the same type. -
Native AOT / trimming is now compiler-enforced via
<IsAotCompatible>true</IsAotCompatible>
onNilog.Core: the trim, single-file, and AOT analyzers run on every build and, with
TreatWarningsAsErrorsin Release, fail the build on any unsafe construct. The Native AOT
compiler emits native code fromNilog.dllwith zero warnings. -
Nilog.Demo,Nilog.Function, andNilog.Benchmarkupdated to exercise 6–8 typed args,
the 9-argumentparamsescape hatch, realFlushAsync/RegisterFlush, and the four analyzer
rules. Package/product version bumped to 1.0.3.
Fixed
-
Nilogger.Logwith 5–8 args silently allocated. The typedLog<T0..Tn>overloads carried
[OverloadResolutionPriority(-1)], which let theparams object[]overload win for a normal
(no-exception) call — so the staticLogAPI allocated an array, contradicting the documented
zero-array guarantee. Fixed by promoting the exception overload
(Log(ILogger, LogLevel, string, Exception, params object[])→ priority 1) instead of demoting
the typed ones: a trailingExceptionstill binds correctly (regression-tested), while a plain
typed call now binds to the zero-array overload. Discovered by the new allocation gate. -
Trimming/AOT hazard removed. Enabling the AOT analyzers surfaced two uses of
Exception.TargetSite([RequiresUnreferencedCode]) in the exception formatter. The
Target Siteline — redundant with the stack trace — was dropped, making the library
genuinely trim/AOT-clean. (The only behavioural change is one fewer line in the exception
report text.)
Performance
Measured with BenchmarkDotNet (ShortRun: 3 warmup + 3 measurement, Server GC), .NET 10.0,
Intel Core i7-13850HX:
| Path | v1.0.2 | v1.0.3 | Δ |
|---|---|---|---|
8-arg disabled — WriteInformation("{A}…{H}", …) |
params, 221 ns / 336 B (≈ Microsoft) | 0.82 ns / 0 B | ~268× faster, zero alloc |
6-arg enabled — WriteInformation("{A}…{F}", …) |
params, 180 ns / 264 B (≈ Microsoft) | 100.6 ns / 192 B | ~44% faster, ~27% less alloc |
8-arg enabled — WriteInformation("{A}…{H}", …) |
params, 233 ns / 336 B (≈ Microsoft) | 117.0 ns / 248 B | ~50% faster, ~26% less alloc |
Static Nilogger.Log, 5–8 typed args, disabled |
params array (allocated) | 0 B | bug fix — now zero-array |
The 0–5-arg paths are unchanged from v1.0.2 (already correct).
Full Changelog: v1.0.2...v1.0.3