diff --git a/.editorconfig b/.editorconfig
index e50b5f2f1..35c9c6ac7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -206,6 +206,9 @@ dotnet_diagnostic.KR1037.severity = none
dotnet_diagnostic.NUnit2045.severity = none
# IDE0079: Remove unnecessary suppressions
dotnet_diagnostic.IDE0079.severity = none
+# IDE0005: Remove unnecessary usings. Shown in the IDE only: enforcing it on build would also need
+# EnforceCodeStyleInBuild and GenerateDocumentationFile, which the test projects do not set.
+dotnet_diagnostic.IDE0005.severity = warning
[*.xml]
tab_width = 2
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index f2af14e8e..2599ffcd0 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -56,4 +56,20 @@ jobs:
- name: Test
run: |
- dotnet test ./src/log4net.sln
\ No newline at end of file
+ dotnet test ./src/log4net.sln
+
+ # Runs the same probes twice, JIT compiled and published with Native AOT, so that a
+ # difference between the two is caused by AOT rather than by the platform. The project
+ # knows which probes are expected to fail in each mode and exits non-zero when reality
+ # does not match, in either direction.
+ - name: AOT probes
+ shell: pwsh
+ env:
+ "log4net.AotEnvironmentProbe": from-environment
+ run: |
+ dotnet run --project ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ dotnet publish ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release -o ./aot-probes
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ & "./aot-probes/log4net.Tests.Aot$($IsWindows ? '.exe' : '')"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3afa2a786..f1035f39d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -254,3 +254,6 @@ validate
/graphify-out/.graphify_python
/graphify-out/*.sig
/graphify-out/20*
+
+# output of the AOT probe step
+/aot-probes/
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 126deb498..9f5f9c07c 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -9,10 +9,18 @@
8
true
true
+
+ true
+ true
<_SkipUpgradeNetAnalyzersNuGetWarning>true
true
en;en-US
+
+
+ $(NoWarn);CS1591
+
3.4.0
3.3.2
diff --git a/src/changelog/3.4.0/306-usable-from-a-publishaot-build.xml b/src/changelog/3.4.0/306-usable-from-a-publishaot-build.xml
new file mode 100644
index 000000000..62c79d4a0
--- /dev/null
+++ b/src/changelog/3.4.0/306-usable-from-a-publishaot-build.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ Make log4net usable from a `PublishAot` build, where `LogManager.GetLogger()`
+ used to throw `PlatformNotSupportedException` from `Assembly.GetCallingAssembly()`, and where
+ repositories and pattern converters were left without a constructor by the trimmer. Configuration
+ has to be done in code - see the new
+ https://logging.apache.org/log4net/latest/manual/native-aot.html[Native AOT and trimming] page
+ (reported by @vpenades, implemented by @FreeAndNil in https://github.com/apache/logging-log4net/pull/306[#306])
+
diff --git a/src/integration-testing/log4net-611-lib/DerivedAppender.cs b/src/integration-testing/log4net-611-lib/DerivedAppender.cs
index 57e3d4c6d..8e48af8a2 100644
--- a/src/integration-testing/log4net-611-lib/DerivedAppender.cs
+++ b/src/integration-testing/log4net-611-lib/DerivedAppender.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics;
using log4net.Appender;
using log4net.Core;
diff --git a/src/log4net.Ext.Mail.Tests/Appender/FakeSmtpTransport.cs b/src/log4net.Ext.Mail.Tests/Appender/FakeSmtpTransport.cs
index 7b268952b..47ba1e5f7 100644
--- a/src/log4net.Ext.Mail.Tests/Appender/FakeSmtpTransport.cs
+++ b/src/log4net.Ext.Mail.Tests/Appender/FakeSmtpTransport.cs
@@ -23,7 +23,6 @@
using System.Net;
using System.Text;
-using log4net.Ext.Mail.Appender;
using log4net.Ext.Mail.Appender.Internal;
using MailKit.Security;
diff --git a/src/log4net.Ext.Mail.Tests/Appender/SmtpAppenderTest.cs b/src/log4net.Ext.Mail.Tests/Appender/SmtpAppenderTest.cs
index 5a7117c06..bb23df02a 100644
--- a/src/log4net.Ext.Mail.Tests/Appender/SmtpAppenderTest.cs
+++ b/src/log4net.Ext.Mail.Tests/Appender/SmtpAppenderTest.cs
@@ -24,7 +24,6 @@
using System.Text;
using log4net.Core;
using log4net.Ext.Mail.Appender;
-using log4net.Ext.Mail.Appender.Internal;
using log4net.Layout;
using MailKit.Security;
using MimeKit;
diff --git a/src/log4net.Ext.Mail.Tests/log4net.Ext.Mail.Tests.csproj b/src/log4net.Ext.Mail.Tests/log4net.Ext.Mail.Tests.csproj
index 6507ad71b..2165e083c 100644
--- a/src/log4net.Ext.Mail.Tests/log4net.Ext.Mail.Tests.csproj
+++ b/src/log4net.Ext.Mail.Tests/log4net.Ext.Mail.Tests.csproj
@@ -2,7 +2,7 @@
true
net10.0
- NETSDK1138;CS1701
+ $(NoWarn);NETSDK1138;CS1701
Library
bin\$(Configuration)
Debug;Release
@@ -11,7 +11,7 @@
false
TRACE;DEBUG;$(DefineConstants)
- CS8032
+ $(NoWarn);CS8032
quackers
diff --git a/src/log4net.Tests.Aot/App.config b/src/log4net.Tests.Aot/App.config
new file mode 100644
index 000000000..9fc4067c6
--- /dev/null
+++ b/src/log4net.Tests.Aot/App.config
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/src/log4net.Tests.Aot/Probe.cs b/src/log4net.Tests.Aot/Probe.cs
new file mode 100644
index 000000000..cb380545d
--- /dev/null
+++ b/src/log4net.Tests.Aot/Probe.cs
@@ -0,0 +1,36 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+
+namespace log4net.Tests.Aot;
+
+///
+/// One named piece of log4net surface to exercise.
+///
+/// the grouping the probe belongs to
+/// what the probe exercises
+/// the probe itself, which throws to signal failure
+internal sealed record Probe(string Area, string Name, Action Run)
+{
+ ///
+ /// Identifies the probe in the expected failure lists.
+ ///
+ internal string Key => $"{Area}/{Name}";
+}
diff --git a/src/log4net.Tests.Aot/Probes.cs b/src/log4net.Tests.Aot/Probes.cs
new file mode 100644
index 000000000..c9e976964
--- /dev/null
+++ b/src/log4net.Tests.Aot/Probes.cs
@@ -0,0 +1,245 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Xml;
+
+using log4net.Appender;
+using log4net.Config;
+using log4net.Core;
+using log4net.Filter;
+using log4net.Layout;
+using log4net.Repository;
+using log4net.Repository.Hierarchy;
+using log4net.Util;
+
+namespace log4net.Tests.Aot;
+
+///
+/// The log4net surface this project exercises.
+///
+///
+///
+/// Each probe is run identically whether the assembly was JIT compiled or published with Native
+/// AOT, so a difference between the two runs is an AOT effect rather than a platform one.
+///
+///
+internal static class Probes
+{
+ ///
+ /// Probes that are expected to fail under Native AOT, with the reason.
+ ///
+ ///
+ ///
+ /// Keep it honest: an unlisted failure and a listed probe that starts passing both fail the run.
+ ///
+ ///
+ internal static readonly Dictionary ExpectedAotFailures = new(StringComparer.Ordinal)
+ {
+ ["config/XmlConfigurator.Configure(element)"]
+ = "appender and layout types are named as strings in XML, so the trimmer removes them",
+ ["settings/appSettings from app.config"]
+ = "System.Configuration cannot initialize once trimmed; environment variables stand in",
+ };
+
+ ///
+ /// Probes that are expected to fail when the assembly is JIT compiled.
+ ///
+ internal static readonly Dictionary ExpectedJitFailures = new(StringComparer.Ordinal)
+ {
+ ["settings/appSettings from environment"]
+ = "the environment only stands in once the configuration system is known to be unavailable",
+ };
+
+ ///
+ /// Every probe, in the order they are run.
+ ///
+ /// the probes
+ internal static IEnumerable All()
+ {
+ MemoryAppender memory = Configure();
+ return
+ [
+ .. Core(memory),
+ .. Configuration(),
+ .. Each("appender", Appenders(), Activate),
+ .. Each("layout", Layouts(), layout => Render(layout)),
+ .. Patterns(),
+ .. Each("filter", Filters(), Activate),
+ .. Settings(),
+ .. Shutdown(),
+ ];
+ }
+
+ ///
+ /// Wires up the appender the core probes assert against.
+ ///
+ private static MemoryAppender Configure()
+ {
+ MemoryAppender memory = new()
+ {
+ Layout = new PatternLayout("%level %logger %message"),
+ Threshold = Level.All,
+ };
+ memory.ActivateOptions();
+ BasicConfigurator.Configure(memory);
+ return memory;
+ }
+
+ private static IEnumerable Core(MemoryAppender memory)
+ {
+ yield return new("core", "GetLogger(Type)", () => LogManager.GetLogger(typeof(Probes)));
+ yield return new("core", "GetLogger(string)", () => LogManager.GetLogger("by.name"));
+ yield return new("core", "GetRepository()", () => LogManager.GetRepository());
+ yield return new("core", "Exists", () => LogManager.Exists("by.name"));
+ yield return new("core", "GetCurrentLoggers", () => LogManager.GetCurrentLoggers());
+ yield return new("core", "CreateRepository(name)", () => LogManager.CreateRepository("probe-repository"));
+ yield return new("core", "event reaches appender", () =>
+ {
+ memory.Clear();
+ LogManager.GetLogger(typeof(Probes)).Info("hello");
+ Require(memory.GetEvents().Length == 1, "the event did not reach the appender");
+ });
+ yield return new("core", "level lookup by name", () =>
+ {
+ Hierarchy hierarchy = (Hierarchy)LogManager.GetRepository();
+ Require(hierarchy.LevelMap["WARN"] is not null, "WARN is not in the level map");
+ });
+ yield return new("core", "context properties", () =>
+ {
+ ThreadContext.Properties["thread"] = 1;
+ GlobalContext.Properties["global"] = 2;
+ LogicalThreadContext.Properties["logical"] = 3;
+ });
+ }
+
+ private static IEnumerable Configuration()
+ {
+ yield return new("config", "BasicConfigurator.Configure(appender)", () => BasicConfigurator.Configure(new MemoryAppender()));
+ yield return new("config", "XmlConfigurator.Configure(element)", () =>
+ {
+ XmlDocument document = new();
+ document.LoadXml("""
+
+
+
+
+ """);
+ ILoggerRepository repository = LogManager.CreateRepository("xml-repository");
+ XmlConfigurator.Configure(repository, document.DocumentElement!);
+ Require(repository.GetAppenders().Length > 0, "no appenders were created from the XML");
+ });
+ }
+
+ private static IEnumerable Patterns()
+ {
+ yield return new("layout", "PatternLayout with every built-in converter", () =>
+ {
+ PatternLayout layout = new("%a %c %C %d %F %l %L %m %M %n %p %P %r %t %u %w %x %X %utcdate %exception %stacktrace %stacktracedetail");
+ Require(Render(layout).Length > 0, "the layout rendered nothing");
+ });
+ yield return new("layout", "PatternString", () =>
+ Require(new PatternString("%processid %appdomain %date{yyyy}").Format().Length > 0, "the pattern rendered nothing"));
+ }
+
+ private static IEnumerable Settings()
+ {
+ yield return new("settings", "appSettings from app.config", () =>
+ Require(SystemInfo.GetAppSetting("log4net.AotProbe") == "from-config", "the key was not read from app.config"));
+ yield return new("settings", "appSettings from environment", () =>
+ Require(SystemInfo.GetAppSetting(Program.EnvironmentProbeKey) == "from-environment", "the key was not read from the environment"));
+ }
+
+ private static IEnumerable Shutdown()
+ {
+ yield return new("shutdown", "Flush", () => LogManager.Flush(1000));
+ yield return new("shutdown", "Shutdown", LogManager.Shutdown);
+ }
+
+ ///
+ /// One probe per entry, each creating its subject and handing it to .
+ ///
+ private static IEnumerable Each(string area, Dictionary> subjects, Action check)
+ {
+ foreach (KeyValuePair> subject in subjects)
+ {
+ yield return new(area, subject.Key, () => check(subject.Value()));
+ }
+ }
+
+ private static Dictionary> Appenders() => new(StringComparer.Ordinal)
+ {
+ ["ConsoleAppender"] = () => new ConsoleAppender { Layout = new SimpleLayout() },
+ ["MemoryAppender"] = () => new MemoryAppender(),
+ ["DebugAppender"] = () => new DebugAppender { Layout = new SimpleLayout() },
+ ["TraceAppender"] = () => new TraceAppender { Layout = new SimpleLayout() },
+ ["ForwardingAppender"] = () => new ForwardingAppender(),
+ ["BufferingForwardingAppender"] = () => new BufferingForwardingAppender { BufferSize = 2 },
+ ["FileAppender"] = () => new FileAppender { File = TempFile("aot-probe-file.log"), Layout = new SimpleLayout() },
+ ["RollingFileAppender"] = () => new RollingFileAppender { File = TempFile("aot-probe-roll.log"), Layout = new SimpleLayout() },
+ ["UdpAppender"] = () => new UdpAppender { RemoteAddress = IPAddress.Loopback, RemotePort = 9999, Layout = new SimpleLayout() },
+ ["AnsiColorTerminalAppender"] = () => new AnsiColorTerminalAppender { Layout = new SimpleLayout() },
+ };
+
+ private static Dictionary> Layouts() => new(StringComparer.Ordinal)
+ {
+ ["SimpleLayout"] = () => new SimpleLayout(),
+ ["PatternLayout"] = () => new PatternLayout("%level %logger %message%newline"),
+ ["ExceptionLayout"] = () => new ExceptionLayout(),
+ ["XmlLayout"] = () => new XmlLayout(),
+ };
+
+ private static Dictionary> Filters() => new(StringComparer.Ordinal)
+ {
+ ["LevelRangeFilter"] = () => new LevelRangeFilter { LevelMin = Level.Debug, LevelMax = Level.Fatal },
+ ["LevelMatchFilter"] = () => new LevelMatchFilter { LevelToMatch = Level.Info },
+ ["StringMatchFilter"] = () => new StringMatchFilter { StringToMatch = "probe" },
+ ["PropertyFilter"] = () => new PropertyFilter { Key = "probe", StringToMatch = "value" },
+ ["DenyAllFilter"] = () => new DenyAllFilter(),
+ };
+
+ private static string TempFile(string name) => Path.Combine(Path.GetTempPath(), name);
+
+ private static void Activate(object option) => (option as IOptionHandler)?.ActivateOptions();
+
+ private static string Render(ILayout layout)
+ {
+ Activate(layout);
+ using StringWriter writer = new();
+ layout.Format(writer, new(new()
+ {
+ Level = Level.Info,
+ LoggerName = "probe",
+ Message = "message",
+ TimeStampUtc = DateTime.UtcNow,
+ }));
+ return writer.ToString();
+ }
+
+ private static void Require(bool condition, string message)
+ {
+ if (!condition)
+ {
+ throw new InvalidOperationException(message);
+ }
+ }
+}
diff --git a/src/log4net.Tests.Aot/Program.cs b/src/log4net.Tests.Aot/Program.cs
new file mode 100644
index 000000000..5f484cb4a
--- /dev/null
+++ b/src/log4net.Tests.Aot/Program.cs
@@ -0,0 +1,147 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+using log4net.Util;
+
+namespace log4net.Tests.Aot;
+
+///
+/// Records what log4net can and cannot do when an application is published with Native AOT.
+///
+///
+///
+/// The build runs this assembly twice, JIT compiled and published with PublishAot, so a
+/// difference between the runs is caused by AOT rather than by the platform. The expected
+/// differences are listed in .
+///
+///
+internal static class Program
+{
+ ///
+ /// The environment variable the settings probe expects, set by the build before running.
+ ///
+ internal const string EnvironmentProbeKey = "log4net.AotEnvironmentProbe";
+
+ private static int Main()
+ {
+ // The probes report their own failures, so log4net's internal error reporting is only noise
+ // here, and a passing run that prints errors reads as a broken one.
+ LogLog.EmitInternalMessages = false;
+
+ bool isAot = !IsGetCallingAssemblySupported();
+ string mode = isAot ? "AOT" : "JIT";
+ Dictionary expectedFailures
+ = isAot ? Probes.ExpectedAotFailures : Probes.ExpectedJitFailures;
+
+ Console.WriteLine($"log4net probes, {mode}");
+ Console.WriteLine(new string('-', 100));
+
+ List regressions = [];
+ List unexpectedPasses = [];
+
+ foreach (Probe probe in Probes.All())
+ {
+ string? failure = Run(probe);
+ bool expectedToFail = expectedFailures.ContainsKey(probe.Key);
+
+ if (failure is null)
+ {
+ Console.WriteLine($" {"ok",-6} {probe.Key}");
+ if (expectedToFail)
+ {
+ unexpectedPasses.Add(probe.Key);
+ }
+ }
+ else
+ {
+ Console.WriteLine($" {"FAIL",-6} {probe.Key}");
+ Console.WriteLine($" {failure}");
+ if (expectedToFail)
+ {
+ Console.WriteLine($" expected under {mode}: {expectedFailures[probe.Key]}");
+ }
+ else
+ {
+ regressions.Add($"{probe.Key}: {failure}");
+ }
+ }
+ }
+
+ Console.WriteLine(new string('-', 100));
+ foreach (string regression in regressions)
+ {
+ Console.WriteLine($"REGRESSION {regression}");
+ }
+ foreach (string unexpectedPass in unexpectedPasses)
+ {
+ Console.WriteLine($"NOW WORKING {unexpectedPass} is listed as an expected {mode} failure but passed. "
+ + "Remove it from the list and update the Native AOT page in the manual.");
+ }
+
+ int problems = regressions.Count + unexpectedPasses.Count;
+ Console.WriteLine(problems == 0
+ ? $"{mode}: matches expectations"
+ : $"{mode}: {problems} probe(s) did not match expectations");
+ return problems == 0 ? 0 : 1;
+ }
+
+ ///
+ /// Whether this process is running Native AOT.
+ ///
+ ///
+ ///
+ /// RuntimeFeature.IsDynamicCodeSupported cannot be used: setting
+ /// PublishAot turns that switch off for an ordinary run of the same project too, so it
+ /// reports AOT either way. is only unsupported when the
+ /// application really was compiled ahead of time, which is also the capability log4net keys off.
+ ///
+ ///
+ private static bool IsGetCallingAssemblySupported()
+ {
+ try
+ {
+ _ = Assembly.GetCallingAssembly();
+ return true;
+ }
+ catch (PlatformNotSupportedException)
+ {
+ return false;
+ }
+ }
+
+ private static string? Run(Probe probe)
+ {
+ try
+ {
+ probe.Run();
+ return null;
+ }
+ catch (Exception e) when (e is not (OutOfMemoryException or StackOverflowException))
+ {
+ // A probe reports whatever it failed with, because letting the exception escape would lose
+ // every later probe. The filter mirrors log4net's own Log4NetAssert.IsFatal, which is
+ // internal and not worth linking in for two type checks.
+ return $"{e.GetType().Name}: {e.Message.Split('\n')[0]}";
+ }
+ }
+}
diff --git a/src/log4net.Tests.Aot/log4net.Tests.Aot.csproj b/src/log4net.Tests.Aot/log4net.Tests.Aot.csproj
new file mode 100644
index 000000000..350814f5f
--- /dev/null
+++ b/src/log4net.Tests.Aot/log4net.Tests.Aot.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net10.0
+ log4net.Tests.Aot
+ disable
+ false
+
+ true
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/log4net.Tests/Appender/AppenderSkeletonTest.cs b/src/log4net.Tests/Appender/AppenderSkeletonTest.cs
index ca41386a0..7b967eac7 100644
--- a/src/log4net.Tests/Appender/AppenderSkeletonTest.cs
+++ b/src/log4net.Tests/Appender/AppenderSkeletonTest.cs
@@ -49,7 +49,7 @@ public void AddFilter_FirstFilter_SetsFilterHead()
}
///
- /// Verifies that a second call appends the filter via .
+ /// Verifies that a second call appends the filter via IFilter.Next.
///
[Test]
public void AddFilter_SecondFilter_LinksToChain()
diff --git a/src/log4net.Tests/Core/LoggingEventTest.cs b/src/log4net.Tests/Core/LoggingEventTest.cs
index dfdb33aff..fd11b6083 100644
--- a/src/log4net.Tests/Core/LoggingEventTest.cs
+++ b/src/log4net.Tests/Core/LoggingEventTest.cs
@@ -19,9 +19,11 @@
using System;
using System.Globalization;
-using System.IO;
using System.Reflection;
+#if NET462_OR_GREATER
+using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
+#endif
using log4net.Core;
using log4net.Util;
using NUnit.Framework;
@@ -119,7 +121,7 @@ public void DeserializeV2()
#endif // NET462_OR_GREATER
///
- /// Tests
+ /// Tests LoggingEvent.ReviseThreadName, which is not publicly visible.
///
[Test]
public void ReviseThreadNameTest()
diff --git a/src/log4net.Tests/Core/ShutdownTest.cs b/src/log4net.Tests/Core/ShutdownTest.cs
index 6161e67f5..611247423 100644
--- a/src/log4net.Tests/Core/ShutdownTest.cs
+++ b/src/log4net.Tests/Core/ShutdownTest.cs
@@ -29,7 +29,8 @@
namespace log4net.Tests.Core;
///
-///
+/// Tests shutting log4net down.
+///
[TestFixture]
public class ShutdownTest
{
diff --git a/src/log4net.Tests/Hierarchy/XmlHierarchyConfiguratorTest.cs b/src/log4net.Tests/Hierarchy/XmlHierarchyConfiguratorTest.cs
index 6fdb5d7da..13b31f29d 100644
--- a/src/log4net.Tests/Hierarchy/XmlHierarchyConfiguratorTest.cs
+++ b/src/log4net.Tests/Hierarchy/XmlHierarchyConfiguratorTest.cs
@@ -23,7 +23,6 @@
using NUnit.Framework;
using log4net.Repository.Hierarchy;
-using HierarchyClass = log4net.Repository.Hierarchy.Hierarchy;
namespace log4net.Tests.Hierarchy;
diff --git a/src/log4net.Tests/Util/AotCompatibilityTest.cs b/src/log4net.Tests/Util/AotCompatibilityTest.cs
new file mode 100644
index 000000000..a8c1ef23a
--- /dev/null
+++ b/src/log4net.Tests/Util/AotCompatibilityTest.cs
@@ -0,0 +1,169 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+using log4net.Config;
+using log4net.Core;
+using log4net.Layout;
+using log4net.Util;
+
+using NUnit.Framework;
+
+namespace log4net.Tests.Util;
+
+///
+/// Guards the annotations that keep log4net working when a consumer publishes with
+/// PublishAot or PublishTrimmed.
+///
+///
+///
+/// Dropping one of these annotations breaks nothing in an ordinary build: the failure only shows up
+/// as a inside a consumer's published application. These tests
+/// fail on the spot instead. The attribute is matched by full name, because log4net polyfills it as
+/// an internal type, which is also how the trimmer recognises it.
+///
+///
+[TestFixture]
+public class AotCompatibilityTest
+{
+ private const string DynamicallyAccessedMembers
+ = "System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute";
+
+ ///
+ /// The public entry point a caller passes a repository type to.
+ ///
+ [Test]
+ public void LogManagerCreateRepositoryAnnotatesTheRepositoryType()
+ => AssertAnnotated(Parameter(typeof(LogManager), nameof(LogManager.CreateRepository), typeof(Type)));
+
+ ///
+ /// The step between and the repository selector.
+ ///
+ [Test]
+ public void LoggerManagerCreateRepositoryAnnotatesTheRepositoryType()
+ => AssertAnnotated(Parameter(typeof(LoggerManager), nameof(LoggerManager.CreateRepository),
+ typeof(Assembly), typeof(Type)));
+
+ ///
+ /// The interface, so a custom selector keeps the annotation too.
+ ///
+ [Test]
+ public void RepositorySelectorCreateRepositoryAnnotatesTheRepositoryType()
+ => AssertAnnotated(Parameter(typeof(IRepositorySelector), nameof(IRepositorySelector.CreateRepository),
+ typeof(Assembly), typeof(Type)));
+
+ ///
+ /// Where typeof(Hierarchy) enters, and the default repository is created from.
+ ///
+ [Test]
+ public void DefaultRepositorySelectorAnnotatesItsDefaultRepositoryType()
+ {
+ ConstructorInfo constructor = typeof(DefaultRepositorySelector).GetConstructor([typeof(Type)])
+ ?? throw new InvalidOperationException("DefaultRepositorySelector(Type) no longer exists.");
+ AssertAnnotated(constructor.GetParameters()[0]);
+ }
+
+ ///
+ /// The assembly attribute that lets an application choose its own repository type.
+ ///
+ [Test]
+ public void RepositoryAttributeAnnotatesItsRepositoryType()
+ => AssertAnnotated(typeof(RepositoryAttribute).GetProperty(nameof(RepositoryAttribute.RepositoryType))
+ ?? throw new InvalidOperationException("RepositoryAttribute.RepositoryType no longer exists."));
+
+ ///
+ /// What carries a converter type through the registries, where a bare would lose the annotation.
+ ///
+ [Test]
+ public void ConverterInfoAnnotatesItsType()
+ => AssertAnnotated(typeof(ConverterInfo).GetProperty(nameof(ConverterInfo.Type))
+ ?? throw new InvalidOperationException("ConverterInfo.Type no longer exists."));
+
+ ///
+ /// The built-in converters are only ever created reflectively, so a lost constructor would
+ /// surface only once a consumer trims their application.
+ ///
+ [Test]
+ public void BuiltInPatternLayoutConvertersAreConstructible()
+ => AssertConvertersAreConstructible(typeof(PatternLayout));
+
+ ///
+ /// The same guarantee for the registry.
+ ///
+ [Test]
+ public void BuiltInPatternStringConvertersAreConstructible()
+ => AssertConvertersAreConstructible(typeof(PatternString));
+
+ ///
+ /// Every registered name really produces a converter rather than the error a missing
+ /// constructor would give.
+ ///
+ [Test]
+ public void EveryBuiltInPatternLayoutConverterCanBeCreated()
+ {
+ foreach (string name in GlobalRules(typeof(PatternLayout)).Keys)
+ {
+ PatternLayout layout = new($"%{name}");
+ Assert.DoesNotThrow(layout.ActivateOptions, $"converter [{name}] could not be activated");
+ }
+ }
+
+ private static void AssertConvertersAreConstructible(Type owner)
+ {
+ Dictionary rules = GlobalRules(owner);
+ Assert.That(rules, Is.Not.Empty);
+
+ foreach (KeyValuePair rule in rules)
+ {
+ Type converter = rule.Value.Type
+ ?? throw new InvalidOperationException($"converter [{rule.Key}] has no type");
+ Assert.That(converter.GetConstructor(Type.EmptyTypes), Is.Not.Null,
+ $"converter [{rule.Key}] ({converter.FullName}) has no public parameterless constructor");
+ }
+ }
+
+ private static Dictionary GlobalRules(Type owner)
+ {
+ FieldInfo field = owner.GetField("_sGlobalRulesRegistry", BindingFlags.Static | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException($"{owner.Name}._sGlobalRulesRegistry no longer exists - update this test along with it.");
+ return (Dictionary)field.GetValue(null)!;
+ }
+
+ private static ParameterInfo Parameter(Type owner, string methodName, params Type[] signature)
+ {
+ MethodInfo method = owner.GetMethod(methodName, signature)
+ ?? throw new InvalidOperationException($"{owner.Name}.{methodName} no longer has the expected signature.");
+ return method.GetParameters().Single(p => p.ParameterType == typeof(Type));
+ }
+
+ private static void AssertAnnotated(ParameterInfo parameter)
+ => Assert.That(IsAnnotated(parameter.GetCustomAttributesData()), Is.True,
+ $"parameter [{parameter.Name}] of [{parameter.Member}] lost its {DynamicallyAccessedMembers}");
+
+ private static void AssertAnnotated(MemberInfo member)
+ => Assert.That(IsAnnotated(member.GetCustomAttributesData()), Is.True,
+ $"[{member.DeclaringType?.Name}.{member.Name}] lost its {DynamicallyAccessedMembers}");
+
+ private static bool IsAnnotated(IEnumerable attributes)
+ => attributes.Any(attribute => attribute.AttributeType.FullName == DynamicallyAccessedMembers);
+}
diff --git a/src/log4net.Tests/Util/CallerAssemblyTest.cs b/src/log4net.Tests/Util/CallerAssemblyTest.cs
new file mode 100644
index 000000000..cfc134696
--- /dev/null
+++ b/src/log4net.Tests/Util/CallerAssemblyTest.cs
@@ -0,0 +1,76 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using log4net.Util;
+using NUnit.Framework;
+
+namespace log4net.Tests.Util;
+
+///
+/// Tests for , the guard that keeps the
+/// based overloads usable under Native AOT.
+///
+///
+///
+/// The AOT half of the behaviour cannot be covered here - these tests always run on a JIT
+/// runtime, where is and
+/// is never consulted. What they do cover is that the
+/// guard stays inert on a JIT runtime, so that no call site silently starts attributing
+/// loggers to the entry assembly instead of the caller.
+///
+///
+[TestFixture]
+public class CallerAssemblyTest
+{
+ ///
+ /// The probe recognises a runtime that does implement
+ /// , so the guard stays out of the way everywhere
+ /// except Native AOT. A false negative here would silently move every logger to the entry
+ /// assembly's repository.
+ ///
+ [Test]
+ public void IsSupportedOnAJitRuntime() => Assert.That(CallerAssembly.IsSupported, Is.True);
+
+ ///
+ /// There is always a replacement assembly to attribute a call to, even though the entry
+ /// assembly is in a host without a managed entry point.
+ ///
+ [Test]
+ public void FallbackIsAvailable() => Assert.That(CallerAssembly.Fallback, Is.Not.Null);
+
+ ///
+ /// The guard has to leave in the method whose
+ /// caller is wanted, so a call from this assembly still resolves to this assembly.
+ ///
+ [Test]
+ public void GuardedCallStillReportsTheCallersAssembly()
+ => Assert.That(GuardedCallingAssembly(), Is.SameAs(typeof(CallerAssemblyTest).Assembly));
+
+ ///
+ /// Stands in for a public log4net entry point. Inlining is suppressed because it would
+ /// hand a different frame - the same effect
+ /// that makes the release build of unable to assert on an
+ /// exact assembly.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static Assembly GuardedCallingAssembly()
+ => CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback;
+}
diff --git a/src/log4net.Tests/Util/SystemInfoTest.cs b/src/log4net.Tests/Util/SystemInfoTest.cs
index 9b9067c91..fbf544bdc 100644
--- a/src/log4net.Tests/Util/SystemInfoTest.cs
+++ b/src/log4net.Tests/Util/SystemInfoTest.cs
@@ -23,6 +23,8 @@
using NUnit.Framework;
+using System.Configuration;
+using System.IO;
using System.Linq.Expressions;
using System.Reflection;
@@ -169,6 +171,117 @@ public void EqualsIgnoringCase_DifferentStrings_false()
[Test]
[Platform(Include = "Win,Linux,MacOsX")]
- public void IsAndoid()
+ public void IsAndroid()
=> Assert.That(typeof(SystemInfo).GetProperty("IsAndroid", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null), Is.False);
+
+ ///
+ /// falls back to environment variables once the
+ /// configuration system has failed - which is what happens under Native AOT, where
+ /// System.Configuration is trimmed away.
+ ///
+ ///
+ ///
+ /// That failure cannot be provoked on a JIT runtime, so the latch that records it is flipped
+ /// directly, the same way reaches a non-public member. The environment
+ /// must stay untouched while the configuration system still works, otherwise a malformed
+ /// app.config would silently change where every setting comes from.
+ ///
+ ///
+ [Test]
+ [NonParallelizable]
+ public void GetAppSettingFallsBackToTheEnvironmentOnceConfigurationIsUnavailable()
+ {
+ const string Key = "log4net.Tests.AppSettingFallback";
+ const string Value = "from-the-environment";
+
+ FieldInfo latch = AppSettingsUnavailableLatch();
+ bool originalLatch = (bool)latch.GetValue(null)!;
+ Environment.SetEnvironmentVariable(Key, Value);
+ try
+ {
+ latch.SetValue(null, false);
+ Assert.That(SystemInfo.GetAppSetting(Key), Is.Null);
+
+ latch.SetValue(null, true);
+ Assert.That(SystemInfo.GetAppSetting(Key), Is.EqualTo(Value));
+ }
+ finally
+ {
+ latch.SetValue(null, originalLatch);
+ Environment.SetEnvironmentVariable(Key, null);
+ }
+ }
+
+ ///
+ /// A key that is missing from the environment as well reads as , so the
+ /// fallback leaves callers with the same "no such setting" answer they get from a working
+ /// configuration system.
+ ///
+ [Test]
+ [NonParallelizable]
+ public void GetAppSettingReturnsNullForAnUnsetEnvironmentVariable()
+ {
+ FieldInfo latch = AppSettingsUnavailableLatch();
+ bool originalLatch = (bool)latch.GetValue(null)!;
+ try
+ {
+ latch.SetValue(null, true);
+ Assert.That(SystemInfo.GetAppSetting("log4net.Tests.NoSuchSettingAnywhere"), Is.Null);
+ }
+ finally
+ {
+ latch.SetValue(null, originalLatch);
+ }
+ }
+
+ ///
+ /// A configuration file that does not parse is reported, not routed to the environment - the
+ /// behaviour on every runtime that has a working configuration system is unchanged.
+ ///
+ [Test]
+ public void MalformedConfigurationIsNotTreatedAsAMissingConfigurationSystem()
+ => Assert.That(IsMissingConfigurationSystem(new ConfigurationErrorsException("malformed")), Is.False);
+
+ ///
+ /// Native AOT surfaces a trimmed configuration system as a ,
+ /// the same type a malformed file produces. The two are not told apart by guessing at the inner
+ /// exception: on a runtime that has a configuration system at all, this is reported as an error.
+ ///
+ [Test]
+ public void TrimmedHostExceptionIsNotGuessedAtOnAJitRuntime()
+ => Assert.That(IsMissingConfigurationSystem(
+ new ConfigurationErrorsException("Configuration system failed to initialize",
+ new MissingMethodException("No parameterless constructor defined for type 'System.Configuration.ClientConfigurationHost'."))),
+ Is.False);
+
+ ///
+ /// A deployment without the System.Configuration.ConfigurationManager assembly is recognised by
+ /// the name of the assembly that could not be loaded.
+ ///
+ [Test]
+ public void MissingConfigurationAssemblyIsRecognised()
+ => Assert.That(IsMissingConfigurationSystem(
+ new FileNotFoundException("Could not load file or assembly", "System.Configuration.ConfigurationManager")), Is.True);
+
+ ///
+ /// The application's own missing assembly is its problem, not evidence that the configuration
+ /// system is gone, so it must keep being reported rather than silently redirecting every setting
+ /// to the environment.
+ ///
+ [Test]
+ public void MissingApplicationAssemblyIsNotTreatedAsAMissingConfigurationSystem()
+ => Assert.That(IsMissingConfigurationSystem(
+ new ConfigurationErrorsException("An error occurred creating the configuration section handler",
+ new FileNotFoundException("Could not load file or assembly", "Contoso.SectionHandlers"))), Is.False);
+
+ private static bool IsMissingConfigurationSystem(Exception exception)
+ {
+ MethodInfo method = typeof(SystemInfo).GetMethod("IsMissingConfigurationSystem", BindingFlags.Static | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException("SystemInfo.IsMissingConfigurationSystem no longer exists - update this test along with it.");
+ return (bool)method.Invoke(null, [exception])!;
+ }
+
+ private static FieldInfo AppSettingsUnavailableLatch()
+ => typeof(SystemInfo).GetField("_configurationSystemUnavailable", BindingFlags.Static | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException("SystemInfo._configurationSystemUnavailable no longer exists - update this test along with it.");
}
\ No newline at end of file
diff --git a/src/log4net.Tests/log4net.Tests.csproj b/src/log4net.Tests/log4net.Tests.csproj
index 1ce53b952..38b006386 100644
--- a/src/log4net.Tests/log4net.Tests.csproj
+++ b/src/log4net.Tests/log4net.Tests.csproj
@@ -6,7 +6,7 @@
TestHostNetFramework/testhost.exe, which is not part of the Linux/macOS SDK. Building
them elsewhere only produces assemblies that cannot be run, so skip the target. -->
net10.0
- NETSDK1138;CS1701
+ $(NoWarn);NETSDK1138;CS1701
Library
bin\$(Configuration)
Debug;Release
@@ -15,10 +15,11 @@
false
TRACE;DEBUG;$(DefineConstants)
- CS8032
+ $(NoWarn);CS8032
quackers
+
diff --git a/src/log4net.sln b/src/log4net.sln
index f0c530615..95a848902 100644
--- a/src/log4net.sln
+++ b/src/log4net.sln
@@ -60,6 +60,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "log4net.Ext.Mail", "log4net
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "log4net.Ext.Mail.Tests", "log4net.Ext.Mail.Tests\log4net.Ext.Mail.Tests.csproj", "{56A9114E-30BC-0ED0-843E-97613E9E6221}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "log4net.Tests.Aot", "log4net.Tests.Aot\log4net.Tests.Aot.csproj", "{A89A800C-DBC6-4C5D-AD92-0D68B8C47ECD}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -106,6 +108,10 @@ Global
{56A9114E-30BC-0ED0-843E-97613E9E6221}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56A9114E-30BC-0ED0-843E-97613E9E6221}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56A9114E-30BC-0ED0-843E-97613E9E6221}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A89A800C-DBC6-4C5D-AD92-0D68B8C47ECD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A89A800C-DBC6-4C5D-AD92-0D68B8C47ECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A89A800C-DBC6-4C5D-AD92-0D68B8C47ECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A89A800C-DBC6-4C5D-AD92-0D68B8C47ECD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/log4net/Appender/EventLogAppender.cs b/src/log4net/Appender/EventLogAppender.cs
index a8e66c04e..48844cd35 100644
--- a/src/log4net/Appender/EventLogAppender.cs
+++ b/src/log4net/Appender/EventLogAppender.cs
@@ -23,7 +23,6 @@
using System.Diagnostics;
using log4net.Util;
-using log4net.Layout;
using log4net.Core;
namespace log4net.Appender;
diff --git a/src/log4net/Appender/FileAppender.cs b/src/log4net/Appender/FileAppender.cs
index 5b444afd2..993e2974e 100644
--- a/src/log4net/Appender/FileAppender.cs
+++ b/src/log4net/Appender/FileAppender.cs
@@ -19,6 +19,7 @@
#endregion
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
@@ -838,14 +839,21 @@ public override void OnClose()
///
/// Default locking model (when no locking model was configured)
///
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
private static Type _defaultLockingModelType = typeof(ExclusiveLock);
///
/// Specify default locking model
///
/// Type of LockingModel
- public static void SetDefaultLockingModelType()
- where TLockingModel : LockingModelBase
+ ///
+ ///
+ /// The locking model is created with , so the
+ /// new() constraint is what keeps its constructor alive in a trimmed or Native AOT build.
+ ///
+ ///
+ public static void SetDefaultLockingModelType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TLockingModel>()
+ where TLockingModel : LockingModelBase, new()
=> _defaultLockingModelType = typeof(TLockingModel);
///
diff --git a/src/log4net/Config/BasicConfigurator.cs b/src/log4net/Config/BasicConfigurator.cs
index ea7dfa684..2af06e672 100644
--- a/src/log4net/Config/BasicConfigurator.cs
+++ b/src/log4net/Config/BasicConfigurator.cs
@@ -73,7 +73,8 @@ public static class BasicConfigurator
/// layout style.
///
///
- public static ICollection Configure() => Configure(LogManager.GetRepository(Assembly.GetCallingAssembly()));
+ public static ICollection Configure()
+ => Configure(LogManager.GetRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback));
///
/// Initializes the log4net system using the specified appenders.
@@ -88,7 +89,7 @@ public static ICollection Configure(params IAppender[] appenders)
{
List configurationMessages = new();
- ILoggerRepository repository = LogManager.GetRepository(Assembly.GetCallingAssembly());
+ ILoggerRepository repository = LogManager.GetRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback);
using (new LogLog.LogReceivedAdapter(configurationMessages))
{
diff --git a/src/log4net/Config/RepositoryAttribute.cs b/src/log4net/Config/RepositoryAttribute.cs
index fd5c6308e..5cba2d6ec 100644
--- a/src/log4net/Config/RepositoryAttribute.cs
+++ b/src/log4net/Config/RepositoryAttribute.cs
@@ -18,6 +18,7 @@
#endregion
using System;
+using System.Diagnostics.CodeAnalysis;
namespace log4net.Config;
@@ -104,5 +105,6 @@ public RepositoryAttribute()
/// repository.
///
///
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
public Type? RepositoryType { get; set; }
}
\ No newline at end of file
diff --git a/src/log4net/Config/XmlConfigurator.cs b/src/log4net/Config/XmlConfigurator.cs
index fa060cb1d..e7ffd2db5 100644
--- a/src/log4net/Config/XmlConfigurator.cs
+++ b/src/log4net/Config/XmlConfigurator.cs
@@ -140,7 +140,7 @@ private static void InternalConfigure(ILoggerRepository repository, Func
///
public static ICollection Configure()
- => Configure(LogManager.GetRepository(Assembly.GetCallingAssembly()));
+ => Configure(LogManager.GetRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback));
///
/// Configures log4net using a log4net element
@@ -156,7 +156,7 @@ public static ICollection Configure(XmlElement element)
{
List configurationMessages = [];
- ILoggerRepository repository = LogManager.GetRepository(Assembly.GetCallingAssembly());
+ ILoggerRepository repository = LogManager.GetRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback);
using (new LogLog.LogReceivedAdapter(configurationMessages))
{
@@ -222,9 +222,11 @@ public static ICollection Configure(FileInfo configFile)
{
List configurationMessages = [];
+ Assembly repositoryAssembly = CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback;
+
using (new LogLog.LogReceivedAdapter(configurationMessages))
{
- InternalConfigure(LogManager.GetRepository(Assembly.GetCallingAssembly()), configFile);
+ InternalConfigure(LogManager.GetRepository(repositoryAssembly), configFile);
}
return configurationMessages;
@@ -248,7 +250,7 @@ public static ICollection Configure(Uri configUri)
{
List configurationMessages = [];
- ILoggerRepository repository = LogManager.GetRepository(Assembly.GetCallingAssembly());
+ ILoggerRepository repository = LogManager.GetRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback);
using (new LogLog.LogReceivedAdapter(configurationMessages))
{
InternalConfigure(repository, configUri);
@@ -277,7 +279,7 @@ public static ICollection Configure(Stream configStream)
{
List configurationMessages = [];
- ILoggerRepository repository = LogManager.GetRepository(Assembly.GetCallingAssembly());
+ ILoggerRepository repository = LogManager.GetRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback);
using (new LogLog.LogReceivedAdapter(configurationMessages))
{
InternalConfigure(repository, configStream);
@@ -644,7 +646,7 @@ public static ICollection ConfigureAndWatch(FileInfo configFile)
{
List configurationMessages = [];
- ILoggerRepository repository = LogManager.GetRepository(Assembly.GetCallingAssembly());
+ ILoggerRepository repository = LogManager.GetRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback);
using (new LogLog.LogReceivedAdapter(configurationMessages))
{
diff --git a/src/log4net/Core/DefaultRepositorySelector.cs b/src/log4net/Core/DefaultRepositorySelector.cs
index a14d6c820..406f11529 100644
--- a/src/log4net/Core/DefaultRepositorySelector.cs
+++ b/src/log4net/Core/DefaultRepositorySelector.cs
@@ -21,6 +21,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using log4net.Config;
@@ -71,12 +72,13 @@ public class DefaultRepositorySelector : IRepositorySelector
///
/// is .
/// does not implement .
- public DefaultRepositorySelector(Type defaultRepositoryType)
+ public DefaultRepositorySelector([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type defaultRepositoryType)
{
// Check that the type is a repository
if (!typeof(ILoggerRepository).IsAssignableFrom(defaultRepositoryType.EnsureNotNull()))
{
- throw SystemInfo.CreateArgumentOutOfRangeException("defaultRepositoryType", defaultRepositoryType, $"Parameter: defaultRepositoryType, Value: [{defaultRepositoryType}] out of range. Argument must implement the ILoggerRepository interface");
+ throw SystemInfo.CreateArgumentOutOfRangeException("defaultRepositoryType", defaultRepositoryType,
+ $"Parameter: defaultRepositoryType, Value: [{defaultRepositoryType}] out of range. Argument must implement the ILoggerRepository interface");
}
this._defaultRepositoryType = defaultRepositoryType;
@@ -175,7 +177,8 @@ public ILoggerRepository GetRepository(string repositoryName)
///
///
/// is .
- public ILoggerRepository CreateRepository(Assembly assembly, Type repositoryType)
+ public ILoggerRepository CreateRepository(Assembly assembly,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type repositoryType)
=> CreateRepository(assembly, repositoryType, DefaultRepositoryName, true);
///
@@ -216,7 +219,9 @@ public ILoggerRepository CreateRepository(Assembly assembly, Type repositoryType
///
///
/// is .
- public ILoggerRepository CreateRepository(Assembly repositoryAssembly, Type? repositoryType, string repositoryName, bool readAssemblyAttributes)
+ public ILoggerRepository CreateRepository(Assembly repositoryAssembly,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? repositoryType,
+ string repositoryName, bool readAssemblyAttributes)
{
repositoryAssembly.EnsureNotNull();
@@ -305,7 +310,8 @@ public ILoggerRepository CreateRepository(Assembly repositoryAssembly, Type? rep
///
/// is .
/// already exists.
- public ILoggerRepository CreateRepository(string repositoryName, Type? repositoryType)
+ public ILoggerRepository CreateRepository(string repositoryName,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? repositoryType)
{
repositoryName.EnsureNotNull();
@@ -479,7 +485,8 @@ protected virtual void OnLoggerRepositoryCreatedEvent(ILoggerRepository reposito
/// in/out param to hold the repository name to use for the assembly, caller should set this to the default value before calling.
/// in/out param to hold the type of the repository to create for the assembly, caller should set this to the default value before calling.
/// is .
- private void GetInfoForAssembly(Assembly assembly, ref string repositoryName, ref Type repositoryType)
+ private void GetInfoForAssembly(Assembly assembly, ref string repositoryName,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] ref Type repositoryType)
{
assembly.EnsureNotNull();
@@ -735,5 +742,7 @@ private void LoadAliases(Assembly assembly, ILoggerRepository repository)
private readonly Dictionary _name2Repository = new(StringComparer.Ordinal);
private readonly Dictionary _assembly2Repository = [];
private readonly Dictionary _alias2Repository = new(StringComparer.Ordinal);
+
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
private readonly Type _defaultRepositoryType;
}
\ No newline at end of file
diff --git a/src/log4net/Core/IRepositorySelector.cs b/src/log4net/Core/IRepositorySelector.cs
index 2ac3d52d8..30b85186d 100644
--- a/src/log4net/Core/IRepositorySelector.cs
+++ b/src/log4net/Core/IRepositorySelector.cs
@@ -18,6 +18,7 @@
#endregion
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using log4net.Repository;
@@ -127,7 +128,8 @@ public interface IRepositorySelector
/// this association.
///
///
- ILoggerRepository CreateRepository(Assembly assembly, Type repositoryType);
+ ILoggerRepository CreateRepository(Assembly assembly,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type repositoryType);
///
/// Creates a new repository with the name specified.
@@ -142,7 +144,8 @@ public interface IRepositorySelector
/// same name will return the same repository instance.
///
///
- ILoggerRepository CreateRepository(string repositoryName, Type? repositoryType);
+ ILoggerRepository CreateRepository(string repositoryName,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? repositoryType);
///
/// Test if a named repository exists
diff --git a/src/log4net/Core/LoggerManager.cs b/src/log4net/Core/LoggerManager.cs
index 3ef057959..29176c800 100644
--- a/src/log4net/Core/LoggerManager.cs
+++ b/src/log4net/Core/LoggerManager.cs
@@ -18,6 +18,7 @@
#endregion
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;
using log4net.Util;
@@ -466,7 +467,8 @@ public static ILoggerRepository CreateRepository(string repository)
///
///
/// The specified repository already exists.
- public static ILoggerRepository CreateRepository(string repository, Type repositoryType)
+ public static ILoggerRepository CreateRepository(string repository,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type repositoryType)
=> RepositorySelector.CreateRepository(repository.EnsureNotNull(), repositoryType.EnsureNotNull());
///
@@ -484,7 +486,8 @@ public static ILoggerRepository CreateRepository(string repository, Type reposit
/// same assembly specified will return the same repository instance.
///
///
- public static ILoggerRepository CreateRepository(Assembly repositoryAssembly, Type repositoryType)
+ public static ILoggerRepository CreateRepository(Assembly repositoryAssembly,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type repositoryType)
=> RepositorySelector.CreateRepository(repositoryAssembly.EnsureNotNull(), repositoryType.EnsureNotNull());
///
diff --git a/src/log4net/Core/LoggingEvent.cs b/src/log4net/Core/LoggingEvent.cs
index 42aae5696..016473fa2 100644
--- a/src/log4net/Core/LoggingEvent.cs
+++ b/src/log4net/Core/LoggingEvent.cs
@@ -22,7 +22,9 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+#if NET471_OR_GREATER || NETSTANDARD2_0_OR_GREATER
using System.Runtime.InteropServices;
+#endif
using System.Runtime.Serialization;
using System.Security;
using System.Security.Principal;
diff --git a/src/log4net/Diagnostics/CodeAnalysis/DynamicallyAccessedMemberTypes.cs b/src/log4net/Diagnostics/CodeAnalysis/DynamicallyAccessedMemberTypes.cs
new file mode 100644
index 000000000..54cda914c
--- /dev/null
+++ b/src/log4net/Diagnostics/CodeAnalysis/DynamicallyAccessedMemberTypes.cs
@@ -0,0 +1,53 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+// inspired by https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/DynamicallyAccessedMemberTypes.cs
+
+#if !NET6_0_OR_GREATER
+namespace System.Diagnostics.CodeAnalysis;
+
+///
+/// Specifies the types of members that are dynamically accessed.
+///
+///
+///
+/// The trimmer matches this type by its full name rather than by identity, so the values have to
+/// keep the numbering the framework uses. Only the members log4net annotates with are declared;
+/// add further ones from the runtime source above as they are needed.
+///
+///
+[Flags]
+internal enum DynamicallyAccessedMemberTypes
+{
+ ///
+ /// Specifies no members.
+ ///
+ None = 0,
+
+ ///
+ /// Specifies the default, parameterless public constructor.
+ ///
+ PublicParameterlessConstructor = 0x0001,
+
+ ///
+ /// Specifies all public constructors.
+ ///
+ PublicConstructors = 0x0002 | PublicParameterlessConstructor,
+}
+#endif
diff --git a/src/log4net/Diagnostics/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs b/src/log4net/Diagnostics/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs
new file mode 100644
index 000000000..421f56ca7
--- /dev/null
+++ b/src/log4net/Diagnostics/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs
@@ -0,0 +1,49 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+// inspired by https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs
+
+#if !NET6_0_OR_GREATER
+namespace System.Diagnostics.CodeAnalysis;
+
+///
+/// States which members of a are accessed dynamically, so that a trimmer keeps
+/// them instead of removing them as unused.
+///
+///
+///
+/// Neither net462 nor netstandard2.0 declares this attribute, but the trimmer
+/// recognizes it by full name, so a library can supply its own and still be understood - which is
+/// what lets log4net keep working when a consumer publishes with PublishAot or
+/// PublishTrimmed.
+///
+///
+/// The members that are dynamically accessed.
+[AttributeUsage(
+ AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter
+ | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method,
+ Inherited = false)]
+internal sealed class DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) : Attribute
+{
+ ///
+ /// Gets the members that are dynamically accessed.
+ ///
+ public DynamicallyAccessedMemberTypes MemberTypes { get; } = memberTypes;
+}
+#endif
diff --git a/src/log4net/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs b/src/log4net/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs
new file mode 100644
index 000000000..da664ea05
--- /dev/null
+++ b/src/log4net/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs
@@ -0,0 +1,71 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+// inspired by https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs
+
+#if !NET6_0_OR_GREATER
+namespace System.Diagnostics.CodeAnalysis;
+
+///
+/// Suppresses a trimming or single file warning, unconditionally: unlike
+/// it is kept in the
+/// assembly, so the trimmer can still see it.
+///
+///
+///
+/// Neither net462 nor netstandard2.0 declares this attribute. The trimmer recognizes
+/// it by full name, so log4net supplies its own.
+///
+///
+/// the category of the suppressed warning
+/// the identifier of the suppressed warning
+[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
+internal sealed class UnconditionalSuppressMessageAttribute(string category, string checkId) : Attribute
+{
+ ///
+ /// Gets the category of the suppressed warning.
+ ///
+ public string Category { get; } = category;
+
+ ///
+ /// Gets the identifier of the suppressed warning.
+ ///
+ public string CheckId { get; } = checkId;
+
+ ///
+ /// Gets or sets the scope of the suppression.
+ ///
+ public string? Scope { get; set; }
+
+ ///
+ /// Gets or sets the fully qualified name of the suppression target.
+ ///
+ public string? Target { get; set; }
+
+ ///
+ /// Gets or sets why the warning is suppressed.
+ ///
+ public string? Justification { get; set; }
+
+ ///
+ /// Gets or sets an optional argument expanding on the suppression.
+ ///
+ public string? MessageId { get; set; }
+}
+#endif
diff --git a/src/log4net/Filter/LevelMatchFilter.cs b/src/log4net/Filter/LevelMatchFilter.cs
index 80b3eb0bc..49d40ade0 100644
--- a/src/log4net/Filter/LevelMatchFilter.cs
+++ b/src/log4net/Filter/LevelMatchFilter.cs
@@ -17,8 +17,6 @@
//
#endregion
-using System;
-
using log4net.Core;
using log4net.Util;
diff --git a/src/log4net/Filter/LevelRangeFilter.cs b/src/log4net/Filter/LevelRangeFilter.cs
index dc3104a37..bfff986c9 100644
--- a/src/log4net/Filter/LevelRangeFilter.cs
+++ b/src/log4net/Filter/LevelRangeFilter.cs
@@ -17,8 +17,6 @@
//
#endregion
-using System;
-
using log4net.Core;
using log4net.Util;
diff --git a/src/log4net/Filter/PropertyFilter.cs b/src/log4net/Filter/PropertyFilter.cs
index c872b503e..cef49c75f 100644
--- a/src/log4net/Filter/PropertyFilter.cs
+++ b/src/log4net/Filter/PropertyFilter.cs
@@ -17,8 +17,6 @@
//
#endregion
-using System;
-
using log4net.Core;
using log4net.Util;
diff --git a/src/log4net/Filter/StringMatchFilter.cs b/src/log4net/Filter/StringMatchFilter.cs
index 8458fc7ec..e7819913b 100644
--- a/src/log4net/Filter/StringMatchFilter.cs
+++ b/src/log4net/Filter/StringMatchFilter.cs
@@ -17,7 +17,6 @@
//
#endregion
-using System;
using System.Text.RegularExpressions;
using log4net.Core;
diff --git a/src/log4net/Layout/ExceptionLayout.cs b/src/log4net/Layout/ExceptionLayout.cs
index af0f40196..0121fe7c8 100644
--- a/src/log4net/Layout/ExceptionLayout.cs
+++ b/src/log4net/Layout/ExceptionLayout.cs
@@ -17,7 +17,6 @@
//
#endregion
-using System;
using System.IO;
using log4net.Core;
diff --git a/src/log4net/Layout/PatternLayout.cs b/src/log4net/Layout/PatternLayout.cs
index 29e64746a..1fee419a3 100644
--- a/src/log4net/Layout/PatternLayout.cs
+++ b/src/log4net/Layout/PatternLayout.cs
@@ -18,6 +18,7 @@
#endregion
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.IO;
@@ -820,84 +821,78 @@ public class PatternLayout : LayoutSkeleton
/// This static map is overridden by the converterRegistry instance map
///
///
- private static readonly Dictionary _sGlobalRulesRegistry = new(StringComparer.Ordinal)
+ private static readonly Dictionary _sGlobalRulesRegistry = CreateGlobalRulesRegistry();
+
+ ///
+ /// Builds the registry of built-in pattern converters.
+ ///
+ /// the built-in rules, keyed by the name used in a conversion pattern
+ ///
+ ///
+ /// The registry holds rather than a bare because a
+ /// put into a collection loses any
+ /// annotation,
+ /// which is what left these converters without a constructor once a Native AOT build had trimmed
+ /// them. carries the annotation, so it survives the round trip.
+ ///
+ ///
+ /// The new() constraint states the same requirement a second time, structurally: a
+ /// converter that loses its public parameterless constructor becomes a compile error here rather
+ /// than a run-time failure that only shows up in a trimmed build.
+ ///
+ ///
+ private static Dictionary CreateGlobalRulesRegistry()
{
- ["literal"] = typeof(LiteralPatternConverter),
- ["newline"] = typeof(NewLinePatternConverter),
- ["n"] = typeof(NewLinePatternConverter),
+ Dictionary rules = new(StringComparer.Ordinal);
+
+ Add("literal");
+ Add("newline", "n");
// .NET Standard has no support for ASP.NET
#if NET462_OR_GREATER
- ["aspnet-cache"] = typeof(AspNetCachePatternConverter),
- ["aspnet-context"] = typeof(AspNetContextPatternConverter),
- ["aspnet-request"] = typeof(AspNetRequestPatternConverter),
- ["aspnet-session"] = typeof(AspNetSessionPatternConverter),
+ Add("aspnet-cache");
+ Add("aspnet-context");
+ Add("aspnet-request");
+ Add("aspnet-session");
#endif
- ["c"] = typeof(LoggerPatternConverter),
- ["logger"] = typeof(LoggerPatternConverter),
-
- ["C"] = typeof(TypeNamePatternConverter),
- ["class"] = typeof(TypeNamePatternConverter),
- ["type"] = typeof(TypeNamePatternConverter),
-
- ["d"] = typeof(DatePatternConverter),
- ["date"] = typeof(DatePatternConverter),
-
- ["exception"] = typeof(ExceptionPatternConverter),
-
- ["F"] = typeof(FileLocationPatternConverter),
- ["file"] = typeof(FileLocationPatternConverter),
-
- ["l"] = typeof(FullLocationPatternConverter),
- ["location"] = typeof(FullLocationPatternConverter),
-
- ["L"] = typeof(LineLocationPatternConverter),
- ["line"] = typeof(LineLocationPatternConverter),
-
- ["m"] = typeof(MessagePatternConverter),
- ["message"] = typeof(MessagePatternConverter),
-
- ["M"] = typeof(MethodLocationPatternConverter),
- ["method"] = typeof(MethodLocationPatternConverter),
-
- ["p"] = typeof(LevelPatternConverter),
- ["level"] = typeof(LevelPatternConverter),
-
- ["P"] = typeof(PropertyPatternConverter),
- ["property"] = typeof(PropertyPatternConverter),
- ["properties"] = typeof(PropertyPatternConverter),
-
- ["r"] = typeof(RelativeTimePatternConverter),
- ["timestamp"] = typeof(RelativeTimePatternConverter),
-
- ["stacktrace"] = typeof(StackTracePatternConverter),
- ["stacktracedetail"] = typeof(StackTraceDetailPatternConverter),
-
- ["t"] = typeof(ThreadPatternConverter),
- ["thread"] = typeof(ThreadPatternConverter),
+ Add("c", "logger");
+ Add("C", "class", "type");
+ Add("d", "date");
+ Add("exception");
+ Add("F", "file");
+ Add("l", "location");
+ Add("L", "line");
+ Add("m", "message");
+ Add("M", "method");
+ Add("p", "level");
+ Add("r", "timestamp");
+ Add("stacktrace");
+ Add("stacktracedetail");
+ Add("t", "thread");
// For backwards compatibility the NDC patterns
- ["x"] = typeof(NdcPatternConverter),
- ["ndc"] = typeof(NdcPatternConverter),
+ Add("x", "ndc");
// For backwards compatibility the MDC patterns just do a property lookup
- ["X"] = typeof(PropertyPatternConverter),
- ["mdc"] = typeof(PropertyPatternConverter),
-
- ["a"] = typeof(AppDomainPatternConverter),
- ["appdomain"] = typeof(AppDomainPatternConverter),
+ Add("P", "property", "properties", "X", "mdc");
- ["u"] = typeof(IdentityPatternConverter),
- ["identity"] = typeof(IdentityPatternConverter),
+ Add("a", "appdomain");
+ Add("u", "identity");
+ Add("utcdate", "utcDate", "UtcDate");
+ Add("w", "username");
- ["utcdate"] = typeof(UtcDatePatternConverter),
- ["utcDate"] = typeof(UtcDatePatternConverter),
- ["UtcDate"] = typeof(UtcDatePatternConverter),
+ return rules;
- ["w"] = typeof(UserNamePatternConverter),
- ["username"] = typeof(UserNamePatternConverter),
- };
+ void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(
+ params string[] names) where T : PatternConverter, new()
+ {
+ foreach (string name in names)
+ {
+ rules[name] = new() { Name = name, Type = typeof(T) };
+ }
+ }
+ }
///
/// the head of the pattern converter chain
@@ -982,15 +977,17 @@ protected virtual PatternParser CreatePatternParser(string pattern)
{
PatternParser patternParser = new(pattern);
- // Add all the builtin patterns
- foreach (KeyValuePair entry in _sGlobalRulesRegistry)
+ // Add all the builtin patterns. The registry entries are copied rather than handed out:
+ // PatternConverters is public, and PatternParser assigns ConverterInfo.Properties to every
+ // converter it creates, so sharing one instance would let a single layout mutate state that
+ // every other layout in the process can see.
+ foreach (KeyValuePair entry in _sGlobalRulesRegistry)
{
- ConverterInfo converterInfo = new()
+ patternParser.PatternConverters[entry.Key] = new ConverterInfo
{
- Name = entry.Key,
- Type = entry.Value
+ Name = entry.Value.Name,
+ Type = entry.Value.Type
};
- patternParser.PatternConverters[entry.Key] = converterInfo;
}
// Add the instance patterns
foreach (KeyValuePair entry in _instanceRulesRegistry)
@@ -1093,7 +1090,8 @@ public void AddConverter(ConverterInfo converterInfo)
/// type.
///
///
- public void AddConverter(string name, Type type) => AddConverter(new()
+ public void AddConverter(string name,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type) => AddConverter(new()
{
Name = name.EnsureNotNull(),
Type = type.EnsureNotNull()
diff --git a/src/log4net/Layout/SimpleLayout.cs b/src/log4net/Layout/SimpleLayout.cs
index c25e3a9c4..8722046e8 100644
--- a/src/log4net/Layout/SimpleLayout.cs
+++ b/src/log4net/Layout/SimpleLayout.cs
@@ -17,7 +17,6 @@
//
#endregion
-using System;
using System.IO;
using log4net.Core;
diff --git a/src/log4net/Layout/XmlLayoutBase.cs b/src/log4net/Layout/XmlLayoutBase.cs
index 3c63b5ae9..c51e42754 100644
--- a/src/log4net/Layout/XmlLayoutBase.cs
+++ b/src/log4net/Layout/XmlLayoutBase.cs
@@ -17,7 +17,6 @@
//
#endregion
-using System;
using System.IO;
using System.Xml;
diff --git a/src/log4net/LogManager.cs b/src/log4net/LogManager.cs
index cc255a540..36e958b86 100644
--- a/src/log4net/LogManager.cs
+++ b/src/log4net/LogManager.cs
@@ -71,7 +71,8 @@ public static class LogManager
///
/// The fully qualified logger name to look for.
/// The logger found, or if no logger could be found.
- public static ILog? Exists(string name) => Exists(Assembly.GetCallingAssembly(), name);
+ public static ILog? Exists(string name)
+ => Exists(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback, name);
/// Get the currently defined loggers.
///
@@ -81,7 +82,8 @@ public static class LogManager
/// The root logger is not included in the returned array.
///
/// All the defined loggers.
- public static ILog[] GetCurrentLoggers() => GetCurrentLoggers(Assembly.GetCallingAssembly());
+ public static ILog[] GetCurrentLoggers()
+ => GetCurrentLoggers(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback);
/// Get or create a logger.
///
@@ -101,7 +103,8 @@ public static class LogManager
///
/// The name of the logger to retrieve.
/// The logger with the name specified.
- public static ILog GetLogger(string name) => GetLogger(Assembly.GetCallingAssembly(), name);
+ public static ILog GetLogger(string name)
+ => GetLogger(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback, name);
///
/// Returns the named logger if it exists.
@@ -119,7 +122,8 @@ public static class LogManager
/// The logger found, or if the logger doesn't exist in the specified
/// repository.
///
- public static ILog? Exists(string repository, string name) => WrapLogger(LoggerManager.Exists(repository, name));
+ public static ILog? Exists(string repository, string name)
+ => WrapLogger(LoggerManager.Exists(repository, name));
///
/// Returns the named logger if it exists.
@@ -137,7 +141,8 @@ public static class LogManager
/// The logger, or if the logger doesn't exist in the specified
/// assembly's repository.
///
- public static ILog? Exists(Assembly repositoryAssembly, string name) => WrapLogger(LoggerManager.Exists(repositoryAssembly, name));
+ public static ILog? Exists(Assembly repositoryAssembly, string name)
+ => WrapLogger(LoggerManager.Exists(repositoryAssembly, name));
///
/// Returns all the currently defined loggers in the specified repository.
@@ -147,7 +152,8 @@ public static class LogManager
/// The root logger is not included in the returned array.
///
/// All the defined loggers.
- public static ILog[] GetCurrentLoggers(string repository) => WrapLoggers(LoggerManager.GetCurrentLoggers(repository));
+ public static ILog[] GetCurrentLoggers(string repository)
+ => WrapLoggers(LoggerManager.GetCurrentLoggers(repository));
///
/// Returns all the currently defined loggers in the specified assembly's repository.
@@ -157,7 +163,8 @@ public static class LogManager
/// The root logger is not included in the returned array.
///
/// All the defined loggers.
- public static ILog[] GetCurrentLoggers(Assembly repositoryAssembly) => WrapLoggers(LoggerManager.GetCurrentLoggers(repositoryAssembly));
+ public static ILog[] GetCurrentLoggers(Assembly repositoryAssembly)
+ => WrapLoggers(LoggerManager.GetCurrentLoggers(repositoryAssembly));
///
/// Retrieves or creates a named logger.
@@ -178,7 +185,8 @@ public static class LogManager
/// The repository to lookup in.
/// The name of the logger to retrieve.
/// The logger with the name specified.
- public static ILog GetLogger(string repository, string name) => WrapLogger(LoggerManager.GetLogger(repository, name))!;
+ public static ILog GetLogger(string repository, string name)
+ => WrapLogger(LoggerManager.GetLogger(repository, name))!;
///
/// Retrieves or creates a named logger.
@@ -211,7 +219,10 @@ public static ILog GetLogger(Assembly repositoryAssembly, string name)
/// The full name of will be used as the name of the logger to retrieve.
/// The logger with the name specified.
public static ILog GetLogger(Type type)
- => GetLogger(Assembly.GetCallingAssembly(), type.EnsureNotNull().FullName!);
+ => GetLogger(CallerAssembly.IsSupported
+ ? Assembly.GetCallingAssembly()
+ : CallerAssembly.Fallback,
+ type.EnsureNotNull().FullName!);
///
/// Shorthand for .
@@ -277,7 +288,8 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
/// and again to a nested appender.
///
///
- public static void ShutdownRepository() => ShutdownRepository(Assembly.GetCallingAssembly());
+ public static void ShutdownRepository()
+ => ShutdownRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback);
///
/// Shuts down the repository for the repository specified.
@@ -299,7 +311,8 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
///
///
/// The repository to shut down.
- public static void ShutdownRepository(string repository) => LoggerManager.ShutdownRepository(repository);
+ public static void ShutdownRepository(string repository)
+ => LoggerManager.ShutdownRepository(repository);
///
/// Shuts down the repository specified.
@@ -323,7 +336,8 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
///
///
/// The assembly to use to look up the repository.
- public static void ShutdownRepository(Assembly repositoryAssembly) => LoggerManager.ShutdownRepository(repositoryAssembly);
+ public static void ShutdownRepository(Assembly repositoryAssembly)
+ => LoggerManager.ShutdownRepository(repositoryAssembly);
/// Reset the configuration of a repository
///
@@ -339,7 +353,8 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
/// message disabling is set to its default "off" value.
///
///
- public static void ResetConfiguration() => ResetConfiguration(Assembly.GetCallingAssembly());
+ public static void ResetConfiguration()
+ => ResetConfiguration(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback);
///
/// Resets all values contained in this repository instance to their defaults.
@@ -371,7 +386,8 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
///
///
/// The assembly to use to look up the repository to reset.
- public static void ResetConfiguration(Assembly repositoryAssembly) => LoggerManager.ResetConfiguration(repositoryAssembly);
+ public static void ResetConfiguration(Assembly repositoryAssembly)
+ => LoggerManager.ResetConfiguration(repositoryAssembly);
/// Get a logger repository.
///
@@ -384,7 +400,8 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
///
///
/// The instance for the default repository.
- public static ILoggerRepository GetRepository() => GetRepository(Assembly.GetCallingAssembly());
+ public static ILoggerRepository GetRepository()
+ => GetRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback);
///
/// Returns the default instance.
@@ -410,7 +427,8 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
///
///
/// The assembly to use to look up the repository.
- public static ILoggerRepository GetRepository(Assembly repositoryAssembly) => LoggerManager.GetRepository(repositoryAssembly);
+ public static ILoggerRepository GetRepository(Assembly repositoryAssembly)
+ => LoggerManager.GetRepository(repositoryAssembly);
/// Create a logger repository.
///
@@ -427,7 +445,9 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
/// the same repository instance.
///
///
- public static ILoggerRepository CreateRepository(Type repositoryType) => CreateRepository(Assembly.GetCallingAssembly(), repositoryType);
+ public static ILoggerRepository CreateRepository(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type repositoryType)
+ => CreateRepository(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback, repositoryType);
///
/// Creates a repository with the specified name.
@@ -445,7 +465,8 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
/// The name of the repository, this must be unique amongst repositories.
/// The created for the repository.
/// The specified repository already exists.
- public static ILoggerRepository CreateRepository(string repository) => LoggerManager.CreateRepository(repository);
+ public static ILoggerRepository CreateRepository(string repository)
+ => LoggerManager.CreateRepository(repository);
///
/// Creates a repository with the specified name and repository type.
@@ -462,7 +483,9 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
/// as the for the repository specified.
/// The created for the repository.
/// The specified repository already exists.
- public static ILoggerRepository CreateRepository(string repository, Type repositoryType) => LoggerManager.CreateRepository(repository, repositoryType);
+ public static ILoggerRepository CreateRepository(string repository,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type repositoryType)
+ => LoggerManager.CreateRepository(repository, repositoryType);
///
/// Creates a repository for the specified assembly and repository type.
@@ -479,7 +502,9 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
/// and has a no arg constructor. An instance of this type will be created to act
/// as the for the repository specified.
/// The created for the repository.
- public static ILoggerRepository CreateRepository(Assembly repositoryAssembly, Type repositoryType) => LoggerManager.CreateRepository(repositoryAssembly, repositoryType);
+ public static ILoggerRepository CreateRepository(Assembly repositoryAssembly,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type repositoryType)
+ => LoggerManager.CreateRepository(repositoryAssembly, repositoryType);
///
/// Gets the list of currently defined repositories.
@@ -499,7 +524,8 @@ public static ILog GetLogger(Assembly repositoryAssembly, Type type)
/// if all logging events were flushed successfully, else .
public static bool Flush(int millisecondsTimeout)
{
- if (LoggerManager.GetRepository(Assembly.GetCallingAssembly()) is not IFlushable flushableRepository)
+ Assembly callerAssembly = CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() : CallerAssembly.Fallback;
+ if (LoggerManager.GetRepository(callerAssembly) is not IFlushable flushableRepository)
{
return false;
}
diff --git a/src/log4net/Util/CallerAssembly.cs b/src/log4net/Util/CallerAssembly.cs
new file mode 100644
index 000000000..424a5b1d1
--- /dev/null
+++ b/src/log4net/Util/CallerAssembly.cs
@@ -0,0 +1,74 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+using System.Reflection;
+
+namespace log4net.Util;
+
+///
+/// Support for the calls that select the
+/// of the caller.
+///
+///
+///
+/// Native AOT does not implement - it throws
+/// unconditionally, because the stack frames it
+/// would have to walk no longer exist after compilation. Callers therefore have to test
+/// and use instead.
+///
+///
+/// The test cannot be hidden behind a helper that calls
+/// itself: the calling assembly of such a helper is log4net, not the assembly that called log4net.
+/// has to stay in the public method whose caller is
+/// wanted, so this class only supplies the flag and the replacement value.
+///
+///
+internal static class CallerAssembly
+{
+ ///
+ /// Whether works on the current runtime.
+ ///
+ internal static bool IsSupported { get; } = Probe();
+
+ ///
+ /// The assembly to attribute a call to when is .
+ ///
+ ///
+ ///
+ /// The entry assembly is the closest available stand-in: an application published with
+ /// Native AOT is self-contained, so its loggers would almost always have ended up in the
+ /// entry assembly's repository anyway. Hosts without a managed entry point fall back to
+ /// log4net itself, which yields the default repository.
+ ///
+ ///
+ internal static Assembly Fallback { get; } = Assembly.GetEntryAssembly() ?? typeof(CallerAssembly).Assembly;
+
+ private static bool Probe()
+ {
+ try
+ {
+ return Assembly.GetCallingAssembly() is not null;
+ }
+ catch (PlatformNotSupportedException)
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/log4net/Util/ConverterInfo.cs b/src/log4net/Util/ConverterInfo.cs
index 0a122f6ef..421354545 100644
--- a/src/log4net/Util/ConverterInfo.cs
+++ b/src/log4net/Util/ConverterInfo.cs
@@ -20,6 +20,7 @@
*/
using System;
+using System.Diagnostics.CodeAnalysis;
namespace log4net.Util;
@@ -42,6 +43,7 @@ public sealed class ConverterInfo
///
/// Gets or sets the type of the converter. The type must extend .
///
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
public Type? Type { get; set; }
///
diff --git a/src/log4net/Util/PatternString.cs b/src/log4net/Util/PatternString.cs
index 568b272e6..e4e3e1270 100644
--- a/src/log4net/Util/PatternString.cs
+++ b/src/log4net/Util/PatternString.cs
@@ -18,6 +18,7 @@
#endregion
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using log4net.Util.PatternStringConverters;
@@ -258,29 +259,51 @@ public class PatternString : IOptionHandler
///
/// Internal map of converter identifiers to converter types.
///
- private static readonly Dictionary _sGlobalRulesRegistry = new(StringComparer.Ordinal)
+ private static readonly Dictionary _sGlobalRulesRegistry = CreateGlobalRulesRegistry();
+
+ ///
+ /// Builds the registry of built-in converters.
+ ///
+ /// the built-in rules, keyed by the name used in a pattern
+ ///
+ ///
+ /// Holds rather than a bare for the reason given
+ /// on 's registry: a in a collection loses
+ /// its trimmer annotation, and these converters are only ever created reflectively.
+ ///
+ ///
+ private static Dictionary CreateGlobalRulesRegistry()
{
// TODO - have added common variants of casing for utcdate and appsetting.
// Wouldn't it be better to use a case-insensitive dictionary?
- ["appdomain"] = typeof(AppDomainPatternConverter),
- ["appsetting"] = typeof(AppSettingPatternConverter),
- ["appSetting"] = typeof(AppSettingPatternConverter),
- ["AppSetting"] = typeof(AppSettingPatternConverter),
- ["date"] = typeof(DatePatternConverter),
- ["env"] = typeof(EnvironmentPatternConverter),
- ["envFolderPath"] = typeof(EnvironmentFolderPathPatternConverter),
- ["identity"] = typeof(IdentityPatternConverter),
- ["literal"] = typeof(LiteralPatternConverter),
- ["newline"] = typeof(NewLinePatternConverter),
- ["processid"] = typeof(ProcessIdPatternConverter),
- ["property"] = typeof(PropertyPatternConverter),
- ["random"] = typeof(RandomStringPatternConverter),
- ["username"] = typeof(UserNamePatternConverter),
- ["utcdate"] = typeof(UtcDatePatternConverter),
- ["utcDate"] = typeof(UtcDatePatternConverter),
- ["UtcDate"] = typeof(UtcDatePatternConverter),
- };
+ Dictionary rules = new(StringComparer.Ordinal);
+
+ Add("appdomain");
+ Add("appsetting", "appSetting", "AppSetting");
+ Add("date");
+ Add("env");
+ Add("envFolderPath");
+ Add("identity");
+ Add("literal");
+ Add("newline");
+ Add("processid");
+ Add("property");
+ Add("random");
+ Add("username");
+ Add("utcdate", "utcDate", "UtcDate");
+
+ return rules;
+
+ void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(
+ params string[] names) where T : PatternConverter, new()
+ {
+ foreach (string name in names)
+ {
+ rules[name] = new() { Name = name, Type = typeof(T) };
+ }
+ }
+ }
///
/// the head of the pattern converter chain
@@ -378,15 +401,14 @@ private PatternParser CreatePatternParser(string pattern)
{
PatternParser patternParser = new(pattern);
- // Add all the builtin patterns
- foreach (KeyValuePair entry in _sGlobalRulesRegistry)
+ // Add all the builtin patterns - copied, not shared; see PatternLayout.CreatePatternParser
+ foreach (KeyValuePair entry in _sGlobalRulesRegistry)
{
- ConverterInfo converterInfo = new()
+ patternParser.PatternConverters.Add(entry.Key, new ConverterInfo
{
- Name = entry.Key,
- Type = entry.Value
- };
- patternParser.PatternConverters.Add(entry.Key, converterInfo);
+ Name = entry.Value.Name,
+ Type = entry.Value.Type
+ });
}
// Add the instance patterns
foreach (KeyValuePair entry in _instanceRulesRegistry)
@@ -461,7 +483,8 @@ public void AddConverter(ConverterInfo converterInfo)
///
/// the name of the conversion pattern for this converter
/// the type of the converter
- public void AddConverter(string name, Type type)
+ public void AddConverter(string name,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
{
AddConverter(new()
{
diff --git a/src/log4net/Util/PatternStringConverters/UserNamePatternConverter.cs b/src/log4net/Util/PatternStringConverters/UserNamePatternConverter.cs
index 7e7fab529..f53782dd5 100644
--- a/src/log4net/Util/PatternStringConverters/UserNamePatternConverter.cs
+++ b/src/log4net/Util/PatternStringConverters/UserNamePatternConverter.cs
@@ -20,7 +20,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+#if !NET462_OR_GREATER
using System.Runtime.InteropServices;
+#endif
namespace log4net.Util.PatternStringConverters;
diff --git a/src/log4net/Util/SystemInfo.cs b/src/log4net/Util/SystemInfo.cs
index d5b9f9d39..eb3c656b1 100644
--- a/src/log4net/Util/SystemInfo.cs
+++ b/src/log4net/Util/SystemInfo.cs
@@ -19,9 +19,11 @@
using System;
using System.Configuration;
+using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.IO;
using System.Collections;
+using System.Runtime.CompilerServices;
namespace log4net.Util;
@@ -61,18 +63,18 @@ static SystemInfo()
// Look for log4net.NullText in AppSettings
string? nullTextAppSettingsKey = GetAppSetting("log4net.NullText");
- if (nullTextAppSettingsKey is not null && nullTextAppSettingsKey.Length > 0)
+ if (!string.IsNullOrEmpty(nullTextAppSettingsKey))
{
LogLog.Debug(_declaringType, $"Initializing NullText value to [{nullTextAppSettingsKey}].");
- nullText = nullTextAppSettingsKey;
+ nullText = nullTextAppSettingsKey!;
}
// Look for log4net.NotAvailableText in AppSettings
string? notAvailableTextAppSettingsKey = GetAppSetting("log4net.NotAvailableText");
- if (notAvailableTextAppSettingsKey is not null && notAvailableTextAppSettingsKey.Length > 0)
+ if (!string.IsNullOrEmpty(notAvailableTextAppSettingsKey))
{
LogLog.Debug(_declaringType, $"Initializing NotAvailableText value to [{notAvailableTextAppSettingsKey}].");
- notAvailableText = notAvailableTextAppSettingsKey;
+ notAvailableText = notAvailableTextAppSettingsKey!;
}
NotAvailableText = notAvailableText;
NullText = nullText;
@@ -154,12 +156,36 @@ public static string EntryAssemblyLocation
{
return _entryAssemblyLocation;
}
- return _entryAssemblyLocation = Assembly.GetEntryAssembly()?.Location
+ Assembly entryAssembly = Assembly.GetEntryAssembly()
?? throw new InvalidOperationException($"Unable to determine EntryAssembly location: EntryAssembly is null. Try explicitly setting {nameof(SystemInfo)}.{nameof(EntryAssemblyLocation)}");
+ string location = GetAssemblyLocation(entryAssembly);
+ if (location.Length == 0)
+ {
+ // A single file application has no file on disk for the assemblies it embeds, so the
+ // closest thing to the entry assembly's path is the directory it was published to.
+ location = Path.Combine(AppContext.BaseDirectory, $"{entryAssembly.GetName().Name}.dll");
+ }
+ return _entryAssemblyLocation = location;
}
set => _entryAssemblyLocation = value;
}
+ ///
+ /// Reads .
+ ///
+ /// the assembly to locate
+ /// the path to the assembly, or an empty string when it has none
+ ///
+ ///
+ /// An assembly embedded in a single file application has no path, and
+ /// returns an empty string for it rather than failing. Callers
+ /// handle that here, which is what the suppressed warning asks them to do.
+ ///
+ ///
+ [UnconditionalSuppressMessage("SingleFile", "IL3000",
+ Justification = "The empty string returned by a single file application is handled by the callers.")]
+ private static string GetAssemblyLocation(Assembly assembly) => assembly.Location;
+
///
/// Gets the ID of the current thread.
///
@@ -368,7 +394,8 @@ public static string AssemblyLocationInfo(Assembly myAssembly)
// This call requires FileIOPermission for access to the path
// if we don't have permission then we just ignore it and
// carry on.
- return myAssembly.Location;
+ string location = GetAssemblyLocation(myAssembly);
+ return location.Length > 0 ? location : "Single File Application";
}
catch (NotSupportedException)
{
@@ -475,7 +502,9 @@ public static string AssemblyFileName(Assembly myAssembly)
///
///
public static Type? GetTypeFromString(string typeName, bool throwOnError, bool ignoreCase)
- => GetTypeFromString(Assembly.GetCallingAssembly(), typeName, throwOnError, ignoreCase);
+ => GetTypeFromString(CallerAssembly.IsSupported
+ ? Assembly.GetCallingAssembly()
+ : CallerAssembly.Fallback, typeName, throwOnError, ignoreCase);
///
/// Loads the type specified in the type string.
@@ -680,20 +709,107 @@ public static bool TryParse(string s, out short val)
/// the value for the key, or
public static string? GetAppSetting(string key)
{
- if (IsAndroid)
- return Environment.GetEnvironmentVariable(key); // Android does not support config files
+ // Android does not support config files, and neither does a runtime that has trimmed the
+ // configuration system away.
+ if (IsAndroid || _configurationSystemUnavailable)
+ return Environment.GetEnvironmentVariable(key);
try
{
- return ConfigurationManager.AppSettings[key];
+ return ReadAppSetting(key);
}
catch (Exception e) when (!e.IsFatal())
{
- // If an exception is thrown here then it looks like the config file does not parse correctly.
+ if (IsMissingConfigurationSystem(e))
+ {
+ // 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;
+ LogLog.Debug(_declaringType,
+ "No configuration system on this runtime. Using environment variables for application settings.", e);
+ return Environment.GetEnvironmentVariable(key);
+ }
+
+ // The config file itself does not parse. Report it and treat the setting as absent, without
+ // falling back to the environment - a broken config file must not silently change where
+ // settings come from.
LogLog.Error(_declaringType, "Exception while reading ConfigurationSettings. Check your .config file is well formed XML.", e);
}
return null;
}
+ ///
+ /// Determines whether means that there is no configuration system
+ /// on this runtime, as opposed to a configuration file that does not parse.
+ ///
+ /// the exception thrown while reading an application setting
+ /// if the configuration system itself is unavailable
+ ///
+ ///
+ /// The inner exceptions have to be walked, because Native AOT surfaces this as a
+ /// - the very type a malformed file produces. What
+ /// distinguishes it is further down the chain: a for
+ /// ClientConfigurationHost, whose constructor the trimmer removed.
+ ///
+ ///
+ /// An unrecognized failure is treated as a configuration file problem, which is the safer way
+ /// round: it is reported rather than silently swallowed.
+ ///
+ ///
+ private static bool IsMissingConfigurationSystem(Exception? exception)
+ {
+ // Native AOT is what this exists for, and it identifies itself without any guesswork:
+ // GetCallingAssembly is unsupported there for the same reason the configuration system cannot
+ // initialize, so no configuration file can be read whatever the exception happens to be.
+ if (!CallerAssembly.IsSupported)
+ {
+ return true;
+ }
+
+ // Anywhere else, only a failure that names System.Configuration itself counts. A failure that
+ // names anything else belongs to the application's own configuration and has to keep being
+ // reported as an error rather than silently redirecting every setting to the environment.
+ for (; exception is not null; exception = exception.InnerException)
+ {
+ switch (exception)
+ {
+ case FileNotFoundException { FileName: string fileName }
+ when IsConfigurationSystem(fileName):
+ case TypeLoadException { TypeName: string typeName }
+ when IsConfigurationSystem(typeName):
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static bool IsConfigurationSystem(string name)
+ => name.StartsWith("System.Configuration", StringComparison.Ordinal);
+
+ ///
+ /// Reads a single application setting.
+ ///
+ /// the application settings key to lookup
+ /// the value for the key, or
+ ///
+ ///
+ /// Separate from , and never inlined into it, so that the failure to
+ /// resolve itself is raised on entry to this method - inside
+ /// the caller's try block - rather than on entry to , where nothing
+ /// would catch it and a would escape the static constructor
+ /// as a .
+ ///
+ ///
+ /// The package declares a dependency on System.Configuration.ConfigurationManager, so this only
+ /// arises where the assembly is deployed by other means than the package - it costs one method
+ /// to keep those deployments running instead of failing at type initialization.
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static string? ReadAppSetting(string key) => ConfigurationManager.AppSettings[key];
+
+ private static volatile bool _configurationSystemUnavailable;
+
///
/// Convert a path into a fully qualified local file path.
///
diff --git a/src/log4net/Util/TypeConverters/ConverterRegistry.cs b/src/log4net/Util/TypeConverters/ConverterRegistry.cs
index 03259584a..af8518966 100644
--- a/src/log4net/Util/TypeConverters/ConverterRegistry.cs
+++ b/src/log4net/Util/TypeConverters/ConverterRegistry.cs
@@ -18,6 +18,7 @@
#endregion
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Collections.Concurrent;
namespace log4net.Util.TypeConverters;
@@ -82,7 +83,8 @@ public static void AddConverter(Type? destinationType, object? converter)
///
/// The type being converted to.
/// The type of the type converter to use to convert to the destination type.
- public static void AddConverter(Type destinationType, Type converterType)
+ public static void AddConverter(Type destinationType,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type converterType)
=> AddConverter(destinationType, CreateConverterInstance(converterType.EnsureNotNull()));
///
@@ -187,7 +189,8 @@ public static void AddConverter(Type destinationType, Type converterType)
/// and must have a public default (no argument) constructor.
///
///
- private static object? CreateConverterInstance(Type converterType)
+ private static object? CreateConverterInstance(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type converterType)
{
// Check type is a converter
if (typeof(IConvertFrom).IsAssignableFrom(converterType) || typeof(IConvertTo).IsAssignableFrom(converterType))
diff --git a/src/log4net/Util/WindowsSecurityContext.cs b/src/log4net/Util/WindowsSecurityContext.cs
index 3eee0b87b..ce83bf912 100644
--- a/src/log4net/Util/WindowsSecurityContext.cs
+++ b/src/log4net/Util/WindowsSecurityContext.cs
@@ -19,7 +19,6 @@
#if NET462_OR_GREATER
using System;
-using System.Runtime.InteropServices;
using System.Security;
using System.Security.Principal;
diff --git a/src/site/antora/modules/ROOT/nav.adoc b/src/site/antora/modules/ROOT/nav.adoc
index bf81497b5..8aadc37df 100644
--- a/src/site/antora/modules/ROOT/nav.adoc
+++ b/src/site/antora/modules/ROOT/nav.adoc
@@ -42,6 +42,7 @@
**** xref:manual/configuration/appenders/udpappender.adoc[]
*** xref:manual/configuration/filters.adoc[]
*** xref:manual/configuration/layouts.adoc[]
+** xref:manual/native-aot.adoc[]
** xref:manual/examples.adoc[]
** xref:manual/faq.adoc[]
* xref:features.adoc[]
diff --git a/src/site/antora/modules/ROOT/pages/manual/configuration.adoc b/src/site/antora/modules/ROOT/pages/manual/configuration.adoc
index 6d300ea6e..4619fd13d 100644
--- a/src/site/antora/modules/ROOT/pages/manual/configuration.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/configuration.adoc
@@ -21,6 +21,13 @@
The recommended way to configure log4net is through a configuration file.
This section explains the structure of a configuration file and how log4net processes it.
+[NOTE]
+====
+Configuration files cannot be used in an application published with `PublishAot`, because the types
+they name are removed by the trimmer.
+See xref:manual/native-aot.adoc[] for how to configure log4net in code instead.
+====
+
[source,csharp]
----
using Animals.Carnivora;
diff --git a/src/site/antora/modules/ROOT/pages/manual/native-aot.adoc b/src/site/antora/modules/ROOT/pages/manual/native-aot.adoc
new file mode 100644
index 000000000..ff101412f
--- /dev/null
+++ b/src/site/antora/modules/ROOT/pages/manual/native-aot.adoc
@@ -0,0 +1,196 @@
+////
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+////
+
+[#native-aot]
+= 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]
+== Configuring in code
+
+Build the appenders and layouts yourself and hand them to
+xref:manual/configuration.adoc[`BasicConfigurator`].
+Because you construct them with `new`, the compiler sees them and keeps them:
+
+[source,csharp]
+----
+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:
+
+[source,csharp]
+----
+PatternLayout layout = new();
+layout.AddConverter("mine", typeof(MyPatternConverter));
+layout.ConversionPattern = "%mine %message%newline";
+layout.ActivateOptions();
+----
+
+[#levels-from-a-file]
+== 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:
+
+[source,json]
+----
+{
+ "Logging": {
+ "Default": "WARN",
+ "Loggers": {
+ "Noisy.Component": "ERROR",
+ "Important.Component": "DEBUG"
+ }
+ }
+}
+----
+
+Read them yourself and apply them to the repository:
+
+[source,csharp]
+----
+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.
+
+[TIP]
+====
+`LevelMap` returns `null` for a name it does not know, which is why both lookups are written as
+`is Level`. An unrecognised name in the file then leaves the level unchanged rather than throwing.
+Custom levels registered with `hierarchy.LevelMap.Add(...)` can be named in the file too.
+====
+
+[NOTE]
+====
+`JsonDocument` is used here because it parses without reflection and is safe to trim.
+`JsonSerializer.Deserialize()` is not, unless you generate a `JsonSerializerContext` for your
+settings type. Any other format your application can already read works just as well - the point is
+only that *your* code reads the file, not log4net's.
+====
+
+Levels can be changed whenever you like, so the same code can be run again to reload the file while
+the application is running.
+
+[#unsupported]
+== What does not work
+
+[IMPORTANT]
+====
+xref:manual/configuration.adoc[XML configuration] - `XmlConfigurator.Configure()`, the
+`log4net.config` file and the `` section of `app.config` - is **not available** under
+Native AOT. Appender, layout and filter types are named as strings there, and the trimmer has no way
+to know that they are needed.
+
+This is about *log4net* reading the file. Your application can still read a file of its own and
+apply what it finds - see <>.
+====
+
+`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`:
+
+[source,shell]
+----
+log4net.NullText=NULL
+log4net.NotAvailableText=N/A
+----
+
+[#repositories]
+== 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
+
+[source,csharp]
+----
+[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:
+
+[source,csharp]
+----
+ILog log = LogManager.GetLogger(typeof(MyType).Assembly, typeof(MyType));
+----
+
+[#warnings]
+== Trimming warnings
+
+Publishing may report `IL3000` for `log4net.Util.SystemInfo.AssemblyLocationInfo`, because
+`Assembly.Location` returns an empty string for an assembly embedded in a single-file application.
+This affects the `%file`-style location information only; logging itself is unaffected.