diff --git a/Ical.Net.Tests/Ical.Net.Tests.csproj b/Ical.Net.Tests/Ical.Net.Tests.csproj
index 90065b7a4..964dc2ff5 100644
--- a/Ical.Net.Tests/Ical.Net.Tests.csproj
+++ b/Ical.Net.Tests/Ical.Net.Tests.csproj
@@ -11,8 +11,7 @@
$(NoWarn);CS8600;CS8601;CS8602;CS8603;CS8604;CS8618;CS8620;CS8714
-
-
+
diff --git a/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerAdapter.cs b/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerAdapter.cs
new file mode 100644
index 000000000..17184f7e9
--- /dev/null
+++ b/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerAdapter.cs
@@ -0,0 +1,38 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+#nullable enable
+using System;
+using Microsoft.Extensions.Logging;
+
+namespace Ical.Net.Tests.Logging.Adapters;
+
+#pragma warning disable CA2254 // template should be a static expression
+
+internal class MicrosoftLoggerAdapter(ILogger msLogger) : Ical.Net.Logging.ILogger
+{
+ public void Log(Ical.Net.Logging.LogLevel level, string messageTemplate, params object[] args)
+ => msLogger.Log(ConvertLogLevel(level), messageTemplate, args);
+
+ public void Log(Ical.Net.Logging.LogLevel level, Exception exception, string messageTemplate, params object[] args)
+ => msLogger.Log(ConvertLogLevel(level), exception, messageTemplate, args);
+
+ public bool IsEnabled(Ical.Net.Logging.LogLevel level) =>
+ msLogger.IsEnabled(ConvertLogLevel(level));
+
+ private static LogLevel ConvertLogLevel(Ical.Net.Logging.LogLevel level)
+ {
+ return level switch
+ {
+ Ical.Net.Logging.LogLevel.Trace => LogLevel.Trace,
+ Ical.Net.Logging.LogLevel.Debug => LogLevel.Debug,
+ Ical.Net.Logging.LogLevel.Information => LogLevel.Information,
+ Ical.Net.Logging.LogLevel.Warning => LogLevel.Warning,
+ Ical.Net.Logging.LogLevel.Error => LogLevel.Error,
+ Ical.Net.Logging.LogLevel.Critical => LogLevel.Critical,
+ _ => LogLevel.None
+ };
+ }
+}
+#pragma warning restore CA2254
diff --git a/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerFactoryAdapter.cs b/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerFactoryAdapter.cs
new file mode 100644
index 000000000..52c5430fc
--- /dev/null
+++ b/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerFactoryAdapter.cs
@@ -0,0 +1,67 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+#nullable enable
+using System;
+
+namespace Ical.Net.Tests.Logging.Adapters;
+
+///
+/// Adapts an
+/// to the interface.
+///
+/// This class provides a bridge between the framework and the
+/// abstraction. It allows the creation of
+/// instances using the underlying
+/// .
+///
+internal class MicrosoftLoggerFactoryAdapter : Ical.Net.Logging.ILoggerFactory
+{
+ private readonly Microsoft.Extensions.Logging.ILoggerFactory _msLoggerFactory;
+ private bool _disposed;
+
+ ///
+ /// Initializes a new instance of the class, using the specified
+ /// Microsoft.Extensions.Logging .
+ ///
+ /// The instance used to create loggers.
+ /// This parameter cannot be .
+ ///
+ public MicrosoftLoggerFactoryAdapter(Microsoft.Extensions.Logging.ILoggerFactory msLoggerFactory)
+ {
+ _msLoggerFactory = msLoggerFactory;
+ }
+
+ ///
+ /// Creates a logger instance for the specified category name.
+ ///
+ /// The name of the category for the logger.
+ /// A instance configured to log messages for the specified category.
+ public Ical.Net.Logging.ILogger CreateLogger(string categoryName)
+ {
+ var msLogger = _msLoggerFactory.CreateLogger(categoryName);
+ return new MicrosoftLoggerAdapter(msLogger);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed) return;
+
+ if (disposing)
+ _msLoggerFactory.Dispose();
+ _disposed = true;
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ~MicrosoftLoggerFactoryAdapter()
+ {
+ Dispose(false);
+ }
+}
diff --git a/Ical.Net.Tests/Logging/LoggingProviderUnitTests.cs b/Ical.Net.Tests/Logging/LoggingProviderUnitTests.cs
index 34bb9a3ff..5b0564aad 100644
--- a/Ical.Net.Tests/Logging/LoggingProviderUnitTests.cs
+++ b/Ical.Net.Tests/Logging/LoggingProviderUnitTests.cs
@@ -6,16 +6,49 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading;
using Ical.Net.DataTypes;
using Ical.Net.Logging;
-using Microsoft.Extensions.Logging;
+using Ical.Net.Logging.Internal;
using NUnit.Framework;
-
namespace Ical.Net.Tests.Logging;
+// LoggingProvider.SetLoggerFactory is static and gets set in each test,
+// so we need to ensure that the tests are not run in parallel.
+[Parallelizable(ParallelScope.None)]
internal class LoggingProviderUnitTests
{
+ [Test]
+ public void InitializingLoggerFactoryTwice_ShouldThrow()
+ {
+ using var logging1 = new TestLoggingProvider(new Options { DebugModeOnly = false });
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(LoggingProvider.FactoryIsSet, Is.True);
+ Assert.That(() =>
+ {
+ using var logging2 = new TestLoggingProvider(new Options { DebugModeOnly = false });
+ }, Throws.InvalidOperationException);
+ }
+ }
+
+ [Test]
+ public void LoggerFactoryNotSet_ShouldFallBackToNullLoggerFactory()
+ {
+ using var logging1 = new TestLoggingProvider(new Options { DebugModeOnly = false });
+ LoggingProvider.SetLoggerFactory(null, true);
+ var logger = LoggingProvider.CreateLogger();
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(LoggingProvider.LoggerFactory, Is.InstanceOf());
+ Assert.That(logger, Is.InstanceOf>());
+ Assert.That(logger.IsEnabled(LogLevel.Information), Is.False);
+ Assert.That(() => LoggingProvider.LoggerFactory.Dispose(), Throws.Nothing);
+ }
+ }
+
[TestCase(true)]
[TestCase(false)]
public void SubmittedLogMessages_ShouldBeInTheLogs(bool useFileTarget)
@@ -35,7 +68,7 @@ public void SubmittedLogMessages_ShouldBeInTheLogs(bool useFileTarget)
var catLogger = LoggingProvider.CreateLogger(typeof(LoggingProviderUnitTests).FullName!);
catLogger.LogInformation("This is an info message.");
- genLogger.LogError(new ArgumentException(), "This is an error message.");
+ genLogger.LogError(new ArgumentException(),"This is an error message.");
genLogger.LogWarning("This is a warning message with structured data. {Data}", new { Key1 = "Value1", Key2 = "Value2" });
var logs = logging.Logs.ToList();
@@ -89,6 +122,34 @@ public void OnlyLatestMaxLogsCount_ShouldBeReturned(bool useFileTarget)
}
}
+ [TestCase(true)]
+ [TestCase(false)]
+ public void DebugModeOnly_IsTrue_ShouldOnlyLogWhenDebugging(bool useFileTarget)
+ {
+ var logFile = useFileTarget
+ ? Path.ChangeExtension(Path.GetTempFileName(), ".log")
+ : null;
+
+ using var logging = logFile is not null
+ ? new TestLoggingProvider(logFile) // Use file target for logging
+ : new TestLoggingProvider(); // Use in-memory target for logging
+
+ var logger = LoggingProvider.CreateLogger();
+ logger.LogInformation("##{Counter}##", 123);
+
+ var logs = logging.Logs.ToList();
+
+ if (System.Diagnostics.Debugger.IsAttached)
+ {
+ Assert.That(logs, Has.Count.EqualTo(1));
+ }
+ else
+ {
+ // If not in debug mode, no logs should be present
+ Assert.That(logs, Is.Empty);
+ }
+ }
+
[Test]
public void MissingFileTargetToReadFrom_ShouldThrow()
{
@@ -126,8 +187,8 @@ public void AllLogLevels_CanBeUsedAsFilters(bool unrecognizedPattern)
DebugModeOnly = false,
Filters = allLevels.Select(l => new Filter
{
- MinLogLevel = l,
- MaxLogLevel = l,
+ MinLogLevel = (Microsoft.Extensions.Logging.LogLevel) l,
+ MaxLogLevel = (Microsoft.Extensions.Logging.LogLevel) l,
LoggerNamePattern = pattern
}).ToList()
};
@@ -138,31 +199,38 @@ public void AllLogLevels_CanBeUsedAsFilters(bool unrecognizedPattern)
var logger = LoggingProvider.CreateLogger();
// Act: log one message for each level
+ var exception = new InvalidOperationException("Test exception");
foreach (var level in allLevels)
switch (level)
{
case LogLevel.Trace:
logger.LogTrace("Trace message");
+ logger.LogTrace(exception, string.Empty);
break;
case LogLevel.Debug:
logger.LogDebug("Debug message");
+ logger.LogDebug(exception, string.Empty);
break;
case LogLevel.Information:
logger.LogInformation("Info message");
+ logger.LogInformation(exception, string.Empty);
break;
case LogLevel.Warning:
logger.LogWarning("Warning message");
+ logger.LogWarning(exception, string.Empty);
break;
case LogLevel.Error:
logger.LogError("Error message");
+ logger.LogError(exception, string.Empty);
break;
case LogLevel.Critical:
logger.LogCritical("Critical message");
+ logger.LogCritical(exception, string.Empty);
break;
}
var logs = logging.Logs.ToList();
- Assert.That(logs, unrecognizedPattern ? Has.Count.EqualTo(6) : Has.Count.EqualTo(0));
+ Assert.That(logs, unrecognizedPattern ? Has.Count.EqualTo(12) : Has.Count.EqualTo(0));
}
[Test]
@@ -175,8 +243,8 @@ public void UsingFilters_ShouldExcludeLogMessages()
[
new Filter
{
- MinLogLevel = LogLevel.Error,
- MaxLogLevel = LogLevel.Error,
+ MinLogLevel = (Microsoft.Extensions.Logging.LogLevel) LogLevel.Error,
+ MaxLogLevel = (Microsoft.Extensions.Logging.LogLevel) LogLevel.Error,
LoggerNamePattern = $"{typeof(LoggingProviderUnitTests).FullName}"
}
]
@@ -217,6 +285,74 @@ public void LoggingForDebugModeOnly_ShouldLog()
: Has.Count.EqualTo(0));
}
+ [Test]
+ public void ConcurrentLogging_ShouldLogAllMessagesCorrectly()
+ {
+ // Set up in-memory logging with (using 1 ILoggerFactory)
+ using var logging = new TestLoggingProvider(new Options { DebugModeOnly = false });
+
+ const int threadCount = 10;
+ const int logsPerThread = 20;
+ var runningThreads = 0;
+ var maxParallelThreads = 0;
+
+ // Create and start multiple threads to log messages concurrently
+
+ var threads = Enumerable.Range(0, threadCount)
+ .Select(t => new Thread(() => LogThread(t)))
+ .ToArray();
+
+ foreach (var thread in threads)
+ thread.Start();
+ foreach (var thread in threads)
+ thread.Join();
+
+ var logs = logging.Logs.ToList();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(logs, Has.Count.EqualTo(threadCount * logsPerThread));
+ Assert.That(maxParallelThreads, Is.GreaterThan(1), "Threads did not run in parallel");
+ for (var t = 0; t < threadCount; t++)
+ {
+ for (var i = 0; i < logsPerThread; i++)
+ {
+ var expected = $"Log {i:D3} - Thread {t:D2}";
+ Assert.That(logs.Any(log => log.Contains(expected)), Is.True, $"Expected log message '{expected}' not found");
+ }
+ }
+ }
+
+ return;
+
+ // Local functions
+
+ void LogThread(int threadNo)
+ {
+ var current = Interlocked.Increment(ref runningThreads);
+ UpdateMaxParallelThreads(current);
+
+ var logger = LoggingProvider.CreateLogger();
+ for (var i = 0; i < logsPerThread; i++)
+ {
+ logger.LogInformation("Log {Log:D3} - Thread {Thread:D2}", i, threadNo);
+ }
+ Interlocked.Decrement(ref runningThreads);
+ }
+
+ void UpdateMaxParallelThreads(int current)
+ {
+ do
+ {
+ var prevMax = maxParallelThreads;
+ if (current > prevMax)
+ {
+ Interlocked.CompareExchange(ref maxParallelThreads, current, prevMax);
+ }
+ } while (current > maxParallelThreads);
+ }
+ }
+
[Test]
public void DemoForOccurrences()
{
diff --git a/Ical.Net.Tests/Logging/TestLoggingProviderBase.cs b/Ical.Net.Tests/Logging/TestLoggingProviderBase.cs
index beb4770d8..fd6494b21 100644
--- a/Ical.Net.Tests/Logging/TestLoggingProviderBase.cs
+++ b/Ical.Net.Tests/Logging/TestLoggingProviderBase.cs
@@ -4,8 +4,8 @@
#nullable enable
using System;
using Ical.Net.Logging;
+using Ical.Net.Tests.Logging.Adapters;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
using NLog.Config;
using NLog.Extensions.Logging;
using NLog.Targets;
@@ -13,7 +13,7 @@
namespace Ical.Net.Tests.Logging;
///
-/// Provides logging operations by configuring and providing a
+/// Provides logging operations by configuring and providing a
/// and a for log storage.
///
///
@@ -40,18 +40,16 @@ protected TestLoggingProviderBase(Target target, Options? options)
{
Options = options ?? new Options();
+ Target = target;
if (Options.DebugModeOnly && !System.Diagnostics.Debugger.IsAttached)
{
- Target = new NullTarget();
- LoggerFactory = NullLoggerFactory.Instance;
- return;
+ Target = new NullTarget("null");
}
-
- Target = target;
- var config = Configure(target, Options);
+ var config = Configure(Target, Options);
LoggerFactory = CreateFactory(config);
// Set the iCal.NET logging configuration
+ // Don't force the logger factory to be set if it already exists!
LoggingProvider.SetLoggerFactory(LoggerFactory);
}
@@ -61,20 +59,21 @@ protected TestLoggingProviderBase(Target target, Options? options)
protected internal Target Target { get; }
///
- /// Gets the instance used for creating logger instances.
+ /// Gets the instance used for creating logger instances.
///
- public ILoggerFactory LoggerFactory { get; protected set; }
+ public Ical.Net.Logging.ILoggerFactory LoggerFactory { get; protected set; }
- private static ILoggerFactory CreateFactory(LoggingConfiguration config)
+ private static Ical.Net.Logging.ILoggerFactory CreateFactory(LoggingConfiguration config)
{
// Create a LoggerFactory with NLog as the provider
- var factory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
+ var msLoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
{
builder.ClearProviders();
builder.AddNLog(config);
});
- return factory;
+ // Wrap the Microsoft logger factory with the Ical adapter
+ return new MicrosoftLoggerFactoryAdapter(msLoggerFactory);
}
private static LoggingConfiguration Configure(Target target, Options options)
@@ -109,7 +108,7 @@ private void Dispose(bool disposing)
Target.Dispose();
LoggerFactory.Dispose();
NLog.LogManager.Configuration = null;
- LoggingProvider.SetLoggerFactory(null);
+ LoggingProvider.SetLoggerFactory(null, true);
}
_isDisposed = true;
@@ -125,17 +124,17 @@ public void Dispose()
///
~TestLoggingProviderBase() => Dispose(disposing: false);
- private static NLog.LogLevel MapLogLevel(LogLevel logLevel)
+ private static NLog.LogLevel MapLogLevel(Microsoft.Extensions.Logging.LogLevel logLevel)
{
#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
return logLevel switch
{
- LogLevel.Trace => NLog.LogLevel.Trace,
- LogLevel.Debug => NLog.LogLevel.Debug,
- LogLevel.Information => NLog.LogLevel.Info,
- LogLevel.Warning => NLog.LogLevel.Warn,
- LogLevel.Error => NLog.LogLevel.Error,
- LogLevel.Critical => NLog.LogLevel.Fatal,
+ Microsoft.Extensions.Logging.LogLevel.Trace => NLog.LogLevel.Trace,
+ Microsoft.Extensions.Logging.LogLevel.Debug => NLog.LogLevel.Debug,
+ Microsoft.Extensions.Logging.LogLevel.Information => NLog.LogLevel.Info,
+ Microsoft.Extensions.Logging.LogLevel.Warning => NLog.LogLevel.Warn,
+ Microsoft.Extensions.Logging.LogLevel.Error => NLog.LogLevel.Error,
+ Microsoft.Extensions.Logging.LogLevel.Critical => NLog.LogLevel.Fatal,
};
#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
}
diff --git a/Ical.Net.Tests/Logging/ToLogExtensions.cs b/Ical.Net.Tests/Logging/ToLogExtensions.cs
index a5e1ccbd1..564134c02 100644
--- a/Ical.Net.Tests/Logging/ToLogExtensions.cs
+++ b/Ical.Net.Tests/Logging/ToLogExtensions.cs
@@ -10,6 +10,7 @@
using Ical.Net.DataTypes;
namespace Ical.Net.Tests.Logging;
+
internal static class ToLogExtensions
{
public static string ToLog(this Exception? exception)
diff --git a/Ical.Net/Ical.Net.csproj b/Ical.Net/Ical.Net.csproj
index 4a4cf3f1a..2d4e5b641 100644
--- a/Ical.Net/Ical.Net.csproj
+++ b/Ical.Net/Ical.Net.csproj
@@ -7,7 +7,6 @@
latest
-
diff --git a/Ical.Net/Logging/ILogger.cs b/Ical.Net/Logging/ILogger.cs
new file mode 100644
index 000000000..fcc5b31d4
--- /dev/null
+++ b/Ical.Net/Logging/ILogger.cs
@@ -0,0 +1,47 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+using System;
+
+namespace Ical.Net.Logging;
+
+///
+/// Represents a logging interface for writing log entries with varying severity levels.
+///
+internal interface ILogger // Make public when logging is used in library classes
+{
+ ///
+ /// Writes a log entry.
+ ///
+ /// The severity level of the log entry.
+ /// The message template. Ex: "Processed {OrderId} in {ElapsedTime}ms".
+ /// The objects to format into the message template.
+ void Log(LogLevel level, string messageTemplate, params object[] args);
+
+ ///
+ /// Writes a log entry that includes an exception.
+ ///
+ /// The severity level of the log entry.
+ /// The exception to log.
+ /// The message template.
+ /// The objects to format into the message template.
+ void Log(LogLevel level, Exception exception, string messageTemplate, params object[] args);
+
+ ///
+ /// Determines whether logging is enabled for the specified log level.
+ ///
+ /// This method can be used to check if a particular log level is enabled before performing logging
+ /// operations. It is useful for optimizing performance by avoiding unnecessary log message generation when logging is
+ /// disabled for the given level.
+ ///
+ ///
+ /// The log level to check. Must be a valid value.
+ ///
+ ///
+ /// if logging is enabled for the specified
+ /// level; otherwise, .
+ ///
+ bool IsEnabled(LogLevel level);
+}
diff --git a/Ical.Net/Logging/ILoggerFactory.cs b/Ical.Net/Logging/ILoggerFactory.cs
new file mode 100644
index 000000000..a5f13b2e0
--- /dev/null
+++ b/Ical.Net/Logging/ILoggerFactory.cs
@@ -0,0 +1,25 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+using System;
+
+namespace Ical.Net.Logging;
+
+///
+/// Represents a factory for creating instances.
+///
+internal interface ILoggerFactory : IDisposable // Make public when logging is used in library classes
+{
+ ///
+ /// Creates a new logger instance for the specified category name.
+ ///
+ ///
+ /// The name of the category for the logger.
+ ///
+ ///
+ /// An instance configured for the specified category name.
+ ///
+ ILogger CreateLogger(string categoryName);
+}
diff --git a/Ical.Net/Logging/ILoggerT.cs b/Ical.Net/Logging/ILoggerT.cs
new file mode 100644
index 000000000..f0444b812
--- /dev/null
+++ b/Ical.Net/Logging/ILoggerT.cs
@@ -0,0 +1,10 @@
+namespace Ical.Net.Logging
+{
+ ///
+ /// Represents a type-specific logger.
+ ///
+ /// The type for which the logger is created.
+ internal interface ILogger : ILogger // Make public when logging is used in library classes
+ {
+ }
+}
diff --git a/Ical.Net/Logging/Internal/Logger.cs b/Ical.Net/Logging/Internal/Logger.cs
new file mode 100644
index 000000000..b977edbbf
--- /dev/null
+++ b/Ical.Net/Logging/Internal/Logger.cs
@@ -0,0 +1,70 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+using System;
+
+namespace Ical.Net.Logging.Internal;
+
+///
+/// Provides logging functionality by delegating to an
+/// underlying instance.
+///
+internal class Logger : ILogger
+{
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class
+ /// using the specified logger factory and category name.
+ ///
+ ///
+ /// The factory used to create the logger instance.
+ ///
+ /// The category name used to identify the logger.
+ ///
+ public Logger(ILoggerFactory loggerFactory, string categoryName)
+ {
+ _logger = loggerFactory.CreateLogger(categoryName);
+ }
+
+ ///
+ /// Determines whether logging is enabled for the specified .
+ ///
+ /// The log level to check.
+ ///
+ /// if logging is enabled for the specified ;
+ /// otherwise, .
+ ///
+ public bool IsEnabled(LogLevel level) => _logger.IsEnabled(level);
+
+ ///
+ /// Logs a message at the specified log level using the provided message template and arguments.
+ ///
+ /// The severity level of the log message. Must be a valid .
+ /// The message template containing placeholders for arguments.
+ /// An array of objects to format into the placeholders in the message template.
+ public void Log(LogLevel level, string messageTemplate, params object[] args)
+ {
+ if (IsEnabled(level))
+ {
+ _logger.Log(level, messageTemplate, args);
+ }
+ }
+
+ ///
+ /// Logs a message at the specified log level using the provided message template and arguments.
+ ///
+ /// The severity level of the log message. Must be a valid .
+ /// The to add to the log.
+ /// The message template containing placeholders for arguments.
+ /// An array of objects to format into the placeholders in the message template.
+ public void Log(LogLevel level, Exception exception, string messageTemplate, params object[] args)
+ {
+ if (IsEnabled(level))
+ {
+ _logger.Log(level, exception, messageTemplate, args);
+ }
+ }
+}
diff --git a/Ical.Net/Logging/Internal/LoggerT.cs b/Ical.Net/Logging/Internal/LoggerT.cs
new file mode 100644
index 000000000..871c05795
--- /dev/null
+++ b/Ical.Net/Logging/Internal/LoggerT.cs
@@ -0,0 +1,20 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+namespace Ical.Net.Logging.Internal;
+
+///
+internal class Logger : Logger
+{
+ ///
+ /// Initializes a new instance of the class
+ /// for the specified type T, using the specified logger factory.
+ ///
+ public Logger(ILoggerFactory loggerFactory)
+ : base(loggerFactory, typeof(T).FullName ?? typeof(T).Name)
+ {
+ // This constructor is used to create a logger for a specific type T.
+ }
+}
diff --git a/Ical.Net/Logging/Internal/NullLogger.cs b/Ical.Net/Logging/Internal/NullLogger.cs
new file mode 100644
index 000000000..03d70ba17
--- /dev/null
+++ b/Ical.Net/Logging/Internal/NullLogger.cs
@@ -0,0 +1,36 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Ical.Net.Logging.Internal;
+
+///
+/// Represents a no-operation implementation of the interface.
+///
+internal class NullLogger : ILogger
+{
+ public static readonly NullLogger Instance = new();
+
+ private NullLogger() { }
+
+ ///
+ [ExcludeFromCodeCoverage]
+ public void Log(LogLevel level, string messageTemplate, params object[] args)
+ {
+ // will never get called because all LogLevels are disabled
+ }
+
+ ///
+ [ExcludeFromCodeCoverage]
+ public void Log(LogLevel level, Exception exception, string messageTemplate, params object[] args)
+ {
+ // will never get called because all LogLevels are disabled
+ }
+
+ ///
+ public bool IsEnabled(LogLevel level) => false;
+}
diff --git a/Ical.Net/Logging/Internal/NullLoggerFactory.cs b/Ical.Net/Logging/Internal/NullLoggerFactory.cs
new file mode 100644
index 000000000..4606436c0
--- /dev/null
+++ b/Ical.Net/Logging/Internal/NullLoggerFactory.cs
@@ -0,0 +1,33 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+namespace Ical.Net.Logging.Internal;
+
+///
+/// Provides a no-operation implementation of that produces
+/// instances. This factory is intended for scenarios where logging is not required or should be disabled.
+///
+internal class NullLoggerFactory : ILoggerFactory
+{
+ ///
+ /// Provides a singleton instance of a that produces no-op loggers.
+ ///
+ public static readonly NullLoggerFactory Instance = new();
+
+ private NullLoggerFactory() { }
+
+ ///
+ /// Creates a .
+ ///
+ /// The parameter is not used.
+ /// A
+ public ILogger CreateLogger(string categoryName) => NullLogger.Instance;
+
+ ///
+ public void Dispose()
+ {
+ // no-op
+ }
+}
diff --git a/Ical.Net/Logging/LogLevel.cs b/Ical.Net/Logging/LogLevel.cs
new file mode 100644
index 000000000..2aefa4c23
--- /dev/null
+++ b/Ical.Net/Logging/LogLevel.cs
@@ -0,0 +1,46 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+namespace Ical.Net.Logging;
+
+///
+/// Specifies the severity level of a log message.
+///
+internal enum LogLevel // Make public when logging is used in library classes
+{
+ ///
+ /// Logs that contain the most detailed messages. These messages may contain sensitive application data.
+ /// These messages are disabled by default and should never be enabled in a production environment.
+ ///
+ Trace,
+ ///
+ /// Logs that are used for interactive investigation during development. These logs should primarily contain
+ /// information useful for debugging and have no long-term value.
+ ///
+ Debug,
+ ///
+ /// Logs that track the general flow of the application. These logs should have long-term value.
+ ///
+ Information,
+ ///
+ /// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the
+ /// application execution to stop.
+ ///
+ Warning,
+ ///
+ /// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a
+ /// failure in the current activity, not an application-wide failure.
+ ///
+ Error,
+ ///
+ /// Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires
+ /// immediate attention.
+ ///
+ Critical,
+ ///
+ /// Not used for writing log messages. Specifies that a logging category should not write any messages.
+ ///
+ None
+}
diff --git a/Ical.Net/Logging/LoggerExtensions.cs b/Ical.Net/Logging/LoggerExtensions.cs
new file mode 100644
index 000000000..a595daaf5
--- /dev/null
+++ b/Ical.Net/Logging/LoggerExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace Ical.Net.Logging;
+
+internal static class LoggerExtensions // Make public when logging is used in library classes
+{
+ public static void LogTrace(this ILogger logger, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Trace, messageTemplate, args);
+
+ public static void LogTrace(this ILogger logger, Exception exception, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Trace, exception, messageTemplate, args);
+
+ public static void LogDebug(this ILogger logger, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Debug, messageTemplate, args);
+
+ public static void LogDebug(this ILogger logger, Exception exception, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Debug, exception, messageTemplate, args);
+
+ public static void LogInformation(this ILogger logger, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Information, messageTemplate, args);
+
+ public static void LogInformation(this ILogger logger, Exception exception, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Information, exception, messageTemplate, args);
+
+ public static void LogWarning(this ILogger logger, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Warning, messageTemplate, args);
+
+ public static void LogWarning(this ILogger logger, Exception exception, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Warning, exception, messageTemplate, args);
+
+ public static void LogError(this ILogger logger, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Error, messageTemplate, args);
+
+ public static void LogError(this ILogger logger, Exception exception, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Error, exception, messageTemplate, args);
+
+ public static void LogCritical(this ILogger logger, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Critical, messageTemplate, args);
+
+ public static void LogCritical(this ILogger logger, Exception exception, string messageTemplate, params object[] args)
+ => logger.Log(LogLevel.Critical, exception, messageTemplate, args);
+}
diff --git a/Ical.Net/Logging/LoggingProvider.cs b/Ical.Net/Logging/LoggingProvider.cs
index bc880f8e7..ae0e84562 100644
--- a/Ical.Net/Logging/LoggingProvider.cs
+++ b/Ical.Net/Logging/LoggingProvider.cs
@@ -3,8 +3,9 @@
// Licensed under the MIT license.
//
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Threading;
+using Ical.Net.Logging.Internal;
namespace Ical.Net.Logging;
@@ -15,34 +16,54 @@ namespace Ical.Net.Logging;
///
internal static class LoggingProvider // Make public when logging is used in library classes
{
+ private static readonly AsyncLocal _loggerFactory =
+ new() { Value = null };
+
+ ///
+ /// Returns if a implementation
+ /// has been set (using ).
+ ///
+ public static bool FactoryIsSet => _loggerFactory.Value is not null;
+
///
/// Sets the global used by Ical.Net for creating instances.
- /// This should be called once at application startup by the consuming application.
- /// If , will be used, resulting in no-op logging.
+ /// This should be called once at application startup by the consuming application or thread that initializes logging.
+ ///
+ /// Before calling this method, or setting a no-op logger factory will be used.
+ ///
+ /// Check to determine if a logger factory has already been set.
///
/// The instance from the consuming application's DI container or direct creation.
- public static void SetLoggerFactory(ILoggerFactory? loggerFactory)
- => LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
+ /// If , an existing will be overwritten; else the method will throw.
+ public static void SetLoggerFactory(ILoggerFactory? loggerFactory, bool force = false)
+ {
+ if (!force && _loggerFactory.Value != null)
+ throw new InvalidOperationException(
+ "LoggerFactory has already been set. To override, set force to true.");
+ _loggerFactory.Value = loggerFactory;
+ }
+
///
/// Gets an instance for the specified category name.
///
/// The category name for the logger (typically the full type name).
/// An instance.
internal static ILogger CreateLogger(string categoryName)
- => LoggerFactory.CreateLogger(categoryName);
+ => (_loggerFactory.Value ?? NullLoggerFactory.Instance).CreateLogger(categoryName);
///
/// Gets an instance for the specified type.
/// The category name will be the full name of the type T.
///
/// The type for which to create the logger.
- /// An instance.
- internal static ILogger CreateLogger()
- => new Logger(LoggerFactory);
+ /// An instance.
+ internal static ILogger CreateLogger()
+ => new Logger(_loggerFactory.Value ?? NullLoggerFactory.Instance);
///
/// Gets the current instance of the logger factory for Ical.Net loggers.
///
- internal static ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance;
+ internal static ILoggerFactory LoggerFactory
+ => _loggerFactory.Value ?? NullLoggerFactory.Instance;
}