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/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..c24914ff1 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;
@@ -171,4 +173,102 @@ public void EqualsIgnoringCase_DifferentStrings_false()
[Platform(Include = "Win,Linux,MacOsX")]
public void IsAndoid()
=> 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, so only the inner exception tells them apart.
+ ///
+ [Test]
+ public void TrimmedConfigurationSystemIsRecognisedThroughTheInnerException()
+ => Assert.That(IsMissingConfigurationSystem(
+ new ConfigurationErrorsException("Configuration system failed to initialize",
+ new MissingMethodException("No parameterless constructor defined for type 'System.Configuration.ClientConfigurationHost'."))),
+ Is.True);
+
+ ///
+ /// A deployment without the System.Configuration.ConfigurationManager assembly fails on the
+ /// outermost exception rather than an inner one.
+ ///
+ [Test]
+ public void MissingConfigurationAssemblyIsRecognised()
+ => Assert.That(IsMissingConfigurationSystem(new FileNotFoundException("System.Configuration.ConfigurationManager")), Is.True);
+
+ 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..f9668c7a3 100644
--- a/src/log4net.Tests/log4net.Tests.csproj
+++ b/src/log4net.Tests/log4net.Tests.csproj
@@ -19,6 +19,7 @@
quackers
+
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/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/Layout/PatternLayout.cs b/src/log4net/Layout/PatternLayout.cs
index 29e64746a..d1bf32cd2 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
@@ -983,14 +978,9 @@ protected virtual PatternParser CreatePatternParser(string pattern)
PatternParser patternParser = new(pattern);
// Add all the builtin patterns
- foreach (KeyValuePair entry in _sGlobalRulesRegistry)
+ foreach (KeyValuePair entry in _sGlobalRulesRegistry)
{
- ConverterInfo converterInfo = new()
- {
- Name = entry.Key,
- Type = entry.Value
- };
- patternParser.PatternConverters[entry.Key] = converterInfo;
+ patternParser.PatternConverters[entry.Key] = entry.Value;
}
// Add the instance patterns
foreach (KeyValuePair entry in _instanceRulesRegistry)
@@ -1093,7 +1083,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/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..7978c9566 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
@@ -379,14 +402,9 @@ private PatternParser CreatePatternParser(string pattern)
PatternParser patternParser = new(pattern);
// Add all the builtin patterns
- foreach (KeyValuePair entry in _sGlobalRulesRegistry)
+ foreach (KeyValuePair entry in _sGlobalRulesRegistry)
{
- ConverterInfo converterInfo = new()
- {
- Name = entry.Key,
- Type = entry.Value
- };
- patternParser.PatternConverters.Add(entry.Key, converterInfo);
+ patternParser.PatternConverters.Add(entry.Key, entry.Value);
}
// Add the instance patterns
foreach (KeyValuePair entry in _instanceRulesRegistry)
@@ -461,7 +479,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/SystemInfo.cs b/src/log4net/Util/SystemInfo.cs
index d5b9f9d39..d0c2647c6 100644
--- a/src/log4net/Util/SystemInfo.cs
+++ b/src/log4net/Util/SystemInfo.cs
@@ -22,6 +22,7 @@
using System.Reflection;
using System.IO;
using System.Collections;
+using System.Runtime.CompilerServices;
namespace log4net.Util;
@@ -475,7 +476,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 +683,90 @@ 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)
+ {
+ for (; exception is not null; exception = exception.InnerException)
+ {
+ if (exception is MissingMethodException or TypeLoadException or FileNotFoundException
+ or PlatformNotSupportedException or NotSupportedException)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// 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 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/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.