diff --git a/src/Spring/Spring.Aop/Aspects/Logging/AbstractLoggingAdvice.cs b/src/Spring/Spring.Aop/Aspects/Logging/AbstractLoggingAdvice.cs
index 3c14126b7..176872698 100644
--- a/src/Spring/Spring.Aop/Aspects/Logging/AbstractLoggingAdvice.cs
+++ b/src/Spring/Spring.Aop/Aspects/Logging/AbstractLoggingAdvice.cs
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-using System.Reflection;
using System.Runtime.Serialization;
using AopAlliance.Intercept;
using Microsoft.Extensions.Logging;
@@ -33,7 +32,9 @@ namespace Spring.Aspects.Logging;
public abstract class AbstractLoggingAdvice : IMethodInterceptor, IDeserializationCallback
{
///
- /// The default ILog instance used to write logging messages.
+ /// The explicitly supplied instance used to write logging messages,
+ /// if any. When null, the logger is resolved through ,
+ /// either by or dynamically per invocation.
///
[NonSerialized] protected ILogger defaultLogger;
@@ -52,7 +53,7 @@ public abstract class AbstractLoggingAdvice : IMethodInterceptor, IDeserializati
///
protected AbstractLoggingAdvice()
{
- SetDefaultLogger(MethodBase.GetCurrentMethod().DeclaringType.FullName);
+ SetDefaultLogger(GetType().FullName);
}
///
@@ -89,8 +90,9 @@ public bool UseDynamicLogger
/// Sets the name of the logger to use.
///
///
- /// The name will be passed to the underlying logging implementation through Common.Logging,
- /// getting interpreted as the log category according to the loggers configuration.
+ /// The name will be passed to the underlying logging implementation through the configured
+ /// , getting interpreted as the log category
+ /// according to the loggers configuration.
///
/// This can be specified to not log into the category of a Type (whether this
/// interceptor's class or the class getting called) but rather to a specific named category.
@@ -227,26 +229,35 @@ protected virtual ILogger GetLoggerForInvocation(IMethodInvocation invocation)
{
return defaultLogger;
}
- else
+
+ if (defaultLoggerName != null)
{
- object target = invocation.This;
- Type logCategoryType = target.GetType();
- if (hideProxyTypeNames)
- {
- logCategoryType = AopUtils.GetTargetType(target);
- }
+ return LogManager.GetLogger(defaultLoggerName);
+ }
- return LogManager.GetLogger(logCategoryType);
+ object target = invocation.This;
+ Type logCategoryType = target.GetType();
+ if (hideProxyTypeNames)
+ {
+ logCategoryType = AopUtils.GetTargetType(target);
}
+
+ return LogManager.GetLogger(logCategoryType);
}
///
- /// Sets the default logger to the given name.
+ /// Sets the name of the default logger.
///
- /// if null, the default logger is removed.
+ ///
+ /// The logger is resolved lazily through on each invocation,
+ /// so a assigned after this advice has been
+ /// created is still picked up.
+ ///
+ /// if null, the default logger is removed and a dynamic,
+ /// per-target logger is used instead.
protected void SetDefaultLogger(string name)
{
- defaultLogger = (name == null ? null : LogManager.GetLogger(name));
+ defaultLogger = null;
defaultLoggerName = name;
}
diff --git a/src/Spring/Spring.Aop/Aspects/Logging/SimpleLoggingAdvice.cs b/src/Spring/Spring.Aop/Aspects/Logging/SimpleLoggingAdvice.cs
index 25e2df22c..d8173a4db 100644
--- a/src/Spring/Spring.Aop/Aspects/Logging/SimpleLoggingAdvice.cs
+++ b/src/Spring/Spring.Aop/Aspects/Logging/SimpleLoggingAdvice.cs
@@ -140,6 +140,12 @@ public string Separator
///
/// Gets or sets the entry log level.
///
+ ///
+ /// When configured from a string value, the legacy Common.Logging level names used by
+ /// Spring.NET configurations prior to 3.0 are also accepted: All maps to
+ /// Trace, Info to Information, Warn to Warning,
+ /// Fatal to Critical and Off to None.
+ ///
/// The entry log level.
public LogLevel LogLevel
{
diff --git a/src/Spring/Spring.Core/Core/TypeConversion/LogLevelConverter.cs b/src/Spring/Spring.Core/Core/TypeConversion/LogLevelConverter.cs
new file mode 100644
index 000000000..b4529f3f3
--- /dev/null
+++ b/src/Spring/Spring.Core/Core/TypeConversion/LogLevelConverter.cs
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2002-2026 the original author or authors.
+ *
+ * Licensed 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.
+ */
+
+using System.ComponentModel;
+using System.Globalization;
+using Microsoft.Extensions.Logging;
+
+namespace Spring.Core.TypeConversion;
+
+///
+/// Converter for instances.
+///
+///
+/// In addition to the member names
+/// (Trace, Debug, Information, Warning, Error,
+/// Critical, None), the legacy Common.Logging level names used by
+/// Spring.NET configurations prior to 3.0 are accepted: All maps to
+/// Trace, Info to Information, Warn to Warning,
+/// Fatal to Critical and Off to None.
+/// Names are matched case-insensitively.
+///
+public class LogLevelConverter : EnumConverter
+{
+ private static readonly Dictionary LegacyLevels = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["All"] = LogLevel.Trace,
+ ["Info"] = LogLevel.Information,
+ ["Warn"] = LogLevel.Warning,
+ ["Fatal"] = LogLevel.Critical,
+ ["Off"] = LogLevel.None
+ };
+
+ ///
+ /// Creates a new instance of the
+ /// class.
+ ///
+ public LogLevelConverter() : base(typeof(LogLevel))
+ {
+ }
+
+ ///
+ /// Convert from a string value to a instance.
+ ///
+ ///
+ /// A
+ /// that provides a format context.
+ ///
+ ///
+ /// The to use
+ /// as the current culture.
+ ///
+ ///
+ /// The value that is to be converted.
+ ///
+ ///
+ /// A if successful.
+ ///
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ if (value is string text && LegacyLevels.TryGetValue(text.Trim(), out LogLevel level))
+ {
+ return level;
+ }
+
+ return base.ConvertFrom(context, culture, value);
+ }
+}
diff --git a/src/Spring/Spring.Core/Core/TypeConversion/TypeConverterRegistry.cs b/src/Spring/Spring.Core/Core/TypeConversion/TypeConverterRegistry.cs
index a6d567f84..6e2051fec 100644
--- a/src/Spring/Spring.Core/Core/TypeConversion/TypeConverterRegistry.cs
+++ b/src/Spring/Spring.Core/Core/TypeConversion/TypeConverterRegistry.cs
@@ -20,6 +20,7 @@
using System.Net;
using System.Resources;
using System.Text.RegularExpressions;
+using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using Spring.Core.TypeResolution;
using Spring.Util;
@@ -57,6 +58,7 @@ static TypeConverterRegistry()
converters[typeof(ResourceManager)] = new ResourceManagerConverter();
converters[typeof(Regex)] = new RegexConverter();
converters[typeof(TimeSpan)] = new TimeSpanConverter();
+ converters[typeof(LogLevel)] = new LogLevelConverter();
converters[typeof(ICredentials)] = new CredentialConverter();
converters[typeof(NetworkCredential)] = new CredentialConverter();
converters[typeof(RegistryKey)] = new RegistryKeyConverter();
diff --git a/src/Spring/Spring.Core/LogManager.cs b/src/Spring/Spring.Core/LogManager.cs
index 02bd9d955..73ef74237 100644
--- a/src/Spring/Spring.Core/LogManager.cs
+++ b/src/Spring/Spring.Core/LogManager.cs
@@ -24,6 +24,12 @@ public static class LogManager
///
/// Gets or sets the current log provider based on logger factory.
///
+ ///
+ /// Until a factory is assigned, all loggers returned by the GetLogger methods
+ /// are no-op instances. Assigning a factory takes effect for
+ /// all loggers resolved afterwards, including lazily resolved ones such as those used
+ /// by Spring.Aspects.Logging.AbstractLoggingAdvice.
+ ///
public static ILoggerFactory LoggerFactory { get; set; }
public static ILogger GetLogger(string category) => LoggerFactory?.CreateLogger(category) ?? NullLogger.Instance;
diff --git a/test/Spring/Spring.Aop.Tests/Aspects/Logging/SimpleLoggingAdviceTests.cs b/test/Spring/Spring.Aop.Tests/Aspects/Logging/SimpleLoggingAdviceTests.cs
index 1d9055332..20607e4cf 100644
--- a/test/Spring/Spring.Aop.Tests/Aspects/Logging/SimpleLoggingAdviceTests.cs
+++ b/test/Spring/Spring.Aop.Tests/Aspects/Logging/SimpleLoggingAdviceTests.cs
@@ -15,12 +15,15 @@
*/
using System.Reflection;
+using System.Text;
using AopAlliance.Intercept;
using FakeItEasy;
using FakeItEasy.Configuration;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Spring.Aop.Framework;
+using Spring.Core.IO;
+using Spring.Objects.Factory.Xml;
namespace Spring.Aspects.Logging;
@@ -43,9 +46,18 @@ public void DoSomething()
}
}
+ private ILoggerFactory originalLoggerFactory;
+
[SetUp]
public void Setup()
{
+ originalLoggerFactory = LogManager.LoggerFactory;
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ LogManager.LoggerFactory = originalLoggerFactory;
}
[Test]
@@ -164,6 +176,93 @@ public void SunnyDayLoggingAllOptionalInformationCorrectly()
log.VerifyLogMustHaveHappened(LogLevel.Trace, "Entering Bark");
log.VerifyLogMustHaveHappened(LogLevel.Trace, "Exiting Bark");
}
+
+ [Test]
+ public void LoggerFactoryAssignedAfterAdviceConstructionIsUsed()
+ {
+ LogManager.LoggerFactory = null;
+ SimpleLoggingAdvice loggingAdvice = new SimpleLoggingAdvice();
+
+ ProxyFactory pf = new ProxyFactory(new TestTarget());
+ pf.AddAdvice(loggingAdvice);
+ ITestTarget ptt = (ITestTarget) pf.GetProxy();
+
+ RecordingLoggerFactory loggerFactory = new RecordingLoggerFactory();
+ LogManager.LoggerFactory = loggerFactory;
+
+ ptt.DoSomething();
+
+ Assert.That(loggerFactory.Messages, Has.Some.Contains("Entering DoSomething"));
+ Assert.That(loggerFactory.Messages, Has.Some.Contains("Exiting DoSomething"));
+ }
+
+ [Test]
+ public void DefaultLoggerCategoryIsConcreteAdviceType()
+ {
+ RecordingLoggerFactory loggerFactory = new RecordingLoggerFactory();
+ LogManager.LoggerFactory = loggerFactory;
+
+ SimpleLoggingAdvice loggingAdvice = new SimpleLoggingAdvice();
+ ProxyFactory pf = new ProxyFactory(new TestTarget());
+ pf.AddAdvice(loggingAdvice);
+ ((ITestTarget) pf.GetProxy()).DoSomething();
+
+ Assert.That(loggerFactory.Categories, Does.Contain(typeof(SimpleLoggingAdvice).FullName));
+ }
+
+ [Test]
+ public void LegacyLogLevelNameCanBeConfiguredFromXml()
+ {
+ string xml = $@"
+
+
+";
+
+ XmlObjectFactory objectFactory = new XmlObjectFactory(new StringResource(xml, Encoding.UTF8));
+ SimpleLoggingAdvice advice = (SimpleLoggingAdvice) objectFactory.GetObject("loggingAdvice");
+
+ Assert.That(advice.LogLevel, Is.EqualTo(LogLevel.Information));
+ }
+
+ private sealed class RecordingLoggerFactory : ILoggerFactory
+ {
+ public List Categories { get; } = [];
+ public List Messages { get; } = [];
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ Categories.Add(categoryName);
+ return new RecordingLogger(Messages);
+ }
+
+ public void AddProvider(ILoggerProvider provider)
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+
+ private sealed class RecordingLogger(List messages) : ILogger
+ {
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ messages.Add(formatter(state, exception));
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return true;
+ }
+
+ public IDisposable BeginScope(TState state)
+ {
+ return null;
+ }
+ }
+ }
}
public class Dog
diff --git a/test/Spring/Spring.Core.Tests/Core/TypeConversion/LogLevelConverterTests.cs b/test/Spring/Spring.Core.Tests/Core/TypeConversion/LogLevelConverterTests.cs
new file mode 100644
index 000000000..3bcab5c44
--- /dev/null
+++ b/test/Spring/Spring.Core.Tests/Core/TypeConversion/LogLevelConverterTests.cs
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2026 the original author or authors.
+ *
+ * Licensed 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.
+ */
+
+using Microsoft.Extensions.Logging;
+using NUnit.Framework;
+
+namespace Spring.Core.TypeConversion;
+
+///
+/// Unit tests for the LogLevelConverter class.
+///
+[TestFixture]
+public sealed class LogLevelConverterTests
+{
+ private readonly LogLevelConverter converter = new LogLevelConverter();
+
+ [TestCase("All", LogLevel.Trace)]
+ [TestCase("Info", LogLevel.Information)]
+ [TestCase("info", LogLevel.Information)]
+ [TestCase(" Info ", LogLevel.Information)]
+ [TestCase("Warn", LogLevel.Warning)]
+ [TestCase("WARN", LogLevel.Warning)]
+ [TestCase("Fatal", LogLevel.Critical)]
+ [TestCase("Off", LogLevel.None)]
+ public void ConvertsLegacyCommonLoggingLevelNames(string text, LogLevel expected)
+ {
+ Assert.That(converter.ConvertFrom(text), Is.EqualTo(expected));
+ }
+
+ [TestCase("Trace", LogLevel.Trace)]
+ [TestCase("Debug", LogLevel.Debug)]
+ [TestCase("Information", LogLevel.Information)]
+ [TestCase("information", LogLevel.Information)]
+ [TestCase("Warning", LogLevel.Warning)]
+ [TestCase("Error", LogLevel.Error)]
+ [TestCase("Critical", LogLevel.Critical)]
+ [TestCase("None", LogLevel.None)]
+ public void ConvertsCanonicalLevelNames(string text, LogLevel expected)
+ {
+ Assert.That(converter.ConvertFrom(text), Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void ConvertsNumericStrings()
+ {
+ Assert.That(converter.ConvertFrom("2"), Is.EqualTo(LogLevel.Information));
+ }
+
+ [Test]
+ public void ThrowsFormatExceptionForUnknownLevelName()
+ {
+ Assert.Throws(() => converter.ConvertFrom("Verbose"));
+ }
+
+ [Test]
+ public void CanConvertFromString()
+ {
+ Assert.IsTrue(converter.CanConvertFrom(typeof(string)));
+ }
+}
diff --git a/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConversionUtilsTests.cs b/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConversionUtilsTests.cs
index 0653df176..c1b94632f 100644
--- a/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConversionUtilsTests.cs
+++ b/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConversionUtilsTests.cs
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+using Microsoft.Extensions.Logging;
using NUnit.Framework;
namespace Spring.Core.TypeConversion;
@@ -64,4 +65,17 @@ public void ConvertValueWithDutchCultureForDecimalMarkWithCommaReturnsValue()
object o = TypeConversionUtils.ConvertValueIfNecessary(typeof(Double), "1,2", "foo");
Assert.That(o, Is.EqualTo(1.2));
}
+
+ [Test]
+ public void ConvertsLegacyCommonLoggingLogLevelName()
+ {
+ object o = TypeConversionUtils.ConvertValueIfNecessary(typeof(LogLevel), "Info", "LogLevel");
+ Assert.That(o, Is.EqualTo(LogLevel.Information));
+ }
+
+ [Test]
+ public void ThrowsTypeMismatchForUnknownLogLevelName()
+ {
+ Assert.Throws(() => TypeConversionUtils.ConvertValueIfNecessary(typeof(LogLevel), "Verbose", "LogLevel"));
+ }
}
diff --git a/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConverterRegistryTests.cs b/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConverterRegistryTests.cs
index 1346c05ab..395b5d41f 100644
--- a/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConverterRegistryTests.cs
+++ b/test/Spring/Spring.Core.Tests/Core/TypeConversion/TypeConverterRegistryTests.cs
@@ -33,6 +33,13 @@ public void GetConverterForEnums()
Assert.IsTrue(converter is EnumConverter);
}
+ [Test]
+ public void GetConverterForLogLevel()
+ {
+ TypeConverter converter = TypeConverterRegistry.GetConverter(typeof(Microsoft.Extensions.Logging.LogLevel));
+ Assert.IsTrue(converter is LogLevelConverter);
+ }
+
[Test]
public void GetInternalConverter()
{