This file provides guidance to coding agents when working with code in this repository.
See SECURITY.md for the threat model and security boundaries.
Derived from .editorconfig, src/Directory.Build.props, src/log4net.globalconfig and the
existing sources. Match the surrounding file first; the notes below are what that file will
almost always be doing.
- 2-space indent, spaces not tabs, CRLF line endings.
- Every source file starts with the
#region Apache License/#endregionASF header (238 of 244 files insrc/log4net). Copy it verbatim into new files. - File-scoped namespaces (
namespace log4net.Appender;). Note.editorconfigstill sayscsharp_style_namespace_declarations = block_scoped:silent, but 242 of 244 files are file-scoped: follow the code, not that setting. usingdirectives outside the namespace, in one contiguous block.
- Explicit types, not
var: all threecsharp_style_var_*options arefalse. WriteStringWriter writer = new(...). - Target-typed
new()and collection expressions (private static readonly char[] _x = [',', ';'];). Omit the type wherever the target is known, includingreturn new(…);and=> new(…);, where the enclosing member's return type supplies it. It cannot be omitted when the target type is an interface or abstract class, as inFunc<ISmtpTransport> f = () => new MailKitSmtpTransport();. x?.Method() ?? falserather thanx is not null && x.Method(), and merge nested guards into one condition.- In a method whose conditions are hard to follow, give their parts names: small local functions
such as
IsBaseFileRolledByDate()orHasNumberedBackups(), even one-liners and even chained, so theifreads as a sentence. SeeRollingFileAppender.DoesDatePatternCollideWithBackupNames. In review, do not suggest inlining them to save lines. A simple guard stays inline. - Expression-bodied members whenever the body fits on one line, including constructors
(
resharper_constructor_or_destructor_body = expression_body). - Braces on
if/elsebodies even for a single statement. LangVersionislatest, and current C# features are welcome and in use: primary constructors (csharp_style_prefer_primary_constructors = true), thefieldkeyword in property accessors, list patterns,switchexpressions.- Wrap long string literals with a multi-line raw string (
"""), never with+concatenation. This includes attribute arguments; see the[Obsolete(...)]message onlog4net.Appender.SmtpAppender. Raw strings have no line-continuation, so each source line break really is a\nin the value. For a compiler diagnostic that is free, since the tooling renders those newlines as spaces. For a message that is logged, throughLogLogor anErrorHandler, the breaks reach the output:LogLogprefixes once per message, so the continuation lines carry nolog4net:ERROR. That is accepted, and the rule holds there too; do not reintroduce+concatenation to keep such a message on one line. Raw strings are constant expressions, so they are legal in attributes, and the feature is purely syntactic, so it works onnet462/netstandard2.0too. - Private fields are
_camelCase. Private fields and helper methods are commonly placed after the public surface of the type rather than at the top. - A small private carrier type is a
readonly record structwith a primary constructor. Extra intent-revealing constructors chain onto it (internal Item(T payload) : this(payload, null) { }). - Infrastructure that has to be
publiconly to cross an assembly boundary gets[EditorBrowsable(EditorBrowsableState.Never)]and a<remarks>paragraph saying why it is public, so it stays out of consumers' IntelliSense. Seelog4net.Util.BackgroundSender.
Nullableis enabled solution-wide withWarningsAsErrors=nullable: any nullability warning is a build error, so it cannot be deferred.log4nettargetsnet462;netstandard2.0. Neither reference assembly is nullable-annotated, so BCL postcondition attributes are invisible to the compiler.string.IsNullOrEmptydoes not narrow astring?, which is whyLog4NetAssert.EnsureNotNullOrEmptytestsvalue is string { Length: > 0 }instead. Reach for a pattern the compiler's flow analysis understands before reaching for!or a#pragma.- Missing framework attributes are polyfilled as
internaltypes undersrc/log4net/Diagnostics/CodeAnalysis/(NotNullAttribute,CallerArgumentExpressionAttribute,ValidatedNotNullAttribute, …).
- Use the internal
log4net.Util.Log4NetAssertextensions rather than hand-rolled checks:EnsureNotNull(),EnsureNotNullOrEmpty(),EnsureIs<T>(). They carry[CallerArgumentExpression(nameof(value))], so no argument name is passed at the call site. This includes constructor and property assignments: write_x = x.EnsureNotNull();, not_x = x ?? throw new ArgumentNullException(nameof(x));. - Appenders never let exceptions escape to the caller. The house pattern is
catch (Exception e) when (!e.IsFatal()) { ErrorHandler.Error("...", e); }. - A catch of
Exceptionalways carrieswhen (!e.IsFatal()). The filter is not a licence to widen: a catch that handles one expected exception keeps naming that type. Widening it toExceptionturns an unrelated bug into a silently counted failure, which is the opposite of what the idiom is for. - On a background thread the same rule is harder: an exception that escapes the thread body is
unhandled and takes the process down. Everything the thread calls out to, the error reporting
included, has to be wrapped, because a caller-supplied error handler can throw too. See the
Reporthelper inlog4net.Util.BackgroundSender, and itsfinallyblock, which may run against an already disposed queue.
- All package versions live in
src/Directory.Build.propsas<XxxPackageVersion>properties. Never hardcode a version in a.csproj. - Such a version is a floor, and raising one is a minor-release change, never part of a fix.
NuGet resolves to the maximum of all requests, so a consumer who updates gets the newer
version through their own graph whatever our floor says; the only consumers a floor moves are
those who pinned deliberately, and they get an
NU1605downgrade error that stops their build. Argue a bump on dependency-hygiene grounds on its own, not as a fix for the symptom that prompted it. System.Configuration.ConfigurationManageris referenced only whenTargetFramework != net462(src/log4net/log4net.csproj:82-88), so its floor affects thenetstandard2.0asset alone.Log4NetAssertand theDiagnostics/CodeAnalysisattributes areinternaltolog4net. Satellite projects therefore compile the sources in rather than referencing them:<Compile Include="..\log4net\Util\Log4NetAssert.cs" Link="Util\Log4NetAssert.cs" />(seelog4net.Ext.Mail.csprojandlog4net.Tests.csproj). LinkingLog4NetAssertalso requires linkingNotNullAttribute,ValidatedNotNullAttributeandCallerArgumentExpressionAttribute, or you getCS0122.log4netis strong named (SignAssembly,log4net.snk);log4net.Ext.Maildeliberately is not, and says so in its.csproj. The CLR forbids a strong-named assembly from grantingInternalsVisibleToto an unsigned one, so no internal oflog4netcan ever be reached fromlog4net.Ext.Mail. Shared infrastructure is therefore eitherpublic(with[EditorBrowsable], above) or compiled in by<Compile Include>asLog4NetAssertis.- Analyzers (
Microsoft.CodeAnalysis.NetAnalyzers,AnalysisLevel 8,src/log4net.globalconfig) run on every build. The solution builds with 0 warnings, keep it that way. Two that bite new code: CA1711 rejects a type name ending inQueue,CollectionorFlags, and CA2000 rejects anIDisposablethat is not disposed on every path. Prefer a design that removes the warning over suppressing it: swapping aManualResetEventSlimfor aTaskCompletionSource<bool>dropped CA2000 and the disposal race behind it at once. Note the non-genericTaskCompletionSourcedoes not exist onnet462/netstandard2.0, so useTaskCompletionSource<bool>.
- Keep them short. A doc comment is one or two sentences, an inline comment is one line. If a remark wants a second paragraph, the reason belongs in the commit message or the changelog, not in the source. Say what the code does not already say, then stop. This is the single most common correction made in review, so err shorter than feels right.
- Every public and protected member gets an XML doc comment, in test code as well as production code: test methods, nested helper classes and hand-written fakes included.
- Use
/// <inheritdoc/>when the member implements an interface or overrides a base member, and a real<summary>for everything else.Log4NetTransactionin the AdoNet test doubles is the pattern to copy. - When checking whether a member is documented, remember that
[Test],#pragmaand// ReSharper disablelines legitimately sit between the doc comment and the declaration.
- Never use an em dash (
—) or en dash (–). Use a plain hyphen, or restructure with a colon, comma or parentheses. This covers comments, XML docs, commit messages, AsciiDoc and chat. - In AsciiDoc,
--is also forbidden: Asciidoctor renders a spaced double hyphen as an em dash, so it breaks the rule even though the source looks like plain hyphens. Grep touched files for[—–]and--before presenting a change. - No underscores in identifiers, including test method names.
AllContainsEveryFlag, notAll_ShouldContainAllFlags. (Private fields are_camelCase, which is the one exception.)
- NUnit 4, not MSTest, and always the constraint model:
Assert.That(actual, Is.EqualTo(expected))(810 uses ofAssert.That, zero ofAssert.AreEqual).[TestFixture],[Test],[TestCase], with[SetUp]/[TearDown]for per-test state. - Use an expression body for a single-statement test:
public void X() => Assert.That(...);. log4nethas noInternalsVisibleTo, so private and internal members are exercised through reflection, not by widening their accessibility.log4net.Ext.Maildoes grantInternalsVisibleToto its own test project.- That reflection goes through
ReflectionExtensionsinsrc/log4net.Tests/Util/, namespacelog4net.Tests, so no test spells outBindingFlagsitself:NonPublicNestedType,Construct<T>,Invoke<T>,GetFieldValue<T>,SetFieldValue, on aTypefor the static members and on an instance for the instance ones. Pass the arguments as an explicit[a, b]; an expandedparamsargument trips CS8620 in a C# 14 extension block, whichWarningsAsErrors=nullablemakes a build error. - NUnit constructs one fixture instance for the whole fixture, so an instance field that
records what a test observed accumulates across the tests in it. Clear such state in
[SetUp]. - Drive a test over a background thread with gates (
ManualResetEventSlim), never withThread.Sleep: park the worker, assert the state you care about, then release it. SeeBackgroundSenderTest, where every wait has a generous timeout and the assertions are exact. Does.Containis culture sensitive and cannot be made ordinal: itsContainsConstrainthas no comparison parameter. Linguistic comparison skips ignorable characters, soDoes.Not.Containreports a match for NUL, soft hyphen or a zero-width character in a string that holds none. When the assertion is about control characters, useContains.Substring(x).Using(StringComparison.Ordinal), negated with the!operator thatConstraintdefines, or assert the whole value withIs.EqualTo, which is ordinal.- Order
[TestCase]attributes shortest to longest by source line, not by argument length. - If no black-box test can reach a defect, extract the sequence into a small private helper
and drive that by reflection. Do not delete the test and call the defect untestable. The
extraction is usually an improvement anyway:
FileAppender.RunWithBestEffortLockreplaced two copies of an acquire/release pair, one of which released a lock it had failed to take. - A test that passes before the fix is worthless. Revert the production change and watch it fail; if it does not, the test is wrong or the defect is not where you think it is.
- Give a
[TestCase]an explicitTestNamewhen an argument holds a control character. Otherwise the whole fixture can become invisible todotnet test --filter, silently: it is listed by--list-testsand runs in a full pass, but every filter reports "No test matches". Reproduced with[TestCase("one", "\x1b[0m")]; a single argument holding the same escape is fine, so it takes two arguments and an escape character.AnsiColorTerminalAppenderTestnames all ten of its cases for that reason, and a filtered run there is 54 ms against 9 s for the suite. - A test that needs a folder takes
using AutoTempFolder folder = new();fromPeanutButter.Utils, as a local in the test method rather than a fixture field. It creates the folder and deletes it on dispose, so no[SetUp]/[TearDown]pair and no NUnit1032. Close the repository or appender before the scope ends, or the delete fails on Windows. - A test that changes an environment variable takes
using AutoTempEnvironmentVariable v = new("NAME", "value");from the same package, again as a local. It restores the previous value, or removes the variable if there was none, on dispose, so notry/finallyaround the assertions. Only the process level, so a test that needsEnvironmentVariableTarget.Useror.Machinestill cleans up itself. - Mark a test
[NonParallelizable]when it mutates static state (LogLog.InternalDebugging, a static field on a test double, a process-wide native registration). - Wrap expected internal logging in
LogLog.ExecuteWithoutEmittingInternalMessages(...)and capture it withLogLog.LogReceivedAdapterrather than letting it reach the console. Appender errors are emitted by default, so a test that provokes one will otherwise add noise to the suite output. - Guard platform-specific tests with
[Platform("Win")]/[Platform("Linux")]. A test that only runs on Windows leaves the behaviour unverified in local Linux runs, so prefer a cross-platform home for the assertion when one exists. - When a diagnosis genuinely needs the other operating system, ask the user to continue the work in a session on that platform rather than approximating it. Many developers work on both Linux and Windows and can switch, so leave a handoff note with what is established and what still needs the other machine, as was done for issue #162.
NUnit.Analyzerswarnings are errors too: for example NUnit1032 requires anIDisposablefixture field to be disposed in a[TearDown]method.- For code that talks to the outside world, introduce a narrow interface and hand-write a fake;
there is no mocking library in any test project. See
ISmtpTransport/FakeSmtpTransport. - Verify with
dotnet build src/log4net.slnanddotnet test src/<project>.Tests/<project>.Tests.csproj. - When inspecting build output, redirect it to a file and read the whole thing; do not pipe
MSBuild through line-oriented tools.
grep/Select-Stringcannot match across newlines, and MSBuild's console logger formats differently when piped than when redirected, so a multi-line diagnostic message then looks truncated when it is not. Before reporting that the toolchain mangles something, re-check withdotnet build … > out.txt 2>&1and inspectout.txt.
SystemInfo.GetAppSetting degrades to environment variables when the configuration system is
unavailable, and IsMissingConfigurationSystem decides that from the shape of the exception. The
unit tests construct those exceptions by hand; to see the real thing:
- The trigger is
Assembly.GetEntryAssembly() == null, which is true in any process that hosts the runtime natively. Reflecting onto the internalAssembly.SetEntryAssembly(null)reproduces it in-process on .NET 10, P/Invokinghostfxrfrompowershell.exe5.1 loads the CoreCLR side by side in a .NET Framework process, and a C++ host built withcl.exeis the real case from issue #162. All three produce the same exception chain. - Which
System.Configuration.ConfigurationManagerasset is loaded decides the behaviour. The net4x assets are around 92 KB and only forward types to the in-boxSystem.Configuration, which handles a null entry assembly happily; thenetstandard2.0asset (around 382 KB at 4.5.0) is the ported implementation that fails. Anetstandard2.0build output dropped into a .NET Framework host therefore behaves unlike the same library restored from NuGet onnet4x.
Every user-visible change gets an entry in src/changelog/<unreleased version>/, named
<issue>-<kebab-case-slug>.xml. The format is the log4j changelog schema:
typeis one ofadded,changed,fixed,removed,updated.- Every
<issue>element requires bothidandlink; the export fails withmissing attribute: linkotherwise, which is only caught by the Maven site build. - Put anything that has no issue number, such as an external finding identifier, in the description
text rather than inventing an
<issue>for it. - The description is whitespace-collapsed before the AsciiDoc transform, so block syntax does
not survive. No bullets, no code blocks:
*ends up mid-sentence as a literal asterisk. Write prose. Bullets are fine in commit messages. - Close the description with an attribution in parentheses, crediting both sides: who raised it and
who did the work, as in
(reported by @viktorgobbi, fixed by @FreeAndNil).implemented byreads better thanfixed byfor anaddedorchangedentry. Do not append the pull request link to the attribution: the<issue>element above the description already carries it, and entries that repeat it are the older form. Take the fixer from the active committers inSTATUS.txt, whose Apache ids are the GitHub handles (freeandnil,gdziadkiewicz,davydm), and identify which one from the session'sgit config user.email. Ask rather than guess if that does not match a listed committer. src/changelog/3.3.2/298-fix-interprocesslock-mutex-leak.xmlshows the shape for a change that came out of an external audit.
The manual lives in src/site/antora/modules/ROOT/pages/. A new appender page needs three edits,
not one: the page itself, an xref line in nav.adoc (kept alphabetical), and the appender table
in manual/configuration/appenders.adoc.
The API reference is DocFX output from the XML doc comments, built by scripts/build-api-docs.ps1
into target/site/api and in CI with warnings as errors, so an unresolved cref or a missing
public doc comment fails the PR. The configuration is in src/site/docfx/; the log4net and
log4net.Ext.Mail folders generated there are ignored. log4net is documented as net462, since
the NET462_OR_GREATER types exist only there. The Ext.Mail entry sets ProduceReferenceAssembly,
without which Roslyn cannot match its project reference to log4net and warns.
- Before writing a helper function in a script, grep
scripts/for its name and its idiom (Push-Location,Remove-Item,Get-Date). A function two scripts need lives inscripts/Common.psm1, imported withImport-Module $PSScriptRoot/Common.psm1 -Force, not copied into the second script. A module takes its preferences from the global scope, so the module sets$ErrorActionPreferenceitself. - Every file
build-release.ps1writes gets LF on every platform.Set-Contentwrites[Environment]::NewLine; use-NoNewlinewith the lines joined by`n. A CR is invisible in an editor, andsha512sumon macOS reads it as part of the file name. - Each release ships the verifier built with it, so never add backward compatibility to
verify-release.ps1or.sh: a compatibility skip path is a hole, not a courtesy. - The scripts are tested with Pester through
scripts/FakeCommands.TestHelper.ps1, which shadowsdotnet,git,gpg,zip,mvnwandcurlwith shims on a prependedPATH(FAKE_FAILnames a command, or a command and its first argument such asdotnet docfx), because PesterMockintercepts only functions and cmdlets. CI runs the suite on macOS, Ubuntu and Windows, so keep the helper's$IsWindowsbranch working and put byte-level assertions where all three see them.
SECURITY.md decides whether something is in scope and whether it is a vulnerability. Read it before triaging a report, and describe a finding in commit messages and changelog entries the way it comes out of that assessment: a correctness bug, a reliability defect or hardening is none the worse for being called one.
What that leaves for this file is where the answers live in the code:
- When a report is likely to recur on a path the threat model already settles, leave a short comment
at the site with a link to the model rather than changing the code.
XmlConfiguratorandXmlHierarchyConfiguratorcarry these for the configuration-is-trusted paths, andSystemStringFormatfor the format string. log4net.Appender.Internal.ContentEscapeandRemoteSyslogAppender.ValidateIdentityare the two sides of the content and structural-identifier rule: content is escaped and never rejected, a malformed identifier is reported rather than quietly repaired.- A sink that cannot carry a character escapes it visibly, and never drops the character, the
rest of the record, or the event. The escapes already in use are
\0for NUL,\rand\nfor newlines, and\uXXXXfor anything else, inContentEscapeand inRemoteSyslogAppender.AppendMessage. Put new ones inContentEscaperather than in the appender: four appenders have needed the same two so far. Escaping before a length limit, not after, since an escape is longer than what it replaces. - Deliberate secure-default choices belong in the changelog with their opt-out named, so that an
upgrade surprise is searchable. See the entries for
SendTimeoutMillis,MatchTimeoutMillisandLockTimeoutMillis.
