Native AOT and trimming
log4net can be used from an application published with PublishAot or PublishTrimmed, with one
important restriction: you have to configure log4net in code.
A Native AOT application is compiled ahead of time and trimmed, so any type that is only ever named in a string is removed from the build. That is exactly how XML configuration works, which is why it cannot be supported.
Configuring in code
Build the appenders and layouts yourself and hand them to
BasicConfigurator.
Because you construct them with new, the compiler sees them and keeps them:
using log4net;
using log4net.Appender;
using log4net.Config;
using log4net.Core;
using log4net.Layout;
ConsoleAppender appender = new()
{
Layout = new PatternLayout("%level %logger - %message%newline"),
Threshold = Level.All,
};
appender.ActivateOptions();
BasicConfigurator.Configure(appender);
ILog log = LogManager.GetLogger(typeof(Program));
log.Info("Hello from Native AOT.");
Conversion patterns work as usual. The built-in pattern converters are resolved by name at run time, but log4net declares them in a way the trimmer understands, so they are preserved for you.
A custom converter is preserved as long as you register it by type:
PatternLayout layout = new();
layout.AddConverter("mine", typeof(MyPatternConverter));
layout.ConversionPattern = "%mine %message%newline";
layout.ActivateOptions();
Reading levels from a configuration file
log4net cannot read a configuration file under Native AOT, but your application can, and levels can be set at any time through the API. That covers the common case of wanting to change verbosity without rebuilding, without needing XML configuration.
Put the levels wherever your application already keeps its settings:
{
"Logging": {
"Default": "WARN",
"Loggers": {
"Noisy.Component": "ERROR",
"Important.Component": "DEBUG"
}
}
}
Read them yourself and apply them to the repository:
using System.Text.Json;
using log4net.Core;
using log4net.Repository.Hierarchy;
Hierarchy hierarchy = (Hierarchy)LogManager.GetRepository();
using JsonDocument document = JsonDocument.Parse(
File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "appsettings.json")));
JsonElement logging = document.RootElement.GetProperty("Logging");
// the level of the root logger, inherited by every logger that has none of its own
if (hierarchy.LevelMap[logging.GetProperty("Default").GetString()!] is Level rootLevel)
{
hierarchy.Root.Level = rootLevel;
}
// and levels for individual loggers
foreach (JsonProperty entry in logging.GetProperty("Loggers").EnumerateObject())
{
if (hierarchy.LevelMap[entry.Value.GetString()!] is Level loggerLevel)
{
((Logger)hierarchy.GetLogger(entry.Name)).Level = loggerLevel;
}
}
With the settings above, Important.Component logs from DEBUG upwards, Noisy.Component only
ERROR and above, and every other logger inherits WARN from the root.
|
|
|
|
Levels can be changed whenever you like, so the same code can be run again to reload the file while the application is running.
What does not work
|
XML configuration - This is about log4net reading the file. Your application can still read a file of its own and apply what it finds - see Reading levels from a configuration file. |
ConfigurationManager cannot initialize either, so log4net’s own appSettings keys are read from
environment variables instead. To set them, use the same names you would have used in
app.config:
log4net.NullText=NULL
log4net.NotAvailableText=N/A
Loggers and repositories
Assembly.GetCallingAssembly() is not implemented by Native AOT. The log4net methods that infer a
repository from their caller - LogManager.GetLogger(string), LogManager.GetLogger(Type),
LogManager.GetRepository() and their siblings - therefore fall back to the entry assembly.
For most applications this changes nothing, because there is a single default repository and both answers lead to it. It matters only if you use per-assembly repositories, for example by placing
[assembly: log4net.Config.Repository("MyRepository")]
on a library. Under Native AOT that library’s loggers are placed in the entry assembly’s repository rather than its own.
If you depend on this, use the overloads that take the assembly explicitly. They are exact on every runtime and need no special handling:
ILog log = LogManager.GetLogger(typeof(MyType).Assembly, typeof(MyType));