Skip to content

make log4net usable from a PublishAOT build (#306) - #306

Open
FreeAndNil wants to merge 1 commit into
masterfrom
Feature/306-aot-calling-assembly
Open

make log4net usable from a PublishAOT build (#306)#306
FreeAndNil wants to merge 1 commit into
masterfrom
Feature/306-aot-calling-assembly

Conversation

@FreeAndNil

@FreeAndNil FreeAndNil commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Native AOT broke log4net in three ways (#233).

Assembly.GetCallingAssembly() throws PlatformNotSupportedException there, so
every overload that resolves the repository from the caller failed -
LogManager.GetLogger(Type) among them. Guard the 18 call sites with
CallerAssembly.IsSupported, a flag probed once, and fall back to the entry
assembly when the runtime does not implement the call. The call itself has to
stay in the public method whose caller is wanted, so it cannot be moved into
the helper.

SystemInfo.GetAppSetting() then reported a caught failure on every lookup,
because a trimmed System.Configuration cannot initialize. Tell that apart from
a configuration file that does not parse - Native AOT surfaces both as a
ConfigurationErrorsException, so only the inner exception distinguishes them -
and treat a missing configuration system as a property of the runtime rather
than a fault: log it at debug level and let environment variables stand in for
the config file, as they already do on Android. A malformed config file is
still reported as an error and still yields no setting.

Finally the trimmer removed the constructors of everything log4net creates
reflectively, so no repository, pattern converter or locking model could be
instantiated. Annotate that flow with DynamicallyAccessedMembers - polyfilled
here, because the trimmer matches it by name and neither target framework
declares it - and hold the built-in converters in a Dictionary of ConverterInfo
rather than of Type, since a Type placed in a collection loses its annotation.
The registries are now built through a generic method whose new() constraint
states the same requirement structurally, so a converter without a public
parameterless constructor fails to compile instead of failing in a trimmed
build.

Configuration still has to be done in code: XmlConfigurator names its types in
strings and cannot work once they have been trimmed. Document that, and the
fact that loggers from non-entry assemblies land in the entry assembly's
repository, on a new Native AOT page in the manual.

FreeAndNil added a commit that referenced this pull request Aug 5, 2026
Native AOT broke two things in the startup path (fixes #233 partially).

Assembly.GetCallingAssembly() throws PlatformNotSupportedException there, so
every overload that resolves the repository from the caller failed -
LogManager.GetLogger(Type) among them. Guard the 18 call sites with
CallerAssembly.IsSupported, a flag probed once, and fall back to the entry
assembly when the runtime does not implement the call. The call itself has to
stay in the public method whose caller is wanted, so it cannot be moved into
the helper.

SystemInfo.GetAppSetting() then failed as well, because System.Configuration
is trimmed away. Its catch never saw that: resolving the missing assembly
fails on entry to the method, before the try region, so the exception escaped
the static constructor as a TypeInitializationException and killed the
process. Read the setting through a separate, never inlined method so the
failure is raised inside the try block, latch the result so a permanent
failure is reported once rather than per lookup, and fall back to environment
variables the way the Android branch already does. That fallback also makes
log4net.NullText and log4net.NotAvailableText settable under AOT, where they
previously could not be configured at all.

Note that the fallback applies on .NET Framework too: a malformed app.config
now reads settings from the environment instead of returning null.

This does not make log4net AOT-clean - repositories, appenders and layouts are
still instantiated via Activator.CreateInstance, so an AOT app still fails
with MissingMethodException on Hierarchy's constructor.
@FreeAndNil
FreeAndNil force-pushed the Feature/306-aot-calling-assembly branch from 26841b2 to 9ca8d88 Compare August 5, 2026 21:15
Native AOT broke log4net in three ways (#233).

Assembly.GetCallingAssembly() throws PlatformNotSupportedException there, so
every overload that resolves the repository from the caller failed -
LogManager.GetLogger(Type) among them. Guard the 18 call sites with
CallerAssembly.IsSupported, a flag probed once, and fall back to the entry
assembly when the runtime does not implement the call. The call itself has to
stay in the public method whose caller is wanted, so it cannot be moved into
the helper.

SystemInfo.GetAppSetting() then reported a caught failure on every lookup,
because a trimmed System.Configuration cannot initialize. Tell that apart from
a configuration file that does not parse - Native AOT surfaces both as a
ConfigurationErrorsException, so only the inner exception distinguishes them -
and treat a missing configuration system as a property of the runtime rather
than a fault: log it at debug level and let environment variables stand in for
the config file, as they already do on Android. A malformed config file is
still reported as an error and still yields no setting.

Finally the trimmer removed the constructors of everything log4net creates
reflectively, so no repository, pattern converter or locking model could be
instantiated. Annotate that flow with DynamicallyAccessedMembers - polyfilled
here, because the trimmer matches it by name and neither target framework
declares it - and hold the built-in converters in a Dictionary of ConverterInfo
rather than of Type, since a Type placed in a collection loses its annotation.
The registries are now built through a generic method whose new() constraint
states the same requirement structurally, so a converter without a public
parameterless constructor fails to compile instead of failing in a trimmed
build.

Configuration still has to be done in code: XmlConfigurator names its types in
strings and cannot work once they have been trimmed. Document that, and the
fact that loggers from non-entry assemblies land in the entry assembly's
repository, on a new Native AOT page in the manual.
@FreeAndNil
FreeAndNil force-pushed the Feature/306-aot-calling-assembly branch from 9ca8d88 to bf839f7 Compare August 6, 2026 20:35
@FreeAndNil
FreeAndNil marked this pull request as ready for review August 6, 2026 20:39

@fluffynuts fluffynuts left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work - just a suggestion to consolidate the logic which determines the caller assembly or falls back on the entry assembly.

/// <seealso cref="Log4NetConfigurationSectionHandler"/>
public static ICollection Configure()
=> Configure(LogManager.GetRepository(Assembly.GetCallingAssembly()));
=> Configure(LogManager.GetRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this logic quite a few times throughout - perhaps move to CallerAssembly with a lazy backing field and reference that static property elsewhere (eg CallerAssembly.ResolvedCallerAssembly? Main reason being that it's no longer just an obvious call to Assembly.GetCallingAssembly(), but now includes logic, which is repeated in quite a few places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fluffynuts
Good catch, but this one can't move - though I don't like it either. Assembly.GetCallingAssembly()
returns the caller of the method containing the call, so in a property on CallerAssembly the caller
is log4net itself - every logger would land in log4net's own repository. Two-assembly harness,
called from UserApp:

inline (current PR)      -> UserApp       <- correct
via property (suggested) -> log4net

The lazy backing field is worse: the first assembly to touch it wins forever, so the result depends on
load order.

The BCL hits this exact problem and needs an internal enum for it - System.Threading.StackCrawlMark
(LookForMyCaller, LookForMyCallersCaller), passed by ref so Assembly.Load can delegate to a
private helper. It's NotPublic and no public API accepts it. It also wouldn't help us: it's
stack-walking machinery, and AOT throws precisely because there is no stack to walk.

Only a Roslyn interceptor would actually remove the repetition - left out here, but I'm happy to open a separate issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I think [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
is the bigger eyesore.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I was also looking at that - but if I were to guess wildly, the attribute as found (when found) in the dotnet runtime, is likely sealed. Otherwise I'd suggest sub-classing with those settings - which still may not even work if the logic doesn't bother to look at attributes' base types (quite likely). I don't think there's much that can be done about it :/

@gdziadkiewicz
gdziadkiewicz requested a balanced review from Copilot August 12, 2026 07:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR makes log4net usable when consumers publish with Native AOT / trimming by guarding unsupported runtime APIs, avoiding repeated configuration-system failures, and annotating reflection-based activation paths so required constructors survive trimming.

Changes:

  • Guarded Assembly.GetCallingAssembly() usage via a cached runtime probe with an entry-assembly fallback.
  • Improved SystemInfo.GetAppSetting() behavior under trimmed/missing System.Configuration by falling back to environment variables and reducing noise.
  • Added trimming annotations (DynamicallyAccessedMembers) and refactored pattern-converter registries to preserve constructors; documented Native AOT constraints in the manual.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/site/antora/modules/ROOT/pages/manual/native-aot.adoc Adds a Native AOT/trimming manual page with guidance and examples.
src/site/antora/modules/ROOT/pages/manual/configuration.adoc Notes XML config is unavailable under PublishAot and links to Native AOT guidance.
src/site/antora/modules/ROOT/nav.adoc Adds the new Native AOT page to the manual navigation.
src/log4net/Util/CallerAssembly.cs Introduces a runtime probe + fallback assembly for unsupported GetCallingAssembly().
src/log4net/LogManager.cs Routes calling-assembly-based overloads through the guard/fallback.
src/log4net/Config/BasicConfigurator.cs Uses guarded calling-assembly resolution for repository selection.
src/log4net/Config/XmlConfigurator.cs Uses guarded calling-assembly resolution to select repositories.
src/log4net/Util/SystemInfo.cs Adds config-system detection, env-var fallback, and a non-inlined settings reader.
src/log4net/Util/TypeConverters/ConverterRegistry.cs Annotates converter Type flows to keep parameterless ctors under trimming.
src/log4net/Layout/PatternLayout.cs Refactors built-in converter registry to retain trimming annotations.
src/log4net/Util/PatternString.cs Same registry refactor for PatternString converters.
src/log4net/Util/ConverterInfo.cs Annotates ConverterInfo.Type to preserve constructors.
src/log4net/Core/LoggerManager.cs Annotates repository type activation paths.
src/log4net/Core/IRepositorySelector.cs Annotates repository type parameters in the interface.
src/log4net/Core/DefaultRepositorySelector.cs Propagates repository-type annotations and stores annotated default type.
src/log4net/Config/RepositoryAttribute.cs Annotates repository type property to preserve constructors.
src/log4net/Appender/FileAppender.cs Adds trimming-safe constraints/annotations for default locking model activation.
src/log4net/Diagnostics/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs Adds a polyfill attribute for TFMs lacking it (for trimmer recognition).
src/log4net/Diagnostics/CodeAnalysis/DynamicallyAccessedMemberTypes.cs Adds a polyfill enum for TFMs lacking it (for trimmer recognition).
src/log4net.Tests/log4net.Tests.csproj Links CallerAssembly.cs into tests.
src/log4net.Tests/Util/SystemInfoTest.cs Adds tests for config-system fallback and detection behavior.
src/log4net.Tests/Util/CallerAssemblyTest.cs Adds tests validating the guarded calling-assembly behavior on JIT.
src/changelog/3.4.0/306-usable-from-a-publishaot-build.xml Adds a changelog entry describing the Native AOT fixes and limitations.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +46 to +51
PublicParameterlessConstructor = 0x0001,

/// <summary>
/// Specifies all public constructors.
/// </summary>
PublicConstructors = 0x0002 | PublicParameterlessConstructor,
Comment on lines +733 to +744
private static bool IsMissingConfigurationSystem(Exception? exception)
{
for (; exception is not null; exception = exception.InnerException)
{
if (exception is MissingMethodException or TypeLoadException or FileNotFoundException
or PlatformNotSupportedException or NotSupportedException)
{
return true;
}
}
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also raised my attention. Allow me to test it and provide evidence that this can bite us (or report that I failed to get such scenario).

Comment on lines +688 to +689
if (IsAndroid || _configurationSystemUnavailable)
return Environment.GetEnvironmentVariable(key);
// There is no configuration system to read - Native AOT trims System.Configuration away.
// That is a property of the runtime rather than a fault, so it is not reported as an
// error, and the environment stands in for the config file as it does on Android.
_configurationSystemUnavailable = true;
[MethodImpl(MethodImplOptions.NoInlining)]
private static string? ReadAppSetting(string key) => ConfigurationManager.AppSettings[key];

private static bool _configurationSystemUnavailable;
Comment on lines +981 to 984
foreach (KeyValuePair<string, ConverterInfo> entry in _sGlobalRulesRegistry)
{
ConverterInfo converterInfo = new()
{
Name = entry.Key,
Type = entry.Value
};
patternParser.PatternConverters[entry.Key] = converterInfo;
patternParser.PatternConverters[entry.Key] = entry.Value;
}
<VSTestLogger>quackers</VSTestLogger>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\log4net\Util\CallerAssembly.cs" Link="Util\CallerAssembly.cs" />
Comment on lines +184 to +186
/// That failure cannot be provoked on a JIT runtime, so the latch that records it is flipped
/// directly, the same way <see cref="IsAndoid"/> reaches a non-public member. The environment
/// must stay untouched while the configuration system still works, otherwise a malformed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants