commit 6bed9b284cdbd7b7b7d7b08183d900bd9eca7efa Author: Matthias Heil Date: Sat Apr 4 13:30:13 2026 +0200 initial commit diff --git a/CSharp/Applications/AvaloniaLog4NetDI/App.axaml b/CSharp/Applications/AvaloniaLog4NetDI/App.axaml new file mode 100644 index 0000000..fdc349d --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/App.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/App.axaml.cs b/CSharp/Applications/AvaloniaLog4NetDI/App.axaml.cs new file mode 100644 index 0000000..2f4deed --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/App.axaml.cs @@ -0,0 +1,161 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using AvaloniaLog4NetDI.ViewModels; +using AvaloniaLog4NetDI.Views; +using Log4Net.Appender.LogView.Core; +using LogViewer.Avalonia; +using MessageBox.Avalonia; +using MessageBox.Avalonia.Enums; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RandomLogging.Service; +using System; +using System.Reflection; +using System.Threading; +using Icon = MessageBox.Avalonia.Enums.Icon; + +namespace AvaloniaLog4NetDI; + +public partial class App : Application +{ + public override void Initialize() + => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin); + + // catch all unhandled errors + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + + HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + + builder + /* + * Note: For information on launch profiles for debugging, + * see article: https://www.codeproject.com/Articles/5354478/NET-App-Settings-Demystified-Csharp-VB + */ + + // Register the Random Logging Service + .AddRandomBackgroundService() + + // visual debugging tools + .AddLogViewer() + + // Log4Net + .Logging.AddLog4Net(builder.Configuration); + + // uncomment to use custom logging colors (note: System.Drawing namespace) + // + //.Logging.AddLog4Net( + // builder.Configuration, + // options => + //{ + // options.Colors[LogLevel.Trace] = new() + // { + // Foreground = Color.White, + // Background = Color.DarkGray + // }; + + // options.Colors[LogLevel.Debug] = new() + // { + // Foreground = Color.White, + // Background = Color.Gray + // }; + + // options.Colors[LogLevel.Information] = new() + // { + // Foreground = Color.White, + // Background = Color.DodgerBlue + // }; + + // options.Colors[LogLevel.Warning] = new() + // { + // Foreground = Color.White, + // Background = Color.Orchid + // }; + //}); + + IServiceCollection services = builder.Services; + + services + .AddSingleton() + .AddSingleton(service => new MainWindow + { + DataContext = service.GetRequiredService() + }); + + _host = builder.Build(); + _cancellationTokenSource = new(); + + try + { + LogStartingMode(); + + // set and show + desktop.MainWindow = _host.Services.GetRequiredService(); + desktop.ShutdownRequested += OnShutdownRequested; + + // startup background services + _ = _host.StartAsync(_cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + // skip + } + catch (Exception ex) + { + ShowMessageBox("Unhandled Error", ex.Message); + return; + } + } + + base.OnFrameworkInitializationCompleted(); + } + + private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) + => _ = _host!.StopAsync(_cancellationTokenSource!.Token); + + #region Fields + + private IHost? _host; + private CancellationTokenSource? _cancellationTokenSource; + + #endregion + + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) + => ShowMessageBox("Unhandled Error", ((Exception)e.ExceptionObject).Message); + + private void ShowMessageBox(string title, string message) + { + MessageBox.Avalonia.BaseWindows.Base.IMsBoxWindow messageBoxStandardWindow = MessageBoxManager + .GetMessageBoxStandardWindow(title, message, ButtonEnum.Ok, Icon.Stop); + + messageBoxStandardWindow.Show(); + } + + private void LogStartingMode() + { + // Get the Launch mode + bool isDevelopment = string.Equals(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"), "debug", + StringComparison.InvariantCultureIgnoreCase); + + // initialize a logger & EventId + ILogger logger = _host!.Services.GetRequiredService>(); + EventId eventId = new EventId(id: 0, name: Assembly.GetEntryAssembly()!.GetName().Name); + + // log a test pattern for each log level + logger.TestPattern(eventId: eventId); + + // log that we have started... + logger.Emit(eventId, LogLevel.Information, $"Running in {(isDevelopment ? "Debug" : "Release")} mode"); + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/AvaloniaLog4NetDI.csproj b/CSharp/Applications/AvaloniaLog4NetDI/AvaloniaLog4NetDI.csproj new file mode 100644 index 0000000..5e4fb47 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/AvaloniaLog4NetDI.csproj @@ -0,0 +1,43 @@ + + + WinExe + net7.0 + enable + true + app.manifest + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/CSharp/Applications/AvaloniaLog4NetDI/Program.cs b/CSharp/Applications/AvaloniaLog4NetDI/Program.cs new file mode 100644 index 0000000..9e9f474 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace AvaloniaLog4NetDI; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/Properties/launchSettings.json b/CSharp/Applications/AvaloniaLog4NetDI/Properties/launchSettings.json new file mode 100644 index 0000000..ef5f09f --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Staging": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Staging" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/ViewLocator.cs b/CSharp/Applications/AvaloniaLog4NetDI/ViewLocator.cs new file mode 100644 index 0000000..5de3ffe --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/ViewLocator.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using AvaloniaLog4NetDI.ViewModels; +using System; + +namespace AvaloniaLog4NetDI; + +public class ViewLocator : IDataTemplate +{ + public IControl Build(object data) + { + string name = data.GetType().FullName!.Replace("ViewModel", "View"); + Type? type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/ViewModels/MainViewModel.cs b/CSharp/Applications/AvaloniaLog4NetDI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..eeb2e55 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/ViewModels/MainViewModel.cs @@ -0,0 +1,21 @@ +using LogViewer.Core.ViewModels; + +namespace AvaloniaLog4NetDI.ViewModels; + +public class MainViewModel : ViewModelBase +{ + #region Constructor + + public MainViewModel(LogViewerControlViewModel logViewer) + { + LogViewer = logViewer; + } + + #endregion + + #region Properties + + public LogViewerControlViewModel LogViewer { get; } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/ViewModels/ViewModelBase.cs b/CSharp/Applications/AvaloniaLog4NetDI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..e9b5258 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace AvaloniaLog4NetDI.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/Views/MainWindow.axaml b/CSharp/Applications/AvaloniaLog4NetDI/Views/MainWindow.axaml new file mode 100644 index 0000000..b47ca6e --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/Views/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/Views/MainWindow.axaml.cs b/CSharp/Applications/AvaloniaLog4NetDI/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..cdc7e4b --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/Views/MainWindow.axaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace AvaloniaLog4NetDI.Views; + +public partial class MainWindow : Window +{ + public MainWindow() => InitializeComponent(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/app.manifest b/CSharp/Applications/AvaloniaLog4NetDI/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaLog4NetDI/appsettings.Development.json b/CSharp/Applications/AvaloniaLog4NetDI/appsettings.Development.json new file mode 100644 index 0000000..8c1ae04 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/appsettings.Development.json @@ -0,0 +1,35 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + }, + "Log4NetCore": { + "Name": "Log4NetLogViewer_Dev", + "LoggerRepository": "LogViewerRepository", + "OverrideCriticalLevelWith": "Critical", + "Watch": false, + "UseWebOrAppConfig": false, + "PropertyOverrides": [ + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/layout/conversionPattern", + "Attributes": { + "Value": "%date [%thread] %-5level | %logger | %message%newline" + } + }, + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/threshold", + "Attributes": { + "Value": "Trace" + } + }, + { + "XPath": "/log4net/appender[@name='DataStoreLogger']/threshold", + "Attributes": { + "Value": "Trace" + } + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/appsettings.Production.json b/CSharp/Applications/AvaloniaLog4NetDI/appsettings.Production.json new file mode 100644 index 0000000..48d4ad8 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/appsettings.Production.json @@ -0,0 +1,35 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + }, + "Log4NetCore": { + "Name": "Log4NetLogViewer_Prod", + "LoggerRepository": "LogViewerRepository", + "OverrideCriticalLevelWith": "Critical", + "Watch": false, + "UseWebOrAppConfig": false, + "PropertyOverrides": [ + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/layout/conversionPattern", + "Attributes": { + "Value": "%date [%thread] %-5level | %logger | %message%newline" + } + }, + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/threshold", + "Attributes": { + "Value": "Warn" + } + }, + { + "XPath": "/log4net/appender[@name='DataStoreLogger']/threshold", + "Attributes": { + "Value": "Warn" + } + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/appsettings.json b/CSharp/Applications/AvaloniaLog4NetDI/appsettings.json new file mode 100644 index 0000000..3b509e1 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/appsettings.json @@ -0,0 +1,35 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + }, + "Log4NetCore": { + "Name": "Log4NetLogViewer_default", + "LoggerRepository": "LogViewerRepository", + "OverrideCriticalLevelWith": "Critical", + "Watch": false, + "UseWebOrAppConfig": false, + "PropertyOverrides": [ + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/layout/conversionPattern", + "Attributes": { + "Value": "%date [%thread] %-5level | %logger | %message%newline" + } + }, + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/threshold", + "Attributes": { + "Value": "Info" + } + }, + { + "XPath": "/log4net/appender[@name='DataStoreLogger']/threshold", + "Attributes": { + "Value": "Info" + } + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetDI/log4net.config b/CSharp/Applications/AvaloniaLog4NetDI/log4net.config new file mode 100644 index 0000000..dd79167 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetDI/log4net.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/App.axaml b/CSharp/Applications/AvaloniaLog4NetNoDI/App.axaml new file mode 100644 index 0000000..e0a4b17 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/App.axaml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/App.axaml.cs b/CSharp/Applications/AvaloniaLog4NetNoDI/App.axaml.cs new file mode 100644 index 0000000..421a92b --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/App.axaml.cs @@ -0,0 +1,29 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using AvaloniaLog4NetNoDI.Views; + +namespace AvaloniaLog4NetNoDI; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin); + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/AvaloniaLog4NetNoDI.csproj b/CSharp/Applications/AvaloniaLog4NetNoDI/AvaloniaLog4NetNoDI.csproj new file mode 100644 index 0000000..5e10e41 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/AvaloniaLog4NetNoDI.csproj @@ -0,0 +1,41 @@ + + + + WinExe + net7.0 + enable + true + app.manifest + + + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/DataStores/MainControlsDataStore.cs b/CSharp/Applications/AvaloniaLog4NetNoDI/DataStores/MainControlsDataStore.cs new file mode 100644 index 0000000..807d88c --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/DataStores/MainControlsDataStore.cs @@ -0,0 +1,10 @@ +using LogViewer.Core; +using LogDataStore = LogViewer.Avalonia.Logging.LogDataStore; + +namespace AvaloniaLog4NetNoDI.DataStores; + +// Application-wide shared instance of the LogDataStore logging entries +public static class MainControlsDataStore +{ + public static ILogDataStore DataStore { get; } = new LogDataStore(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/Extensions/ServiceExtension.cs b/CSharp/Applications/AvaloniaLog4NetNoDI/Extensions/ServiceExtension.cs new file mode 100644 index 0000000..a20fa4c --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/Extensions/ServiceExtension.cs @@ -0,0 +1,30 @@ +using AvaloniaLog4NetNoDI.DataStores; +using LogViewer.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using Log4Net.Appender.LogView.Core; + +namespace AvaloniaLog4NetNoDI.Extensions; + +public static class ServiceExtension +{ + public static ILoggingBuilder AddLog4NetNoDI(this ILoggingBuilder builder, IConfiguration config) + { + // We need to use a shared instance of the DataStore to pass to the LogViewerControl + builder.Services.AddSingleton(MainControlsDataStore.DataStore); + + // call core Log4Net ServiceExtension initializer + builder.AddLog4Net(config); + + return builder; + } + + public static ILoggingBuilder AddLog4NetNoDI(this ILoggingBuilder builder, IConfiguration config, Action configure) + { + builder.AddLog4NetNoDI(config); + builder.Services.Configure(configure); + return builder; + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/Helpers/LoggingHelper.cs b/CSharp/Applications/AvaloniaLog4NetNoDI/Helpers/LoggingHelper.cs new file mode 100644 index 0000000..4749257 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/Helpers/LoggingHelper.cs @@ -0,0 +1,73 @@ +using System; +using System.Drawing; +using Microsoft.Extensions.Logging; +using AvaloniaLog4NetNoDI.Extensions; +using Microsoft.Extensions.Configuration; +using Common.Core; +using Common.Core.Extensions; + +namespace AvaloniaLog4NetNoDI.Helpers; + +// application-wide DataStoreLogger Factory ... returns a wired up Logger instance +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + // retrieve the log level from 'appsettings' + string value = AppSettings.Current("Logging:LogLevel", "Default") ?? "Information"; + Enum.TryParse(value, out LogLevel logLevel); + + IConfigurationRoot configuration = new ConfigurationBuilder() + .Initialize() + .Build(); + + + // wire up the loggers + Factory = LoggerFactory.Create(builder => builder + + // visual debugging tools + .AddLog4NetNoDI(configuration) + + // uncomment to use custom logging colors (note: System.Drawing namespace) + // + //.AddLog4NetNoDI(configuration, options => + //{ + // options.Colors[LogLevel.Trace] = new() + // { + // Foreground = Color.White, + // Background = Color.DarkGray + // }; + + // options.Colors[LogLevel.Debug] = new() + // { + // Foreground = Color.White, + // Background = Color.Gray + // }; + + // options.Colors[LogLevel.Information] = new() + // { + // Foreground = Color.White, + // Background = Color.DodgerBlue + // }; + + // options.Colors[LogLevel.Warning] = new() + // { + // Foreground = Color.White, + // Background = Color.Orchid + // }; + //}) + + // set minimum log level from 'appsettings*.json' + .SetMinimumLevel(logLevel)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/Program.cs b/CSharp/Applications/AvaloniaLog4NetNoDI/Program.cs new file mode 100644 index 0000000..8b8e99f --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace AvaloniaLog4NetNoDI; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/Properties/launchSettings.json b/CSharp/Applications/AvaloniaLog4NetNoDI/Properties/launchSettings.json new file mode 100644 index 0000000..ef5f09f --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Staging": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Staging" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/Views/MainWindow.axaml b/CSharp/Applications/AvaloniaLog4NetNoDI/Views/MainWindow.axaml new file mode 100644 index 0000000..a872850 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/Views/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/Views/MainWindow.axaml.cs b/CSharp/Applications/AvaloniaLog4NetNoDI/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..43b485c --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/Views/MainWindow.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; +using AvaloniaLog4NetNoDI.DataStores; +using AvaloniaLog4NetNoDI.Helpers; +using LogViewer.Core; +using Microsoft.Extensions.Logging; +using RandomLogging.Service; +using System; +using System.Reflection; +using System.Threading; + +namespace AvaloniaLog4NetNoDI.Views; + +public partial class MainWindow : Window, ILogDataStoreImpl +{ + public MainWindow() + { + InitializeComponent(); + + // Initialize service and pass in the Logger + RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); + + // Get the Launch mode + bool isDevelopment = string.Equals(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"), "debug", + StringComparison.InvariantCultureIgnoreCase); + + // initialize a logger & EventId + Logger logger = new Logger(LoggingHelper.Factory); + EventId eventId = new EventId(id: 0, name: Assembly.GetEntryAssembly()!.GetName().Name); + + // log a test pattern for each log level + logger.TestPattern(eventId: eventId); + + // log that we have started... + logger.Emit(eventId, LogLevel.Information, $"Running in {(isDevelopment ? "Debug" : "Release")} mode"); + + // Start generating log entries + _ = service.StartAsync(CancellationToken.None); + + // manually wire up the logging to the view ... the control will show backlog entries... + DataStore = MainControlsDataStore.DataStore; + + // we can't bind the controls' DataContext to a static object, so assign the DataStore to the Window + // and pass a reference to the Window itself + LogViewerControl.DataContext = this; + } + + public ILogDataStore DataStore { get; init; } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/app.manifest b/CSharp/Applications/AvaloniaLog4NetNoDI/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/appsettings.Development.json b/CSharp/Applications/AvaloniaLog4NetNoDI/appsettings.Development.json new file mode 100644 index 0000000..8c1ae04 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/appsettings.Development.json @@ -0,0 +1,35 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + }, + "Log4NetCore": { + "Name": "Log4NetLogViewer_Dev", + "LoggerRepository": "LogViewerRepository", + "OverrideCriticalLevelWith": "Critical", + "Watch": false, + "UseWebOrAppConfig": false, + "PropertyOverrides": [ + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/layout/conversionPattern", + "Attributes": { + "Value": "%date [%thread] %-5level | %logger | %message%newline" + } + }, + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/threshold", + "Attributes": { + "Value": "Trace" + } + }, + { + "XPath": "/log4net/appender[@name='DataStoreLogger']/threshold", + "Attributes": { + "Value": "Trace" + } + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/appsettings.Production.json b/CSharp/Applications/AvaloniaLog4NetNoDI/appsettings.Production.json new file mode 100644 index 0000000..48d4ad8 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/appsettings.Production.json @@ -0,0 +1,35 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + }, + "Log4NetCore": { + "Name": "Log4NetLogViewer_Prod", + "LoggerRepository": "LogViewerRepository", + "OverrideCriticalLevelWith": "Critical", + "Watch": false, + "UseWebOrAppConfig": false, + "PropertyOverrides": [ + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/layout/conversionPattern", + "Attributes": { + "Value": "%date [%thread] %-5level | %logger | %message%newline" + } + }, + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/threshold", + "Attributes": { + "Value": "Warn" + } + }, + { + "XPath": "/log4net/appender[@name='DataStoreLogger']/threshold", + "Attributes": { + "Value": "Warn" + } + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/appsettings.json b/CSharp/Applications/AvaloniaLog4NetNoDI/appsettings.json new file mode 100644 index 0000000..3b509e1 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/appsettings.json @@ -0,0 +1,35 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + }, + "Log4NetCore": { + "Name": "Log4NetLogViewer_default", + "LoggerRepository": "LogViewerRepository", + "OverrideCriticalLevelWith": "Critical", + "Watch": false, + "UseWebOrAppConfig": false, + "PropertyOverrides": [ + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/layout/conversionPattern", + "Attributes": { + "Value": "%date [%thread] %-5level | %logger | %message%newline" + } + }, + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/threshold", + "Attributes": { + "Value": "Info" + } + }, + { + "XPath": "/log4net/appender[@name='DataStoreLogger']/threshold", + "Attributes": { + "Value": "Info" + } + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLog4NetNoDI/log4net.config b/CSharp/Applications/AvaloniaLog4NetNoDI/log4net.config new file mode 100644 index 0000000..dd79167 --- /dev/null +++ b/CSharp/Applications/AvaloniaLog4NetNoDI/log4net.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/App.axaml b/CSharp/Applications/AvaloniaLoggingDI/App.axaml new file mode 100644 index 0000000..d737483 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/App.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/App.axaml.cs b/CSharp/Applications/AvaloniaLoggingDI/App.axaml.cs new file mode 100644 index 0000000..a54dbc5 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/App.axaml.cs @@ -0,0 +1,160 @@ +using System; +using System.Drawing; +using System.Reflection; +using System.Threading; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using AvaloniaLoggingDI.ViewModels; +using AvaloniaLoggingDI.Views; +using LogViewer.Avalonia; +using MessageBox.Avalonia; +using MessageBox.Avalonia.Enums; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MsLogger.Core; +using RandomLogging.Service; +using Icon = MessageBox.Avalonia.Enums.Icon; + +namespace AvaloniaLoggingDI; + +public partial class App : Application +{ + public override void Initialize() + => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin); + + // catch all unhandled errors + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + + HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + + builder + /* + * Note: For information on launch profiles for debugging, + * see article: https://www.codeproject.com/Articles/5354478/NET-App-Settings-Demystified-Csharp-VB + */ + + // Register the Random Logging Service + .AddRandomBackgroundService() + + // visual debugging tools + .AddLogViewer() + + // Microsoft Logger + //.Logging.AddDefaultDataStoreLogger(); + + // uncomment to use custom logging colors (note: System.Drawing namespace) + // + .Logging.AddDefaultDataStoreLogger(options => + { + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; + }); + + IServiceCollection services = builder.Services; + + services + .AddSingleton() + .AddSingleton(service => new MainWindow + { + DataContext = service.GetRequiredService() + }); + + _host = builder.Build(); + _cancellationTokenSource = new(); + + try + { + LogStartingMode(); + + // set and show + desktop.MainWindow = _host.Services.GetRequiredService(); + desktop.ShutdownRequested += OnShutdownRequested; + + // startup background services + _ = _host.StartAsync(_cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + // skip + } + catch (Exception ex) + { + ShowMessageBox("Unhandled Error", ex.Message); + return; + } + } + + base.OnFrameworkInitializationCompleted(); + } + + private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) + => _ = _host!.StopAsync(_cancellationTokenSource!.Token); + + #region Fields + + private IHost? _host; + private CancellationTokenSource? _cancellationTokenSource; + + #endregion + + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) + => ShowMessageBox("Unhandled Error", ((Exception)e.ExceptionObject).Message); + + private void ShowMessageBox(string title, string message) + { + MessageBox.Avalonia.BaseWindows.Base.IMsBoxWindow messageBoxStandardWindow = MessageBoxManager + .GetMessageBoxStandardWindow(title, message, ButtonEnum.Ok, Icon.Stop); + + messageBoxStandardWindow.Show(); + } + + private void LogStartingMode() + { + // Get the Launch mode + bool isDevelopment = string.Equals(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"), "debug", + StringComparison.InvariantCultureIgnoreCase); + + // initialize a logger & EventId + ILogger logger = _host!.Services.GetRequiredService>(); + EventId eventId = new EventId(id: 0, name: Assembly.GetEntryAssembly()!.GetName().Name); + + // log a test pattern for each log level + logger.TestPattern(eventId: eventId); + + // log that we have started... + logger.Emit(eventId, LogLevel.Information, $"Running in {(isDevelopment ? "Debug" : "Release")} mode"); + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/AvaloniaLoggingDI.csproj b/CSharp/Applications/AvaloniaLoggingDI/AvaloniaLoggingDI.csproj new file mode 100644 index 0000000..4295e30 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/AvaloniaLoggingDI.csproj @@ -0,0 +1,37 @@ + + + WinExe + net7.0 + enable + true + app.manifest + + + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaLoggingDI/Program.cs b/CSharp/Applications/AvaloniaLoggingDI/Program.cs new file mode 100644 index 0000000..a45333a --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace AvaloniaLoggingDI; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/Properties/launchSettings.json b/CSharp/Applications/AvaloniaLoggingDI/Properties/launchSettings.json new file mode 100644 index 0000000..ef5f09f --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Staging": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Staging" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/ViewLocator.cs b/CSharp/Applications/AvaloniaLoggingDI/ViewLocator.cs new file mode 100644 index 0000000..aad5e69 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/ViewLocator.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using AvaloniaLoggingDI.ViewModels; +using System; + +namespace AvaloniaLoggingDI; + +public class ViewLocator : IDataTemplate +{ + public IControl Build(object data) + { + string name = data.GetType().FullName!.Replace("ViewModel", "View"); + Type? type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/ViewModels/MainViewModel.cs b/CSharp/Applications/AvaloniaLoggingDI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..6b0849a --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/ViewModels/MainViewModel.cs @@ -0,0 +1,21 @@ +using LogViewer.Core.ViewModels; + +namespace AvaloniaLoggingDI.ViewModels; + +public class MainViewModel : ViewModelBase +{ + #region Constructor + + public MainViewModel(LogViewerControlViewModel logViewer) + { + LogViewer = logViewer; + } + + #endregion + + #region Properties + + public LogViewerControlViewModel LogViewer { get; } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/ViewModels/ViewModelBase.cs b/CSharp/Applications/AvaloniaLoggingDI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..b13057b --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace AvaloniaLoggingDI.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/Views/MainWindow.axaml b/CSharp/Applications/AvaloniaLoggingDI/Views/MainWindow.axaml new file mode 100644 index 0000000..d5caafa --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/Views/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/Views/MainWindow.axaml.cs b/CSharp/Applications/AvaloniaLoggingDI/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..cbfeaba --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/Views/MainWindow.axaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace AvaloniaLoggingDI.Views; + +public partial class MainWindow : Window +{ + public MainWindow() => InitializeComponent(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/app.manifest b/CSharp/Applications/AvaloniaLoggingDI/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaLoggingDI/appsettings.Development.json b/CSharp/Applications/AvaloniaLoggingDI/appsettings.Development.json new file mode 100644 index 0000000..852dfae --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/appsettings.Production.json b/CSharp/Applications/AvaloniaLoggingDI/appsettings.Production.json new file mode 100644 index 0000000..8936027 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/appsettings.Production.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "System.Net.Http.HttpClient": "Warning" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingDI/appsettings.json b/CSharp/Applications/AvaloniaLoggingDI/appsettings.json new file mode 100644 index 0000000..d122a3e --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingDI/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/App.axaml b/CSharp/Applications/AvaloniaLoggingNoDI/App.axaml new file mode 100644 index 0000000..7c51521 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/App.axaml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/App.axaml.cs b/CSharp/Applications/AvaloniaLoggingNoDI/App.axaml.cs new file mode 100644 index 0000000..fb38bcc --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/App.axaml.cs @@ -0,0 +1,29 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using AvaloniaLoggingNoDI.Views; + +namespace AvaloniaLoggingNoDI; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin); + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/AvaloniaLoggingNoDI.csproj b/CSharp/Applications/AvaloniaLoggingNoDI/AvaloniaLoggingNoDI.csproj new file mode 100644 index 0000000..5c582c8 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/AvaloniaLoggingNoDI.csproj @@ -0,0 +1,39 @@ + + + + WinExe + net7.0 + enable + true + app.manifest + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/DataStores/MainControlsDataStore.cs b/CSharp/Applications/AvaloniaLoggingNoDI/DataStores/MainControlsDataStore.cs new file mode 100644 index 0000000..3d8ab88 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/DataStores/MainControlsDataStore.cs @@ -0,0 +1,10 @@ +using LogViewer.Core; +using LogDataStore = LogViewer.Avalonia.Logging.LogDataStore; + +namespace AvaloniaLoggingNoDI.DataStores; + +// Application-wide shared instance of the LogDataStore logging entries +public static class MainControlsDataStore +{ + public static ILogDataStore DataStore { get; } = new LogDataStore(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/Extensions/LoggerExtension.cs b/CSharp/Applications/AvaloniaLoggingNoDI/Extensions/LoggerExtension.cs new file mode 100644 index 0000000..d67fd0c --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/Extensions/LoggerExtension.cs @@ -0,0 +1,30 @@ +using System; +using LogViewer.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Configuration; +using MsLogger.Core; +using AvaloniaLoggingNoDI.DataStores; + +namespace AvaloniaLoggingNoDI.Extensions; + +public static class LoggerExtension +{ + public static ILoggingBuilder AddDataStoreLogger(this ILoggingBuilder builder) + { + builder.AddConfiguration(); + + // We need to use a shared instance of the DataStore to pass to the LogViewerControl + builder.Services.AddSingleton(MainControlsDataStore.DataStore); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + return builder; + } + + public static ILoggingBuilder AddDataStoreLogger(this ILoggingBuilder builder, Action configure) + { + builder.AddDataStoreLogger(); + builder.Services.Configure(configure); + return builder; + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/Helpers/LoggingHelper.cs b/CSharp/Applications/AvaloniaLoggingNoDI/Helpers/LoggingHelper.cs new file mode 100644 index 0000000..5a7cd26 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/Helpers/LoggingHelper.cs @@ -0,0 +1,78 @@ +using System; +using System.Drawing; +using Common.Core; +using Microsoft.Extensions.Logging; +using AvaloniaLoggingNoDI.Extensions; + +namespace AvaloniaLoggingNoDI.Helpers; + +// application-wide DataStoreLogger Factory ... returns a wired up Logger instance +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + // retrieve the log level from 'appsettings' + string value = AppSettings.Current("Logging:LogLevel", "Default") ?? "Information"; + Enum.TryParse(value, out LogLevel logLevel); + + // wire up the loggers + Factory = LoggerFactory.Create(builder => builder + + // visual debugging tools + //.AddDataStoreLogger() + + // uncomment to use custom logging colors (note: System.Drawing namespace) + // + .AddDataStoreLogger(options => + { + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; + }) + + // examples of adding other loggers... + .AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + }) + + // note: + // * The IDE will automatically add the Debugger Logger, even though not visible + // * Adding the DebugLogger is useful for remote debugging + //.AddDebug() + + // set minimum log level from 'appsettings' + .SetMinimumLevel(logLevel)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/Program.cs b/CSharp/Applications/AvaloniaLoggingNoDI/Program.cs new file mode 100644 index 0000000..9e11908 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace AvaloniaLoggingNoDI; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/Properties/launchSettings.json b/CSharp/Applications/AvaloniaLoggingNoDI/Properties/launchSettings.json new file mode 100644 index 0000000..ef5f09f --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Staging": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Staging" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/Views/MainWindow.axaml b/CSharp/Applications/AvaloniaLoggingNoDI/Views/MainWindow.axaml new file mode 100644 index 0000000..d50e7e2 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/Views/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/Views/MainWindow.axaml.cs b/CSharp/Applications/AvaloniaLoggingNoDI/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..60337bb --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/Views/MainWindow.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; +using AvaloniaLoggingNoDI.DataStores; +using AvaloniaLoggingNoDI.Helpers; +using LogViewer.Core; +using Microsoft.Extensions.Logging; +using RandomLogging.Service; +using System; +using System.Reflection; +using System.Threading; + +namespace AvaloniaLoggingNoDI.Views; + +public partial class MainWindow : Window, ILogDataStoreImpl +{ + public MainWindow() + { + InitializeComponent(); + + // Initialize service and pass in the Logger + RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); + + // Get the Launch mode + bool isDevelopment = string.Equals(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"), "debug", + StringComparison.InvariantCultureIgnoreCase); + + // initialize a logger & EventId + Logger logger = new Logger(LoggingHelper.Factory); + EventId eventId = new EventId(id: 0, name: Assembly.GetEntryAssembly()!.GetName().Name); + + // log a test pattern for each log level + logger.TestPattern(eventId: eventId); + + // log that we have started... + logger.Emit(eventId, LogLevel.Information, $"Running in {(isDevelopment ? "Debug" : "Release")} mode"); + + // Start generating log entries + _ = service.StartAsync(CancellationToken.None); + + // manually wire up the logging to the view ... the control will show backlog entries... + DataStore = MainControlsDataStore.DataStore; + + // we can't bind the controls' DataContext to a static object, so assign the DataStore to the Window + // and pass a reference to the Window itself + LogViewerControl.DataContext = this; + } + + public ILogDataStore DataStore { get; init; } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/app.manifest b/CSharp/Applications/AvaloniaLoggingNoDI/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/appsettings.Development.json b/CSharp/Applications/AvaloniaLoggingNoDI/appsettings.Development.json new file mode 100644 index 0000000..852dfae --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/appsettings.Production.json b/CSharp/Applications/AvaloniaLoggingNoDI/appsettings.Production.json new file mode 100644 index 0000000..8936027 --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/appsettings.Production.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "System.Net.Http.HttpClient": "Warning" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaLoggingNoDI/appsettings.json b/CSharp/Applications/AvaloniaLoggingNoDI/appsettings.json new file mode 100644 index 0000000..d122a3e --- /dev/null +++ b/CSharp/Applications/AvaloniaLoggingNoDI/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/App.axaml b/CSharp/Applications/AvaloniaNlogDI/App.axaml new file mode 100644 index 0000000..0a0b4e2 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/App.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/App.axaml.cs b/CSharp/Applications/AvaloniaNlogDI/App.axaml.cs new file mode 100644 index 0000000..07dc1fb --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/App.axaml.cs @@ -0,0 +1,160 @@ +using System; +using System.Drawing; +using System.Reflection; +using System.Threading; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using AvaloniaNlogDI.ViewModels; +using AvaloniaNlogDI.Views; +using LogViewer.Avalonia; +using MessageBox.Avalonia; +using MessageBox.Avalonia.Enums; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NLog.Target.LogView.Core.Extensions; +using RandomLogging.Service; +using Icon = MessageBox.Avalonia.Enums.Icon; + +namespace AvaloniaNlogDI; + +public partial class App : Application +{ + public override void Initialize() + => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin); + + // catch all unhandled errors + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + + HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + + builder + /* + * Note: For information on launch profiles for debugging, + * see article: https://www.codeproject.com/Articles/5354478/NET-App-Settings-Demystified-Csharp-VB + */ + + // Register the Random Logging Service + .AddRandomBackgroundService() + + // visual debugging tools + .AddLogViewer() + + // Nlog Target + //.Logging.AddNLogTargets(builder.Configuration); + + // uncomment to use custom logging colors (note: System.Drawing namespace) + // + .Logging.AddNLogTargets(builder.Configuration, options => + { + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; + }); + + IServiceCollection services = builder.Services; + + services + .AddSingleton() + .AddSingleton(service => new MainWindow + { + DataContext = service.GetRequiredService() + }); + + _host = builder.Build(); + _cancellationTokenSource = new(); + + try + { + LogStartingMode(); + + // set and show + desktop.MainWindow = _host.Services.GetRequiredService(); + desktop.ShutdownRequested += OnShutdownRequested; + + // startup background services + _ = _host.StartAsync(_cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + // skip + } + catch (Exception ex) + { + ShowMessageBox("Unhandled Error", ex.Message); + return; + } + } + + base.OnFrameworkInitializationCompleted(); + } + + private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) + => _ = _host!.StopAsync(_cancellationTokenSource!.Token); + + #region Fields + + private IHost? _host; + private CancellationTokenSource? _cancellationTokenSource; + + #endregion + + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) + => ShowMessageBox("Unhandled Error", ((Exception)e.ExceptionObject).Message); + + private void ShowMessageBox(string title, string message) + { + MessageBox.Avalonia.BaseWindows.Base.IMsBoxWindow messageBoxStandardWindow = MessageBoxManager + .GetMessageBoxStandardWindow(title, message, ButtonEnum.Ok, Icon.Stop); + + messageBoxStandardWindow.Show(); + } + + private void LogStartingMode() + { + // Get the Launch mode + bool isDevelopment = string.Equals(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"), "debug", + StringComparison.InvariantCultureIgnoreCase); + + // initialize a logger & EventId + ILogger logger = _host!.Services.GetRequiredService>(); + EventId eventId = new EventId(id: 0, name: Assembly.GetEntryAssembly()!.GetName().Name); + + // log a test pattern for each log level + logger.TestPattern(eventId: eventId); + + // log that we have started... + logger.Emit(eventId, LogLevel.Information, $"Running in {(isDevelopment ? "Debug" : "Release")} mode"); + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/AvaloniaNlogDI.csproj b/CSharp/Applications/AvaloniaNlogDI/AvaloniaNlogDI.csproj new file mode 100644 index 0000000..5abd9ca --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/AvaloniaNlogDI.csproj @@ -0,0 +1,37 @@ + + + WinExe + net7.0 + enable + true + app.manifest + + + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaNlogDI/Program.cs b/CSharp/Applications/AvaloniaNlogDI/Program.cs new file mode 100644 index 0000000..b7e42f6 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace AvaloniaNlogDI; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/Properties/launchSettings.json b/CSharp/Applications/AvaloniaNlogDI/Properties/launchSettings.json new file mode 100644 index 0000000..ef5f09f --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Staging": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Staging" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/ViewLocator.cs b/CSharp/Applications/AvaloniaNlogDI/ViewLocator.cs new file mode 100644 index 0000000..8e5a087 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/ViewLocator.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using AvaloniaNlogDI.ViewModels; +using System; + +namespace AvaloniaNlogDI; + +public class ViewLocator : IDataTemplate +{ + public IControl Build(object data) + { + string name = data.GetType().FullName!.Replace("ViewModel", "View"); + Type? type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/ViewModels/MainViewModel.cs b/CSharp/Applications/AvaloniaNlogDI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..647f58f --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/ViewModels/MainViewModel.cs @@ -0,0 +1,21 @@ +using LogViewer.Core.ViewModels; + +namespace AvaloniaNlogDI.ViewModels; + +public class MainViewModel : ViewModelBase +{ + #region Constructor + + public MainViewModel(LogViewerControlViewModel logViewer) + { + LogViewer = logViewer; + } + + #endregion + + #region Properties + + public LogViewerControlViewModel LogViewer { get; } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/ViewModels/ViewModelBase.cs b/CSharp/Applications/AvaloniaNlogDI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..ab2c57a --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace AvaloniaNlogDI.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/Views/MainWindow.axaml b/CSharp/Applications/AvaloniaNlogDI/Views/MainWindow.axaml new file mode 100644 index 0000000..2b4ead5 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/Views/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/Views/MainWindow.axaml.cs b/CSharp/Applications/AvaloniaNlogDI/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..c6c7d9a --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/Views/MainWindow.axaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace AvaloniaNlogDI.Views; + +public partial class MainWindow : Window +{ + public MainWindow() => InitializeComponent(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/app.manifest b/CSharp/Applications/AvaloniaNlogDI/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaNlogDI/appsettings.Development.json b/CSharp/Applications/AvaloniaNlogDI/appsettings.Development.json new file mode 100644 index 0000000..a368fc9 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/appsettings.Development.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + }, + "NLog": { + "throwConfigExceptions": true, + "targets": { + "async": true, + "logconsole": { + "type": "Console", + "layout": "${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" + }, + "DataStoreLogger": { + "type": "DataStoreLogger", + "layout": "${message}" + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Trace", + "writeTo": "logconsole, DataStoreLogger" + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/appsettings.Production.json b/CSharp/Applications/AvaloniaNlogDI/appsettings.Production.json new file mode 100644 index 0000000..0963039 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/appsettings.Production.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "System.Net.Http.HttpClient": "Warning" + } + }, + "NLog": { + "throwConfigExceptions": true, + "targets": { + "async": true, + "logconsole": { + "type": "Console", + "layout": "${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" + }, + "DataStoreLogger": { + "type": "DataStoreLogger", + "layout": "${message}" + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Warn", + "writeTo": "logconsole, DataStoreLogger" + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogDI/appsettings.json b/CSharp/Applications/AvaloniaNlogDI/appsettings.json new file mode 100644 index 0000000..00f8cba --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogDI/appsettings.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + }, + "NLog": { + "throwConfigExceptions": true, + "targets": { + "async": true, + "logconsole": { + "type": "Console", + "layout": "${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" + }, + "DataStoreLogger": { + "type": "DataStoreLogger", + "layout": "${message}" + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Info", + "writeTo": "logconsole, DataStoreLogger" + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/App.axaml b/CSharp/Applications/AvaloniaNlogNoDI/App.axaml new file mode 100644 index 0000000..2c26ed1 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/App.axaml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/App.axaml.cs b/CSharp/Applications/AvaloniaNlogNoDI/App.axaml.cs new file mode 100644 index 0000000..d598ead --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/App.axaml.cs @@ -0,0 +1,29 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using AvaloniaNlogNoDI.Views; + +namespace AvaloniaNlogNoDI; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin); + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/AvaloniaNlogNoDI.csproj b/CSharp/Applications/AvaloniaNlogNoDI/AvaloniaNlogNoDI.csproj new file mode 100644 index 0000000..e130b41 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/AvaloniaNlogNoDI.csproj @@ -0,0 +1,35 @@ + + + + WinExe + net7.0 + enable + true + app.manifest + + + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaNlogNoDI/DataStores/MainControlsDataStore.cs b/CSharp/Applications/AvaloniaNlogNoDI/DataStores/MainControlsDataStore.cs new file mode 100644 index 0000000..ae55c22 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/DataStores/MainControlsDataStore.cs @@ -0,0 +1,10 @@ +using LogViewer.Core; +using LogDataStore = LogViewer.Avalonia.Logging.LogDataStore; + +namespace AvaloniaNlogNoDI.DataStores; + +// Application-wide shared instance of the LogDataStore logging entries +public static class MainControlsDataStore +{ + public static ILogDataStore DataStore { get; } = new LogDataStore(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/Extensions/LoggerExtension.cs b/CSharp/Applications/AvaloniaNlogNoDI/Extensions/LoggerExtension.cs new file mode 100644 index 0000000..9f2c3f7 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/Extensions/LoggerExtension.cs @@ -0,0 +1,30 @@ +using AvaloniaNlogNoDI.DataStores; +using LogViewer.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NLog.Target.LogView.Core.Extensions; +using System; + +namespace AvaloniaNlogNoDI.Extensions; + +public static class ServicesExtension +{ + public static ILoggingBuilder AddNLogTargetsNoDI(this ILoggingBuilder builder, IConfiguration config) + { + // We need to use a shared instance of the DataStore to pass to the LogViewerControl + builder.Services.AddSingleton(MainControlsDataStore.DataStore); + + // call core NLog ServiceExtension initializer + builder.AddNLogTargets(config); + + return builder; + } + + public static ILoggingBuilder AddNLogTargetsNoDI(this ILoggingBuilder builder, IConfiguration config, Action configure) + { + builder.AddNLogTargetsNoDI(config); + builder.Services.Configure(configure); + return builder; + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/Helpers/LoggingHelper.cs b/CSharp/Applications/AvaloniaNlogNoDI/Helpers/LoggingHelper.cs new file mode 100644 index 0000000..3f096a1 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/Helpers/LoggingHelper.cs @@ -0,0 +1,73 @@ +using System; +using System.Drawing; +using Microsoft.Extensions.Logging; +using AvaloniaNlogNoDI.Extensions; +using Microsoft.Extensions.Configuration; +using Common.Core; +using Common.Core.Extensions; + +namespace AvaloniaNlogNoDI.Helpers; + +// application-wide DataStoreLogger Factory ... returns a wired up Logger instance +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + // retrieve the log level from 'appsettings' + string value = AppSettings.Current("Logging:LogLevel", "Default") ?? "Information"; + Enum.TryParse(value, out LogLevel logLevel); + + IConfigurationRoot configuration = new ConfigurationBuilder() + .Initialize() + .Build(); + + + // wire up the loggers + Factory = LoggerFactory.Create(builder => builder + + // visual debugging tools + .AddNLogTargetsNoDI(configuration) + + // uncomment to use custom logging colors (note: System.Drawing namespace) + // + //.AddNLogTargets(configuration, options => + //{ + // options.Colors[LogLevel.Trace] = new() + // { + // Foreground = Color.White, + // Background = Color.DarkGray + // }; + + // options.Colors[LogLevel.Debug] = new() + // { + // Foreground = Color.White, + // Background = Color.Gray + // }; + + // options.Colors[LogLevel.Information] = new() + // { + // Foreground = Color.White, + // Background = Color.DodgerBlue + // }; + + // options.Colors[LogLevel.Warning] = new() + // { + // Foreground = Color.White, + // Background = Color.Orchid + // }; + //}) + + // set minimum log level from 'appsettings*.json' + .SetMinimumLevel(logLevel)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/Program.cs b/CSharp/Applications/AvaloniaNlogNoDI/Program.cs new file mode 100644 index 0000000..4b26956 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace AvaloniaNlogNoDI; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/Properties/launchSettings.json b/CSharp/Applications/AvaloniaNlogNoDI/Properties/launchSettings.json new file mode 100644 index 0000000..ef5f09f --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Staging": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Staging" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/Views/MainWindow.axaml b/CSharp/Applications/AvaloniaNlogNoDI/Views/MainWindow.axaml new file mode 100644 index 0000000..dc8cce0 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/Views/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/Views/MainWindow.axaml.cs b/CSharp/Applications/AvaloniaNlogNoDI/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..9bdb3b9 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/Views/MainWindow.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; +using AvaloniaNlogNoDI.DataStores; +using AvaloniaNlogNoDI.Helpers; +using LogViewer.Core; +using Microsoft.Extensions.Logging; +using RandomLogging.Service; +using System; +using System.Reflection; +using System.Threading; + +namespace AvaloniaNlogNoDI.Views; + +public partial class MainWindow : Window, ILogDataStoreImpl +{ + public MainWindow() + { + InitializeComponent(); + + // Initialize service and pass in the Logger + RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); + + // Get the Launch mode + bool isDevelopment = string.Equals(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"), "debug", + StringComparison.InvariantCultureIgnoreCase); + + // initialize a logger & EventId + Logger logger = new Logger(LoggingHelper.Factory); + EventId eventId = new EventId(id: 0, name: Assembly.GetEntryAssembly()!.GetName().Name); + + // log a test pattern for each log level + logger.TestPattern(eventId: eventId); + + // log that we have started... + logger.Emit(eventId, LogLevel.Information, $"Running in {(isDevelopment ? "Debug" : "Release")} mode"); + + // Start generating log entries + _ = service.StartAsync(CancellationToken.None); + + // manually wire up the logging to the view ... the control will show backlog entries... + DataStore = MainControlsDataStore.DataStore; + + // we can't bind the controls' DataContext to a static object, so assign the DataStore to the Window + // and pass a reference to the Window itself + LogViewerControl.DataContext = this; + } + + public ILogDataStore DataStore { get; init; } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/app.manifest b/CSharp/Applications/AvaloniaNlogNoDI/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaNlogNoDI/appsettings.Development.json b/CSharp/Applications/AvaloniaNlogNoDI/appsettings.Development.json new file mode 100644 index 0000000..a368fc9 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/appsettings.Development.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + }, + "NLog": { + "throwConfigExceptions": true, + "targets": { + "async": true, + "logconsole": { + "type": "Console", + "layout": "${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" + }, + "DataStoreLogger": { + "type": "DataStoreLogger", + "layout": "${message}" + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Trace", + "writeTo": "logconsole, DataStoreLogger" + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/appsettings.Production.json b/CSharp/Applications/AvaloniaNlogNoDI/appsettings.Production.json new file mode 100644 index 0000000..0963039 --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/appsettings.Production.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "System.Net.Http.HttpClient": "Warning" + } + }, + "NLog": { + "throwConfigExceptions": true, + "targets": { + "async": true, + "logconsole": { + "type": "Console", + "layout": "${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" + }, + "DataStoreLogger": { + "type": "DataStoreLogger", + "layout": "${message}" + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Warn", + "writeTo": "logconsole, DataStoreLogger" + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaNlogNoDI/appsettings.json b/CSharp/Applications/AvaloniaNlogNoDI/appsettings.json new file mode 100644 index 0000000..00f8cba --- /dev/null +++ b/CSharp/Applications/AvaloniaNlogNoDI/appsettings.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + }, + "NLog": { + "throwConfigExceptions": true, + "targets": { + "async": true, + "logconsole": { + "type": "Console", + "layout": "${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" + }, + "DataStoreLogger": { + "type": "DataStoreLogger", + "layout": "${message}" + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Info", + "writeTo": "logconsole, DataStoreLogger" + } + ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/App.axaml b/CSharp/Applications/AvaloniaSerilogDI/App.axaml new file mode 100644 index 0000000..8e2e882 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/App.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/App.axaml.cs b/CSharp/Applications/AvaloniaSerilogDI/App.axaml.cs new file mode 100644 index 0000000..5a9c54a --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/App.axaml.cs @@ -0,0 +1,207 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using AvaloniaSerilogDI.ViewModels; +using AvaloniaSerilogDI.Views; +using Common.Core.Extensions; +using LogViewer.Avalonia; +using LogViewer.Core; +using MessageBox.Avalonia; +using MessageBox.Avalonia.Enums; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RandomLogging.Service; +using Serilog; +using Serilog.Sinks.LogView.Core; +using System; +using System.Drawing; +using System.Reflection; +using System.Threading; +using Icon = MessageBox.Avalonia.Enums.Icon; + +namespace AvaloniaSerilogDI; + +public partial class App : Application +{ + #region Constructors + + public override void Initialize() + => AvaloniaXamlLoader.Load(this); + + #endregion + + #region Fields + + private IHost? _host; + private CancellationTokenSource? _cancellationTokenSource; + + #endregion + + #region Methods + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin); + + // catch all unhandled errors + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + + HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + + builder + // Register the Random Logging Service + .AddRandomBackgroundService() + + // visual debugging tools + .AddLogViewer(); + + IServiceCollection services = builder.Services; + + // Serilog Logger + + // Azure: https://devblogs.microsoft.com/dotnet/asp-net-core-logging/ + // ApplicationInsights: https://github.com/serilog-contrib/serilog-sinks-applicationinsights + // AmazonCloudWatch: https://blog.ivankahl.com/logging-dotnet-to-aws-cloudwatch-using-serilog/ + // video: https://www.youtube.com/watch?v=nVAkSBpsuTk (How Structured Logging With Serilog Can Make Your Life Easier) + // video: https://www.youtube.com/watch?v=_iryZxv8Rxw (C# Logging with Serilog and Seq - Structured Logging Made Easy) + // ps: docker run -d --restart unless-stopped --name seq -e ACCEPT_EULA=Y -v c:\WIP\LogData:/data -p 8081:80 datalust/seq:latest + // docker rmi datalust/seq --force + + // ref: https://stackoverflow.com/questions/66304596/how-to-dependency-inject-serilog-into-the-rest-of-my-classes-in-net-console-app + services.AddLogging(configure: cfg => + { + Log.Logger = new LoggerConfiguration() + //Serilog.Core.Logger logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .WriteTo.DataStoreLoggerSink( + dataStoreProvider: () => _host!.Services.TryGetService()! + + //dataStoreProvider: () => _host!.Services.TryGetService()!, + //options => + //{ + // options.Colors[LogLevel.Trace] = new() + // { + // Foreground = Color.White, + // Background = Color.DarkGray + // }; + + // options.Colors[LogLevel.Debug] = new() + // { + // Foreground = Color.White, + // Background = Color.Gray + // }; + + // options.Colors[LogLevel.Information] = new() + // { + // Foreground = Color.White, + // Background = Color.DodgerBlue + // }; + + // options.Colors[LogLevel.Warning] = new() + // { + // Foreground = Color.White, + // Background = Color.Orchid + // }; + //} + ) + .CreateLogger(); + + cfg.ClearProviders() + .AddSerilog(Log.Logger); + }); + + services + .AddSingleton() + .AddSingleton(service => new MainWindow + { + DataContext = service.GetRequiredService() + }); + + _host = builder.Build(); + _cancellationTokenSource = new(); + + try + { + LogStartingMode(); + + // set and show + desktop.MainWindow = _host.Services.GetRequiredService(); + desktop.ShutdownRequested += OnShutdownRequested; + + // startup background services + _ = _host.StartAsync(_cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + // skip + } + catch (Exception ex) + { + Log.Fatal(ex, "Application terminated unexpectedly"); + + ShowMessageBox("Unhandled Error", ex.Message); + + CleanUp(); + return; + } + } + + base.OnFrameworkInitializationCompleted(); + } + + private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) + => CleanUp(); + + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + Exception exception = (Exception)e.ExceptionObject; + + Log.Fatal(exception, "Application terminated unexpectedly"); + ShowMessageBox("Unhandled Error", exception.Message); + + CleanUp(); + } + + private void ShowMessageBox(string title, string message) + { + MessageBox.Avalonia.BaseWindows.Base.IMsBoxWindow messageBoxStandardWindow = MessageBoxManager + .GetMessageBoxStandardWindow(title, message, ButtonEnum.Ok, Icon.Stop); + + messageBoxStandardWindow.Show(); + } + + private void LogStartingMode() + { + // Get the Launch mode + bool isDevelopment = string.Equals(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"), "debug", + StringComparison.InvariantCultureIgnoreCase); + + // initialize a logger & EventId + ILogger logger = _host!.Services.GetRequiredService>(); + EventId eventId = new EventId(id: 0, name: Assembly.GetEntryAssembly()!.GetName().Name); + + // log a test pattern for each log level + logger.TestPattern(eventId: eventId); + + // log that we have started... + logger.Emit(eventId, LogLevel.Information, $"Running in {(isDevelopment ? "Debug" : "Release")} mode"); + } + + private void CleanUp() + { + // tell the background services that we are shutting down + _ = _host!.StopAsync(_cancellationTokenSource!.Token); + + // flush logs + Log.CloseAndFlush(); + } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/AvaloniaSerilogDI.csproj b/CSharp/Applications/AvaloniaSerilogDI/AvaloniaSerilogDI.csproj new file mode 100644 index 0000000..0acebe5 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/AvaloniaSerilogDI.csproj @@ -0,0 +1,49 @@ + + + WinExe + net7.0 + enable + true + app.manifest + + + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaSerilogDI/Program.cs b/CSharp/Applications/AvaloniaSerilogDI/Program.cs new file mode 100644 index 0000000..96b242a --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/Program.cs @@ -0,0 +1,20 @@ +using Avalonia; +using System; + +namespace AvaloniaSerilogDI; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/Properties/launchSettings.json b/CSharp/Applications/AvaloniaSerilogDI/Properties/launchSettings.json new file mode 100644 index 0000000..ef5f09f --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Staging": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Staging" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/ViewLocator.cs b/CSharp/Applications/AvaloniaSerilogDI/ViewLocator.cs new file mode 100644 index 0000000..0ad3c39 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/ViewLocator.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using AvaloniaSerilogDI.ViewModels; +using System; + +namespace AvaloniaSerilogDI; + +public class ViewLocator : IDataTemplate +{ + public IControl Build(object data) + { + string name = data.GetType().FullName!.Replace("ViewModel", "View"); + Type? type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/ViewModels/MainViewModel.cs b/CSharp/Applications/AvaloniaSerilogDI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..5f6a9ae --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/ViewModels/MainViewModel.cs @@ -0,0 +1,19 @@ +using LogViewer.Core.ViewModels; + +namespace AvaloniaSerilogDI.ViewModels; + +public class MainViewModel : ViewModelBase +{ + #region Constructor + + public MainViewModel(LogViewerControlViewModel logViewer) + => LogViewer = logViewer; + + #endregion + + #region Properties + + public LogViewerControlViewModel LogViewer { get; } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/ViewModels/ViewModelBase.cs b/CSharp/Applications/AvaloniaSerilogDI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..db5a028 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace AvaloniaSerilogDI.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/Views/MainWindow.axaml b/CSharp/Applications/AvaloniaSerilogDI/Views/MainWindow.axaml new file mode 100644 index 0000000..aebc5d1 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/Views/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/Views/MainWindow.axaml.cs b/CSharp/Applications/AvaloniaSerilogDI/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..6213239 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/Views/MainWindow.axaml.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace AvaloniaSerilogDI.Views; + +public partial class MainWindow : Window +{ + public MainWindow() => InitializeComponent(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/app.manifest b/CSharp/Applications/AvaloniaSerilogDI/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaSerilogDI/appsettings.Development.json b/CSharp/Applications/AvaloniaSerilogDI/appsettings.Development.json new file mode 100644 index 0000000..3d89069 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/appsettings.Development.json @@ -0,0 +1,45 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + }, + "Serilog": { + "Using": [ "Serilog.Sinks.File" ], + "LevelSwitches": { "controlSwitch": "Verbose" }, + "MinimumLevel": { + "Default": "Verbose", + "Override": { + "Microsoft": "Verbose" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {EventId} | {Message:lj} {NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.txt", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "outputTemplate": "{Timestamp:G} {Message}{NewLine:1}{Exception:1}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.json", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "formatter": "Serilog.Formatting.Json.JsonFormatter" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/appsettings.Production.json b/CSharp/Applications/AvaloniaSerilogDI/appsettings.Production.json new file mode 100644 index 0000000..a85adcc --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/appsettings.Production.json @@ -0,0 +1,46 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "System.Net.Http.HttpClient": "Warning" + } + }, + "Serilog": { + "Using": [ "Serilog.Sinks.File" ], + "LevelSwitches": { "controlSwitch": "Warning" }, + "MinimumLevel": { + "Default": "Warning", + "Override": { + "Microsoft": "Warning" + } + }, + + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {EventId.Name} | {Message:lj} {NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.txt", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "outputTemplate": "{Timestamp:G} {Message}{NewLine:1}{Exception:1}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.json", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "formatter": "Serilog.Formatting.Json.JsonFormatter" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogDI/appsettings.json b/CSharp/Applications/AvaloniaSerilogDI/appsettings.json new file mode 100644 index 0000000..08d0a44 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogDI/appsettings.json @@ -0,0 +1,46 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + }, + "Serilog": { + "Using": [ "Serilog.Sinks.File" ], + "LevelSwitches": { "controlSwitch": "Information" }, + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Information" + } + }, + + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {EventId.Name} | {Message:lj} {NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.txt", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "outputTemplate": "{Timestamp:G} {Message}{NewLine:1}{Exception:1}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.json", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "formatter": "Serilog.Formatting.Json.JsonFormatter" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/App.axaml b/CSharp/Applications/AvaloniaSerilogNoDI/App.axaml new file mode 100644 index 0000000..37fa077 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/App.axaml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/App.axaml.cs b/CSharp/Applications/AvaloniaSerilogNoDI/App.axaml.cs new file mode 100644 index 0000000..277e145 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/App.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace AvaloniaSerilogNoDI; + +public partial class App : Application +{ + public override void Initialize() + => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow(); + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/AvaloniaSerilogNoDI.csproj b/CSharp/Applications/AvaloniaSerilogNoDI/AvaloniaSerilogNoDI.csproj new file mode 100644 index 0000000..52bdddd --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/AvaloniaSerilogNoDI.csproj @@ -0,0 +1,48 @@ + + + WinExe + net7.0 + enable + true + app.manifest + + + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/DataStores/MainControlsDataStore.cs b/CSharp/Applications/AvaloniaSerilogNoDI/DataStores/MainControlsDataStore.cs new file mode 100644 index 0000000..5ea1ac2 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/DataStores/MainControlsDataStore.cs @@ -0,0 +1,10 @@ +using LogViewer.Core; +using LogDataStore = LogViewer.Avalonia.Logging.LogDataStore; + +namespace AvaloniaSerilogNoDI.DataStores; + +// Application-wide shared instance of the LogDataStore logging entries +public static class MainControlsDataStore +{ + public static ILogDataStore DataStore { get; } = new LogDataStore(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/Helpers/LoggingHelper.cs b/CSharp/Applications/AvaloniaSerilogNoDI/Helpers/LoggingHelper.cs new file mode 100644 index 0000000..3fc8514 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/Helpers/LoggingHelper.cs @@ -0,0 +1,75 @@ +using AvaloniaSerilogNoDI.DataStores; +using Common.Core.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Sinks.LogView.Core; +using System.Drawing; + +namespace AvaloniaSerilogNoDI.Helpers; + +// application-wide DataStoreLogger Factory ... returns a wired up Logger instance +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + IConfigurationRoot configuration = new ConfigurationBuilder() + .Initialize() + .Build(); + + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .WriteTo.DataStoreLoggerSink( + //dataStoreProvider: () => MainControlsDataStore.DataStore + + dataStoreProvider: () => MainControlsDataStore.DataStore, + options => + { + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; + } + ) + .CreateLogger(); + + // wire up the loggers + Factory = LoggerFactory.Create(loggingBuilder => loggingBuilder.AddSerilog(Log.Logger)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion + + #region Methods + + public static void CloseAndFlush() + => Log.CloseAndFlush(); + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/MainWindow.axaml b/CSharp/Applications/AvaloniaSerilogNoDI/MainWindow.axaml new file mode 100644 index 0000000..476d515 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/MainWindow.axaml.cs b/CSharp/Applications/AvaloniaSerilogNoDI/MainWindow.axaml.cs new file mode 100644 index 0000000..656dcfc --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/MainWindow.axaml.cs @@ -0,0 +1,79 @@ +using Avalonia.Controls; +using AvaloniaSerilogNoDI.DataStores; +using AvaloniaSerilogNoDI.Helpers; +using LogViewer.Core; +using Microsoft.Extensions.Logging; +using RandomLogging.Service; +using System; +using System.ComponentModel; +using System.Reflection; +using System.Threading; + +namespace AvaloniaSerilogNoDI; + +public partial class MainWindow : Window, ILogDataStoreImpl +{ + #region Constructors + + public MainWindow() + { + InitializeComponent(); + + // Initialize _service and pass in the Logger + _service = new(new Logger(LoggingHelper.Factory)); + + // Get the Launch mode + bool isDevelopment = string.Equals(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"), "debug", + StringComparison.InvariantCultureIgnoreCase); + + // initialize a logger & EventId + Logger logger = new Logger(LoggingHelper.Factory); + EventId eventId = new EventId(id: 0, name: Assembly.GetEntryAssembly()!.GetName().Name); + + // log a test pattern for each log level + logger.TestPattern(eventId: eventId); + + // log that we have started... + logger.Emit(eventId, LogLevel.Information, $"Running in {(isDevelopment ? "Debug" : "Release")} mode"); + + // Start generating log entries + _ = _service.StartAsync(CancellationToken.None); + + // manually wire up the logging to the view ... the control will show backlog entries... + DataStore = MainControlsDataStore.DataStore; + + // we can't bind the controls' DataContext to a static object, so assign the DataStore to the Window + // and pass a reference to the Window itself + LogViewerControl.DataContext = this; + + // Listen for when the app is closing + Window.Closing += OnClosing; + } + + #endregion + + #region Fields + + private readonly RandomLoggingService? _service; + + #endregion + + #region Properties + + public ILogDataStore DataStore { get; init; } + + #endregion + + #region Methods + + // flush logs and clean up + private void OnClosing(object? sender, CancelEventArgs e) + { + Window.Closing -= OnClosing; + + _ = _service?.StopAsync(); + LoggingHelper.CloseAndFlush(); + } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/Program.cs b/CSharp/Applications/AvaloniaSerilogNoDI/Program.cs new file mode 100644 index 0000000..c1b0108 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/Program.cs @@ -0,0 +1,20 @@ +using Avalonia; +using System; + +namespace AvaloniaSerilogNoDI; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/Properties/launchSettings.json b/CSharp/Applications/AvaloniaSerilogNoDI/Properties/launchSettings.json new file mode 100644 index 0000000..ef5f09f --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + }, + "Staging": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Staging" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/app.manifest b/CSharp/Applications/AvaloniaSerilogNoDI/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/appsettings.Development.json b/CSharp/Applications/AvaloniaSerilogNoDI/appsettings.Development.json new file mode 100644 index 0000000..3d89069 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/appsettings.Development.json @@ -0,0 +1,45 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + }, + "Serilog": { + "Using": [ "Serilog.Sinks.File" ], + "LevelSwitches": { "controlSwitch": "Verbose" }, + "MinimumLevel": { + "Default": "Verbose", + "Override": { + "Microsoft": "Verbose" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {EventId} | {Message:lj} {NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.txt", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "outputTemplate": "{Timestamp:G} {Message}{NewLine:1}{Exception:1}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.json", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "formatter": "Serilog.Formatting.Json.JsonFormatter" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/appsettings.Production.json b/CSharp/Applications/AvaloniaSerilogNoDI/appsettings.Production.json new file mode 100644 index 0000000..a85adcc --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/appsettings.Production.json @@ -0,0 +1,46 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "System.Net.Http.HttpClient": "Warning" + } + }, + "Serilog": { + "Using": [ "Serilog.Sinks.File" ], + "LevelSwitches": { "controlSwitch": "Warning" }, + "MinimumLevel": { + "Default": "Warning", + "Override": { + "Microsoft": "Warning" + } + }, + + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {EventId.Name} | {Message:lj} {NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.txt", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "outputTemplate": "{Timestamp:G} {Message}{NewLine:1}{Exception:1}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.json", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "formatter": "Serilog.Formatting.Json.JsonFormatter" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ] + } +} \ No newline at end of file diff --git a/CSharp/Applications/AvaloniaSerilogNoDI/appsettings.json b/CSharp/Applications/AvaloniaSerilogNoDI/appsettings.json new file mode 100644 index 0000000..08d0a44 --- /dev/null +++ b/CSharp/Applications/AvaloniaSerilogNoDI/appsettings.json @@ -0,0 +1,46 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + }, + "Serilog": { + "Using": [ "Serilog.Sinks.File" ], + "LevelSwitches": { "controlSwitch": "Information" }, + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Information" + } + }, + + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {EventId.Name} | {Message:lj} {NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.txt", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "outputTemplate": "{Timestamp:G} {Message}{NewLine:1}{Exception:1}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.json", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "formatter": "Serilog.Formatting.Json.JsonFormatter" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ] + } +} \ No newline at end of file diff --git a/CSharp/Background Services/RandomLogging.Service/Extensions/ServicesExtension.cs b/CSharp/Background Services/RandomLogging.Service/Extensions/ServicesExtension.cs new file mode 100644 index 0000000..b630807 --- /dev/null +++ b/CSharp/Background Services/RandomLogging.Service/Extensions/ServicesExtension.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace RandomLogging.Service; + +public static class ServicesExtension +{ + public static HostApplicationBuilder AddRandomBackgroundService(this HostApplicationBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddHostedService(service => service.GetRequiredService()); + + return builder; + } +} \ No newline at end of file diff --git a/CSharp/Background Services/RandomLogging.Service/RandomLogging.Service.csproj b/CSharp/Background Services/RandomLogging.Service/RandomLogging.Service.csproj new file mode 100644 index 0000000..9d0cb97 --- /dev/null +++ b/CSharp/Background Services/RandomLogging.Service/RandomLogging.Service.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/CSharp/Background Services/RandomLogging.Service/RandomLoggingService.cs b/CSharp/Background Services/RandomLogging.Service/RandomLoggingService.cs new file mode 100644 index 0000000..a12787c --- /dev/null +++ b/CSharp/Background Services/RandomLogging.Service/RandomLoggingService.cs @@ -0,0 +1,180 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace RandomLogging.Service; + +public class RandomLoggingService : BackgroundService +{ + #region Constructors + + public RandomLoggingService(ILogger logger) + => _logger = logger; + + #endregion + + #region Fields + + #region Injected + + private readonly ILogger _logger; + + #endregion + + // ChatGPT generated lists + + private readonly List _messages = new() + { + "Bringing your virtual world to life!", + "Preparing a new world of adventure for you.", + "Calculating the ideal balance of work and play.", + "Generating endless possibilities for you to explore.", + "Crafting the perfect balance of life and love.", + "Assembling a world of endless exploration.", + "Bringing your imagination to life one pixel at a time.", + "Creating a world of endless creativity and inspiration.", + "Designing the ultimate dream home for you to live in.", + "Preparing for the ultimate life simulation experience.", + "Loading up your personalized world of dreams and aspirations.", + "Building a new neighborhood full of excitement and adventure.", + "Creating a world full of surprise and wonder.", + "Generating the ultimate adventure for you to embark on.", + "Assembling a community full of life and energy.", + "Crafting the perfect balance of laughter and joy.", + "Bringing your digital world to life with endless possibilities.", + "Calculating the perfect formula for happiness and success.", + "Generating a world of endless imagination and creativity.", + "Designing a world that's truly one-of-a-kind for you." + }; + + readonly List _eventNames = new() + { + "OnButtonClicked", + "OnMenuItemSelected", + "OnWindowResized", + "OnDataLoaded", + "OnFormSubmitted", + "OnTabChanged", + "OnItemSelected", + "OnValidationFailed", + "OnNotificationReceived", + "OnApplicationStarted", + "OnUserLoggedIn", + "OnUploadStarted", + "OnDownloadCompleted", + "OnProgressUpdated", + "OnNetworkErrorOccurred", + "OnPaymentSuccessful", + "OnProfileUpdated", + "OnSearchCompleted", + "OnFilterChanged", + "OnLanguageChanged" + }; + + readonly List _errorMessages = new() + { + "Error: Could not connect to the server. Please check your internet connection.", + "Warning: Your computer's operating system is not compatible with this software.", + "Error: Insufficient memory. Please close other programs and try again.", + "Warning: Your graphics card drivers may be outdated. Please update them before playing.", + "Error: The installation file is corrupt. Please download a new copy.", + "Warning: Your computer may be running too hot. Please check the temperature and cooling system.", + "Error: The required DirectX version is not installed on your computer.", + "Warning: Your sound card may not be supported. Please check the system requirements.", + "Error: The installation directory is full. Please free up space and try again.", + "Warning: Your computer's power supply may not be sufficient. Please check the requirements.", + "Error: The installation process was interrupted. Please restart the setup.", + "Warning: Your antivirus software may interfere with the game. Please add it to the exception list.", + "Error: The required Microsoft library is not installed.", + "Warning: Your input devices may not be compatible. Please check the system requirements.", + "Error: The installation process failed. Please contact support for assistance.", + "Warning: Your network speed may cause lag and disconnections.", + "Error: The setup file is not compatible with your operating system.", + "Warning: Your computer's resolution may cause display issues.", + "Error: The required Microsoft .NET Framework is not installed on your computer.", + "Warning: Your keyboard layout may cause input errors. Please check the settings." + }; + + private readonly Random _random = new(); + private static readonly EventId EventId = new(id: 0x1A4, name: "RandomLoggingService"); + + #endregion + + #region BackgroundService + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.Emit(EventId, LogLevel.Information, "Started"); + + while (!stoppingToken.IsCancellationRequested) + { + // wait for a pre-determined interval + await Task.Delay(1000, stoppingToken).ConfigureAwait(false); + + if (stoppingToken.IsCancellationRequested) + return; + + // heartbeat logging + GenerateLogEntry(); + } + + _logger.Emit(EventId, LogLevel.Information, "Stopped"); + } + + public Task StartAsync() + => StartAsync(CancellationToken.None); + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await Task.Yield(); + + _logger.Emit(EventId, LogLevel.Information, "Starting"); + + await base.StartAsync(cancellationToken).ConfigureAwait(false); + } + + public Task StopAsync() + => StopAsync(CancellationToken.None); + + public override async Task StopAsync(CancellationToken cancellationToken) + { + _logger.Emit(EventId, LogLevel.Information, "Stopping"); + await base.StopAsync(cancellationToken).ConfigureAwait(false); + } + + #endregion + + #region Methods + + private void GenerateLogEntry() + { + LogLevel level = _random.Next(0, 100) switch + { + < 50 => LogLevel.Information, + < 65 => LogLevel.Debug, + < 75 => LogLevel.Trace, + < 85 => LogLevel.Warning, + < 95 => LogLevel.Error, + _ => LogLevel.Critical + }; + + if (level < LogLevel.Error) + { + _logger.Emit(GenerateEventId(), level, GetMessage()); + return; + } + + _logger.Emit(GenerateEventId(), level, GetMessage(), + new Exception(_errorMessages[_random.Next(0, _errorMessages.Count)])); + } + + private EventId GenerateEventId() + { + int index = _random.Next(0, _eventNames.Count); + return new EventId(id: 0x1A4 + index, name: _eventNames[index]); + } + + private string GetMessage() + => _messages[_random.Next(0, _messages.Count)]; + + #endregion +} \ No newline at end of file diff --git a/CSharp/Controls/LogViewer.Avalonia/Converters/ChangeColorTypeConverter.cs b/CSharp/Controls/LogViewer.Avalonia/Converters/ChangeColorTypeConverter.cs new file mode 100644 index 0000000..ef55e9f --- /dev/null +++ b/CSharp/Controls/LogViewer.Avalonia/Converters/ChangeColorTypeConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using SysDrawColor = System.Drawing.Color; + +namespace LogViewer.Avalonia.Converters; + +public class ChangeColorTypeConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) + return new SolidColorBrush((Color)(parameter ?? Colors.Black)); + + SysDrawColor sysDrawColor = (SysDrawColor)value!; + return new SolidColorBrush(Color.FromArgb( + sysDrawColor.A, + sysDrawColor.R, + sysDrawColor.G, + sysDrawColor.B)); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotImplementedException(); +} diff --git a/CSharp/Controls/LogViewer.Avalonia/Converters/EventIdConverter.cs b/CSharp/Controls/LogViewer.Avalonia/Converters/EventIdConverter.cs new file mode 100644 index 0000000..a6ef93e --- /dev/null +++ b/CSharp/Controls/LogViewer.Avalonia/Converters/EventIdConverter.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using Avalonia.Data.Converters; +using Microsoft.Extensions.Logging; + +namespace LogViewer.Avalonia.Converters; + +public class EventIdConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) + return "0"; + + EventId eventId = (EventId)value; + + return eventId.ToString(); + } + + // If not implemented, an error is thrown + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => new EventId(0, value?.ToString() ?? string.Empty); +} \ No newline at end of file diff --git a/CSharp/Controls/LogViewer.Avalonia/Extensions/ServicesExtension.cs b/CSharp/Controls/LogViewer.Avalonia/Extensions/ServicesExtension.cs new file mode 100644 index 0000000..ee9d09b --- /dev/null +++ b/CSharp/Controls/LogViewer.Avalonia/Extensions/ServicesExtension.cs @@ -0,0 +1,18 @@ +using LogViewer.Core; +using LogViewer.Core.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using LogDataStore = LogViewer.Avalonia.Logging.LogDataStore; + +namespace LogViewer.Avalonia; + +public static class ServicesExtension +{ + public static HostApplicationBuilder AddLogViewer(this HostApplicationBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder; + } +} \ No newline at end of file diff --git a/CSharp/Controls/LogViewer.Avalonia/LogViewer.Avalonia.csproj b/CSharp/Controls/LogViewer.Avalonia/LogViewer.Avalonia.csproj new file mode 100644 index 0000000..6dc5021 --- /dev/null +++ b/CSharp/Controls/LogViewer.Avalonia/LogViewer.Avalonia.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + + + %(Filename) + + + + diff --git a/CSharp/Controls/LogViewer.Avalonia/LogViewerControl.axaml b/CSharp/Controls/LogViewer.Avalonia/LogViewerControl.axaml new file mode 100644 index 0000000..361cfdc --- /dev/null +++ b/CSharp/Controls/LogViewer.Avalonia/LogViewerControl.axaml @@ -0,0 +1,58 @@ + + + + + + + + + + + Black + Transparent + + + + + + + + + + + + + + + + + + + + diff --git a/CSharp/Controls/LogViewer.Avalonia/LogViewerControl.axaml.cs b/CSharp/Controls/LogViewer.Avalonia/LogViewerControl.axaml.cs new file mode 100644 index 0000000..bcfb3e3 --- /dev/null +++ b/CSharp/Controls/LogViewer.Avalonia/LogViewerControl.axaml.cs @@ -0,0 +1,42 @@ +using System.Collections.Specialized; +using Avalonia.Controls; +using Avalonia.LogicalTree; +using LogViewer.Core; + +namespace LogViewer.Avalonia; + +public partial class LogViewerControl : UserControl +{ + public LogViewerControl() + => InitializeComponent(); + + private ILogDataStoreImpl? vm; + private LogModel? item; + + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (DataContext is null) + return; + + vm = (ILogDataStoreImpl)DataContext; + vm.DataStore.Entries.CollectionChanged += OnCollectionChanged; + } + + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + => item = MyDataGrid.Items.Cast().LastOrDefault(); + + private void OnLayoutUpdated(object? sender, EventArgs e) + { + if (CanAutoScroll.IsChecked != true || item is null) + return; + + MyDataGrid.ScrollIntoView(item, null); + item = null; + } + + private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e) + { + if (vm is null) return; + vm.DataStore.Entries.CollectionChanged -= OnCollectionChanged; + } +} \ No newline at end of file diff --git a/CSharp/Controls/LogViewer.Avalonia/Logging/LogDataStore.cs b/CSharp/Controls/LogViewer.Avalonia/Logging/LogDataStore.cs new file mode 100644 index 0000000..5898fea --- /dev/null +++ b/CSharp/Controls/LogViewer.Avalonia/Logging/LogDataStore.cs @@ -0,0 +1,13 @@ +using Avalonia.Threading; + +namespace LogViewer.Avalonia.Logging; + +public class LogDataStore : Core.LogDataStore +{ + #region Methods + + public override async void AddEntry(Core.LogModel logModel) + => await Dispatcher.UIThread.InvokeAsync(() => base.AddEntry(logModel)); + + #endregion +} \ No newline at end of file diff --git a/CSharp/Core/Common.Core/Common.Core.csproj b/CSharp/Core/Common.Core/Common.Core.csproj new file mode 100644 index 0000000..6bcf4d4 --- /dev/null +++ b/CSharp/Core/Common.Core/Common.Core.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/CSharp/Core/Common.Core/Extensions/ConfigurationExtension.cs b/CSharp/Core/Common.Core/Extensions/ConfigurationExtension.cs new file mode 100644 index 0000000..cbfcd80 --- /dev/null +++ b/CSharp/Core/Common.Core/Extensions/ConfigurationExtension.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Configuration; + +namespace Common.Core.Extensions; + +public static class ConfigurationExtension +{ + public static IConfigurationBuilder Initialize(this IConfigurationBuilder builder) + { + string env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"; + + return builder + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env}.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + } +} \ No newline at end of file diff --git a/CSharp/Core/Common.Core/Extensions/ServicesExtension.cs b/CSharp/Core/Common.Core/Extensions/ServicesExtension.cs new file mode 100644 index 0000000..1cc42b4 --- /dev/null +++ b/CSharp/Core/Common.Core/Extensions/ServicesExtension.cs @@ -0,0 +1,18 @@ +namespace Common.Core.Extensions; + +public static class ServicesExtension +{ + public static TModel? TryGetService(this IServiceProvider serviceProvider) where TModel : class + { + try + { + return (TModel?)serviceProvider.GetService(typeof(TModel)); + } + catch (ObjectDisposedException) + { + // ignore as we do not care... + } + + return default; + } +} \ No newline at end of file diff --git a/CSharp/Core/Common.Core/Helpers/AppSettings.cs b/CSharp/Core/Common.Core/Helpers/AppSettings.cs new file mode 100644 index 0000000..23a4c04 --- /dev/null +++ b/CSharp/Core/Common.Core/Helpers/AppSettings.cs @@ -0,0 +1,92 @@ +using Microsoft.Extensions.Configuration; + +namespace Common.Core; + +public class AppSettings +{ + #region Constructors + + public AppSettings(IConfigurationSection configSection, string? key = null) + { + _configSection = configSection; + + // ReSharper disable once VirtualMemberCallInConstructor + GetValue(key); + } + + #endregion + + #region Fields + + protected static AppSettings? _appSetting; + + // ReSharper disable once StaticMemberInGenericType + protected static IConfigurationSection? _configSection; + + #endregion + + #region Properties + + public TOption? Value { get; set; } + + #endregion + + #region Methods + + public static TOption? Current(string section, string? key = null) + { + _appSetting = GetCurrentSettings(section, key); + return _appSetting.Value; + } + + public static AppSettings GetCurrentSettings(string section, string? key = null) + { + string env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"; + + IConfigurationBuilder builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env}.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + + IConfigurationRoot configuration = builder.Build(); + + if (string.IsNullOrEmpty(section)) + section = "AppSettings"; // default + + AppSettings settings = new AppSettings(configuration.GetSection(section), key); + + return settings; + } + + protected virtual void GetValue(string? key) + { + if (key is null) + { + // no key, so must be a class/strut object + Value = Activator.CreateInstance(); + _configSection!.Bind(Value); + return; + } + + Type optionType = typeof(TOption); + + if ((optionType == typeof(string) || + optionType == typeof(int) || + optionType == typeof(long) || + optionType == typeof(decimal) || + optionType == typeof(float) || + optionType == typeof(double)) + && _configSection != null) + { + // we must be retrieving a value + Value = _configSection.GetValue(key); + return; + } + + // Could not find a supported type + throw new InvalidCastException($"Type {typeof(TOption).Name} is invalid"); + } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Core/LogViewer.Core/Extensions/LoggerExtensions.cs b/CSharp/Core/LogViewer.Core/Extensions/LoggerExtensions.cs new file mode 100644 index 0000000..92198a2 --- /dev/null +++ b/CSharp/Core/LogViewer.Core/Extensions/LoggerExtensions.cs @@ -0,0 +1,53 @@ +namespace Microsoft.Extensions.Logging; + +public static class LoggerExtensions +{ + public static void Emit(this ILogger logger, EventId eventId, + LogLevel logLevel, string message, Exception? exception = null, params object?[] args) + { + if (logger is null) + return; + + //if (!logger.IsEnabled(logLevel)) + // return; + + switch (logLevel) + { + case LogLevel.Trace: + logger.LogTrace(eventId, message, args); + break; + + case LogLevel.Debug: + logger.LogDebug(eventId, message, args); + break; + + case LogLevel.Information: + logger.LogInformation(eventId, message, args); + break; + + case LogLevel.Warning: + logger.LogWarning(eventId, exception, message, args); + break; + + case LogLevel.Error: + logger.LogError(eventId, exception, message, args); + break; + + case LogLevel.Critical: + logger.LogCritical(eventId, exception, message, args); + break; + } + } + + public static void TestPattern(this ILogger logger, EventId eventId) + { + Exception exception = new Exception("Test Error Message"); + + logger.Emit(eventId, LogLevel.Trace, "Trace Test Pattern"); + logger.Emit(eventId, LogLevel.Debug, "Debug Test Pattern"); + logger.Emit(eventId, LogLevel.Information, "Information Test Pattern"); + logger.Emit(eventId, LogLevel.Warning, "Warning Test Pattern"); + logger.Emit(eventId, LogLevel.Error, "Error Test Pattern", exception); + logger.Emit(eventId, LogLevel.Critical, "Critical Test Pattern", exception); + } +} \ No newline at end of file diff --git a/CSharp/Core/LogViewer.Core/LogViewer.Core.csproj b/CSharp/Core/LogViewer.Core/LogViewer.Core.csproj new file mode 100644 index 0000000..e3c1dd5 --- /dev/null +++ b/CSharp/Core/LogViewer.Core/LogViewer.Core.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/CSharp/Core/LogViewer.Core/Logging/DataStoreLoggerConfiguration.cs b/CSharp/Core/LogViewer.Core/Logging/DataStoreLoggerConfiguration.cs new file mode 100644 index 0000000..a5fb128 --- /dev/null +++ b/CSharp/Core/LogViewer.Core/Logging/DataStoreLoggerConfiguration.cs @@ -0,0 +1,24 @@ +using System.Drawing; +using Microsoft.Extensions.Logging; + +namespace LogViewer.Core; + +public class DataStoreLoggerConfiguration +{ + #region Properties + + public EventId EventId { get; set; } + + public Dictionary Colors { get; } = new() + { + [LogLevel.Trace] = new() { Foreground = Color.DarkGray }, + [LogLevel.Debug] = new() { Foreground = Color.Gray }, + [LogLevel.Information] = new(), + [LogLevel.Warning] = new() { Foreground = Color.Orange}, + [LogLevel.Error] = new() { Foreground = Color.White, Background = Color.OrangeRed }, + [LogLevel.Critical] = new() { Foreground=Color.White, Background = Color.Red }, + [LogLevel.None] = new(), + }; + + #endregion +} \ No newline at end of file diff --git a/CSharp/Core/LogViewer.Core/Logging/ILogDataStore.cs b/CSharp/Core/LogViewer.Core/Logging/ILogDataStore.cs new file mode 100644 index 0000000..67d1c75 --- /dev/null +++ b/CSharp/Core/LogViewer.Core/Logging/ILogDataStore.cs @@ -0,0 +1,9 @@ +using System.Collections.ObjectModel; + +namespace LogViewer.Core; + +public interface ILogDataStore +{ + ObservableCollection Entries { get; } + void AddEntry(LogModel logModel); +} diff --git a/CSharp/Core/LogViewer.Core/Logging/ILogDataStoreImpl.cs b/CSharp/Core/LogViewer.Core/Logging/ILogDataStoreImpl.cs new file mode 100644 index 0000000..ce7a0e1 --- /dev/null +++ b/CSharp/Core/LogViewer.Core/Logging/ILogDataStoreImpl.cs @@ -0,0 +1,6 @@ +namespace LogViewer.Core; + +public interface ILogDataStoreImpl +{ + public ILogDataStore DataStore { get; } +} \ No newline at end of file diff --git a/CSharp/Core/LogViewer.Core/Logging/LogDataStore.cs b/CSharp/Core/LogViewer.Core/Logging/LogDataStore.cs new file mode 100644 index 0000000..982dd28 --- /dev/null +++ b/CSharp/Core/LogViewer.Core/Logging/LogDataStore.cs @@ -0,0 +1,32 @@ +using System.Collections.ObjectModel; + +namespace LogViewer.Core; + +public class LogDataStore : ILogDataStore +{ + #region Fields + + private static readonly SemaphoreSlim _semaphore = new(initialCount: 1); + + #endregion + + #region Properties + + public ObservableCollection Entries { get; } = new(); + + #endregion + + #region Methods + + public virtual void AddEntry(LogModel logModel) + { + // ensure only one operation at time from multiple threads + _semaphore.Wait(); + + Entries.Add(logModel); + + _semaphore.Release(); + } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Core/LogViewer.Core/Logging/LogEntryColor.cs b/CSharp/Core/LogViewer.Core/Logging/LogEntryColor.cs new file mode 100644 index 0000000..c1f7fcf --- /dev/null +++ b/CSharp/Core/LogViewer.Core/Logging/LogEntryColor.cs @@ -0,0 +1,10 @@ +using System.Drawing; + +namespace LogViewer.Core; + +public class LogEntryColor +{ + public Color Foreground { get; set; } = Color.Black; + public Color Background { get; set; } = Color.Transparent; + +} \ No newline at end of file diff --git a/CSharp/Core/LogViewer.Core/Logging/LogModel.cs b/CSharp/Core/LogViewer.Core/Logging/LogModel.cs new file mode 100644 index 0000000..d8e27fd --- /dev/null +++ b/CSharp/Core/LogViewer.Core/Logging/LogModel.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; + +namespace LogViewer.Core; + +public class LogModel +{ + #region Properties + + public DateTime Timestamp { get; set; } + + public LogLevel LogLevel { get; set; } + + public EventId EventId { get; set; } + + public object? State { get; set; } + + public string? Exception { get; set; } + + public LogEntryColor? Color { get; set; } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Core/LogViewer.Core/ViewModels/LogViewerControlViewModel.cs b/CSharp/Core/LogViewer.Core/ViewModels/LogViewerControlViewModel.cs new file mode 100644 index 0000000..86e5d9b --- /dev/null +++ b/CSharp/Core/LogViewer.Core/ViewModels/LogViewerControlViewModel.cs @@ -0,0 +1,21 @@ +using Mvvm.Core; + +namespace LogViewer.Core.ViewModels; + +public class LogViewerControlViewModel : ViewModel, ILogDataStoreImpl +{ + #region Constructor + + public LogViewerControlViewModel(ILogDataStore dataStore) + { + DataStore = dataStore; + } + + #endregion + + #region Properties + + public ILogDataStore DataStore { get; set; } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Core/MsLogger.Core/DataStoreLogger.cs b/CSharp/Core/MsLogger.Core/DataStoreLogger.cs new file mode 100644 index 0000000..b4e8dc1 --- /dev/null +++ b/CSharp/Core/MsLogger.Core/DataStoreLogger.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; +using LogViewer.Core; +using Microsoft.Extensions.Logging; + +namespace MsLogger.Core; + +public class DataStoreLogger: ILogger +{ + // ref: https://learn.microsoft.com/en-us/dotnet/core/extensions/custom-logging-provider + + #region Constructor + + public DataStoreLogger(string name, Func getCurrentConfig, ILogDataStore dataStore) + { + (_name, _getCurrentConfig) = (name, getCurrentConfig); + _dataStore = dataStore; + } + + #endregion + + #region Fields + + private readonly ILogDataStore _dataStore; + private readonly string _name; + private readonly Func _getCurrentConfig; + + #endregion + + #region methods + + public IDisposable BeginScope(TState state) where TState : notnull => default!; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + // check if we are logging for passed log level + if (!IsEnabled(logLevel)) + return; + + DataStoreLoggerConfiguration config = _getCurrentConfig(); + + _dataStore.AddEntry(new() + { + Timestamp = DateTime.UtcNow, + LogLevel = logLevel, + // do we override the default EventId if it exists? + EventId = eventId.Id == 0 && config.EventId != 0 ? config.EventId : eventId, + State = state, + Exception = exception?.Message ?? (logLevel == LogLevel.Error ? state?.ToString() ?? "" : ""), + Color = config.Colors[logLevel], + }); + + Debug.WriteLine($"--- [{logLevel.ToString()[..3]}] {_name} - {formatter(state, exception!)}"); + } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Core/MsLogger.Core/DataStoreLoggerProvider.cs b/CSharp/Core/MsLogger.Core/DataStoreLoggerProvider.cs new file mode 100644 index 0000000..c1d1a94 --- /dev/null +++ b/CSharp/Core/MsLogger.Core/DataStoreLoggerProvider.cs @@ -0,0 +1,48 @@ +using System.Collections.Concurrent; +using LogViewer.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace MsLogger.Core; + +public class DataStoreLoggerProvider: ILoggerProvider +{ + + #region Constructor + + public DataStoreLoggerProvider(IOptionsMonitor config, ILogDataStore dataStore) + { + _dataStore = dataStore; + _currentConfig = config.CurrentValue; + _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig); + } + + #endregion + + #region fields + + private DataStoreLoggerConfiguration _currentConfig; + + private readonly IDisposable? _onChangeToken; + protected readonly ILogDataStore _dataStore; + + protected readonly ConcurrentDictionary _loggers = new(); + + #endregion + + #region Methods + + public ILogger CreateLogger(string categoryName) + => _loggers.GetOrAdd(categoryName, name => new DataStoreLogger(name, GetCurrentConfig, _dataStore)); + + protected DataStoreLoggerConfiguration GetCurrentConfig() + => _currentConfig; + + public void Dispose() + { + _loggers.Clear(); + _onChangeToken?.Dispose(); + } + + #endregion +} \ No newline at end of file diff --git a/CSharp/Core/MsLogger.Core/Extensions/ServicesExtension.cs b/CSharp/Core/MsLogger.Core/Extensions/ServicesExtension.cs new file mode 100644 index 0000000..3a1bdac --- /dev/null +++ b/CSharp/Core/MsLogger.Core/Extensions/ServicesExtension.cs @@ -0,0 +1,22 @@ +using LogViewer.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace MsLogger.Core; + +public static class ServicesExtension +{ + public static ILoggingBuilder AddDefaultDataStoreLogger(this ILoggingBuilder builder) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + return builder; + } + + public static ILoggingBuilder AddDefaultDataStoreLogger(this ILoggingBuilder builder, Action configure) + { + builder.AddDefaultDataStoreLogger(); + builder.Services.Configure(configure); + return builder; + } +} \ No newline at end of file diff --git a/CSharp/Core/MsLogger.Core/MsLogger.Core.csproj b/CSharp/Core/MsLogger.Core/MsLogger.Core.csproj new file mode 100644 index 0000000..79f2d71 --- /dev/null +++ b/CSharp/Core/MsLogger.Core/MsLogger.Core.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/CSharp/Core/Mvvm.Core/Mvvm.Core.csproj b/CSharp/Core/Mvvm.Core/Mvvm.Core.csproj new file mode 100644 index 0000000..cfadb03 --- /dev/null +++ b/CSharp/Core/Mvvm.Core/Mvvm.Core.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/CSharp/Core/Mvvm.Core/ObservableObject.cs b/CSharp/Core/Mvvm.Core/ObservableObject.cs new file mode 100644 index 0000000..64cedac --- /dev/null +++ b/CSharp/Core/Mvvm.Core/ObservableObject.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Mvvm.Core; + +public class ObservableObject : INotifyPropertyChanged +{ + protected bool Set(ref TValue field, TValue newValue, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, newValue)) return false; + field = newValue; + OnPropertyChanged(propertyName); + + return true; + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); +} \ No newline at end of file diff --git a/CSharp/Core/Mvvm.Core/ViewModel.cs b/CSharp/Core/Mvvm.Core/ViewModel.cs new file mode 100644 index 0000000..3785fbc --- /dev/null +++ b/CSharp/Core/Mvvm.Core/ViewModel.cs @@ -0,0 +1,3 @@ +namespace Mvvm.Core; + +public class ViewModel : ObservableObject { /* skip */ } \ No newline at end of file diff --git a/CSharp/Core/Serilog.Sinks.LogView.Core/DataStoreLoggerSink.cs b/CSharp/Core/Serilog.Sinks.LogView.Core/DataStoreLoggerSink.cs new file mode 100644 index 0000000..b38e3c5 --- /dev/null +++ b/CSharp/Core/Serilog.Sinks.LogView.Core/DataStoreLoggerSink.cs @@ -0,0 +1,94 @@ +using Serilog.Events; +using LogViewer.Core; +using Microsoft.Extensions.Logging; +using Serilog.Core; + +namespace Serilog.Sinks.LogView.Core; + +public class DataStoreLoggerSink : ILogEventSink +{ + protected readonly Func _dataStoreProvider; + + private readonly IFormatProvider? _formatProvider; + private readonly Func? _getCurrentConfig; + + public DataStoreLoggerSink(Func dataStoreProvider, + Func? getCurrentConfig = null, + IFormatProvider? formatProvider = null) + { + _formatProvider = formatProvider; + _dataStoreProvider = dataStoreProvider; + _getCurrentConfig = getCurrentConfig; + } + + public void Emit(LogEvent logEvent) + { + LogLevel logLevel = logEvent.Level switch + { + LogEventLevel.Verbose => LogLevel.Trace, + LogEventLevel.Debug => LogLevel.Debug, + LogEventLevel.Warning => LogLevel.Warning, + LogEventLevel.Error => LogLevel.Error, + LogEventLevel.Fatal => LogLevel.Critical, + _ => LogLevel.Information + }; + + DataStoreLoggerConfiguration config = _getCurrentConfig?.Invoke() ?? new DataStoreLoggerConfiguration(); + + EventId eventId = EventIdFactory(logEvent); + if (eventId.Id == 0 && config.EventId != 0) + eventId = config.EventId; + + string message = logEvent.RenderMessage(_formatProvider); + + string exception = logEvent.Exception?.Message ?? (logEvent.Level >= LogEventLevel.Error ? message : string.Empty); + + LogEntryColor color = config.Colors[logLevel]; + + AddLogEntry(logLevel, eventId, message, exception, color); + } + + protected virtual void AddLogEntry(LogLevel logLevel, EventId eventId, string message, string exception, LogEntryColor color) + { + ILogDataStore? dataStore = _dataStoreProvider.Invoke(); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (dataStore == null) + return; // app is shutting down + + dataStore.AddEntry(new() + { + Timestamp = DateTime.UtcNow, + LogLevel = logLevel, + EventId = eventId, + State = message, + Exception = exception, + Color = color + }); + } + + private static EventId EventIdFactory(LogEvent logEvent) + { + EventId eventId; + if (!logEvent.Properties.TryGetValue("EventId", out LogEventPropertyValue? src)) + return new(); + + int? id = null; + string? eventName = null; + + // ref: https://stackoverflow.com/a/56722516 + StructureValue? value = src as StructureValue; + + LogEventProperty? idProperty = value!.Properties.FirstOrDefault(x => x.Name.Equals("Id")); + if (idProperty is not null) + id = int.Parse(idProperty.Value.ToString()); + + LogEventProperty? nameProperty = value.Properties.FirstOrDefault(x => x.Name.Equals("Name")); + if (nameProperty is not null) + eventName = nameProperty.Value.ToString().Trim('"'); + + eventId = new EventId(id ?? 0, eventName ?? string.Empty); + + return eventId; + } +} \ No newline at end of file diff --git a/CSharp/Core/Serilog.Sinks.LogView.Core/Extensions/DataStoreLoggerSinkExtensions.cs b/CSharp/Core/Serilog.Sinks.LogView.Core/Extensions/DataStoreLoggerSinkExtensions.cs new file mode 100644 index 0000000..8113e2c --- /dev/null +++ b/CSharp/Core/Serilog.Sinks.LogView.Core/Extensions/DataStoreLoggerSinkExtensions.cs @@ -0,0 +1,24 @@ +using Serilog.Configuration; +using LogViewer.Core; + +namespace Serilog.Sinks.LogView.Core; + +public static class DataStoreLoggerSinkExtensions +{ + public static LoggerConfiguration DataStoreLoggerSink + ( + this LoggerSinkConfiguration loggerConfiguration, + Func dataStoreProvider, + Action? configuration = null, + IFormatProvider formatProvider = null! + ) + => loggerConfiguration.Sink(new DataStoreLoggerSink(dataStoreProvider, GetConfig(configuration), formatProvider)); + + private static Func GetConfig(Action? configuration) + { + // convert from Action to Func delegate to pass data + DataStoreLoggerConfiguration data = new(); + configuration?.Invoke(data); + return () => data; + } +} \ No newline at end of file diff --git a/CSharp/Core/Serilog.Sinks.LogView.Core/Serilog.Sinks.LogView.Core.csproj b/CSharp/Core/Serilog.Sinks.LogView.Core/Serilog.Sinks.LogView.Core.csproj new file mode 100644 index 0000000..e530631 --- /dev/null +++ b/CSharp/Core/Serilog.Sinks.LogView.Core/Serilog.Sinks.LogView.Core.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/Logging - CS.sln b/Logging - CS.sln new file mode 100644 index 0000000..77d548a --- /dev/null +++ b/Logging - CS.sln @@ -0,0 +1,338 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{A3BEB004-4DF7-4281-9A08-8A7BCD4E3CC9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mvvm.Core", "CSharp\Core\Mvvm.Core\Mvvm.Core.csproj", "{BB614345-449F-46AD-BE8C-5E2B7616EDE2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogViewer.Core", "CSharp\Core\LogViewer.Core\LogViewer.Core.csproj", "{34F75D8B-6F15-4DE4-8335-FED83557EB8E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Apps", "Apps", "{42E99803-0A95-4172-9079-3B8BF8CBDE9F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogViewer.Wpf", "CSharp\Controls\LogViewer.Wpf\LogViewer.Wpf.csproj", "{094DF049-194B-43EE-A4EC-EC647FBB44D5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Controls", "Controls", "{E589E611-C328-4D4F-817D-A91D5A1019FB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfLoggingDI", "CSharp\Applications\WpfLoggingDI\WpfLoggingDI.csproj", "{9FF3C1CB-C95B-4660-8DD7-9B367824B67C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Background Services", "Background Services", "{0CDEA51D-46FE-4767-BA2E-8F14582A926D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogViewer.WinForms", "CSharp\Controls\LogViewer.WinForms\LogViewer.WinForms.csproj", "{5EEBF0FE-9A6E-4B01-A8E3-1F21F183DF22}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsLoggingDI", "CSharp\Applications\WinFormsLoggingDI\WinFormsLoggingDI.csproj", "{82EA3F1F-8267-4920-AD4B-BA7087BBD5DA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsLoggingNoDI", "CSharp\Applications\WinFormsLoggingNoDI\WinFormsLoggingNoDI.csproj", "{E17466C3-8CC2-43EA-81C7-85F7EB1A7BAB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Core", "CSharp\Core\Common.Core\Common.Core.csproj", "{1688A0C1-1AE6-49F6-972E-C419E2A3B58F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfLoggingNoDI", "CSharp\Applications\WpfLoggingNoDI\WpfLoggingNoDI.csproj", "{622A3C25-28FD-484A-9CA5-E468CE926673}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{006FDAED-6319-4976-B8BA-8D94E4574139}" + ProjectSection(SolutionItems) = preProject + LICENSE = LICENSE + readme.md = readme.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RandomLogging.Service", "CSharp\Background Services\RandomLogging.Service\RandomLogging.Service.csproj", "{18BA2294-FE64-481F-A86F-F5FD84438B66}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Wpf", "CSharp\Core\Common.Wpf\Common.Wpf.csproj", "{A221EE8F-EBAD-4016-89EB-F97956BCEC61}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.WinForms", "CSharp\Core\Common.WinForms\Common.WinForms.csproj", "{19CDD4D0-8826-4538-85CE-74ABAF0483B3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MsLogger.Core", "CSharp\Core\MsLogger.Core\MsLogger.Core.csproj", "{0EDAAABD-495D-43A4-BDFB-A0506CAAC07E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LoggerProviders", "LoggerProviders", "{23CB559B-2361-4ED6-8A26-D1B1C2005D65}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MsLogger", "MsLogger", "{8635B709-1D5A-4445-AC45-F99EE264634F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serilog", "Serilog", "{578FF757-F837-4C23-B2CA-3CF8B016F6A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfSerilogDI", "CSharp\Applications\WpfSerilogDI\WpfSerilogDI.csproj", "{59A05E46-AB33-4F81-BDF6-3E8C3A1F5D5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.LogView.Core", "CSharp\Core\Serilog.Sinks.LogView.Core\Serilog.Sinks.LogView.Core.csproj", "{69763AFC-6182-402D-9418-6A48404C89A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfSerilogNoDI", "CSharp\Applications\WpfSerilogNoDI\WpfSerilogNoDI.csproj", "{E666F716-F9BD-4630-8610-D41E965B28BA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaLoggingDI", "CSharp\Applications\AvaloniaLoggingDI\AvaloniaLoggingDI.csproj", "{EA97953E-1223-40D5-A568-8932FDC3105E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogViewer.Avalonia", "CSharp\Controls\LogViewer.Avalonia\LogViewer.Avalonia.csproj", "{C34C889C-4EB3-45F6-83DE-70252D1D67D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaSerilogDI", "CSharp\Applications\AvaloniaSerilogDI\AvaloniaSerilogDI.csproj", "{BCB0601D-E042-4949-8172-7A35A619519C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaSerilogNoDI", "CSharp\Applications\AvaloniaSerilogNoDI\AvaloniaSerilogNoDI.csproj", "{4E892500-CF59-43A9-9A27-80D8EE028821}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsSerilogDI", "CSharp\Applications\WinFormsSerilogDI\WinFormsSerilogDI.csproj", "{44282198-F1B3-4C63-A52E-D2E6651D298C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsSerilogNoDI", "CSharp\Applications\WinFormsSerilogNoDI\WinFormsSerilogNoDI.csproj", "{4F3E2263-7FDD-4EBF-929D-1013473720BE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaLoggingNoDI", "CSharp\Applications\AvaloniaLoggingNoDI\AvaloniaLoggingNoDI.csproj", "{85C96F55-572A-4FDF-A028-12D27A48FB4D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NLog", "NLog", "{D1F38E69-F9D3-4B88-A5E1-D452C276EF93}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfNLogDI", "CSharp\Applications\WpfNLogDI\WpfNLogDI.csproj", "{5029989A-051C-4C2D-B119-E237994AC0A7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfNLogNoDI", "CSharp\Applications\WpfNLogNoDI\WpfNLogNoDI.csproj", "{E0E90703-3050-4764-8631-2948CFC18387}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsNLogDI", "CSharp\Applications\WinFormsNLogDI\WinFormsNLogDI.csproj", "{6C76908A-CA1D-4B73-B4C6-E14FE7AE3405}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsNLogNoDI", "CSharp\Applications\WinFormsNLogNoDI\WinFormsNLogNoDI.csproj", "{B2C63543-DD66-487D-AE75-07C5CE23F040}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaNlogDI", "CSharp\Applications\AvaloniaNlogDI\AvaloniaNlogDI.csproj", "{3B977A58-6F4B-4782-BE1F-82B75A400227}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaNlogNoDI", "CSharp\Applications\AvaloniaNlogNoDI\AvaloniaNlogNoDI.csproj", "{FE9FA295-A5C3-48B8-94C4-FDBA97171D63}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NLog.Target.LogView.Core", "CSharp\Core\NLog.Target.LogView.Core\NLog.Target.LogView.Core.csproj", "{840A16D5-CCF3-4DAA-A767-6AF7A7F3212F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3rd Party", "3rd Party", "{A0B29205-19C3-4FF8-B3A0-28E9E9182E86}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Log4Net.Appender.LogView.Core", "CSharp\Core\Log4Net.Appender.LogView.Core\Log4Net.Appender.LogView.Core.csproj", "{6EA556A7-365C-4693-BDC2-2AAB845560B4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Log4Net", "Log4Net", "{96DDE55E-63E1-4803-BFC5-72D6D38B4746}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfLog4NetDI", "CSharp\Applications\WpfLog4NetDI\WpfLog4NetDI.csproj", "{3AE2C208-0C93-4654-A4A0-A0D0168BC6BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaLog4NetDI", "CSharp\Applications\AvaloniaLog4NetDI\AvaloniaLog4NetDI.csproj", "{E48DA650-1EA0-4180-AE41-6A1007E5E4A8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaLog4NetNoDI", "CSharp\Applications\AvaloniaLog4NetNoDI\AvaloniaLog4NetNoDI.csproj", "{13F887B6-47E1-4FC9-A8FF-DF542AF05A81}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsLog4NetDI", "CSharp\Applications\WinFormsLog4NetDI\WinFormsLog4NetDI.csproj", "{6AF44460-53CE-4563-A03E-A7D297FBC729}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsLog4NetNoDI", "CSharp\Applications\WinFormsLog4NetNoDI\WinFormsLog4NetNoDI.csproj", "{AF87564F-FA75-486C-B35E-C8D29F1A3183}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfLog4NetNoDI", "CSharp\Applications\WpfLog4NetNoDI\WpfLog4NetNoDI.csproj", "{F8F1A367-A352-4338-AAA7-31F6E130CF9C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{7CD1D21E-CCCF-4458-B5E5-63ED2081E439}" +EndProject +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "Avalonia.Resources", "Resources\Avalonia.Resources\Avalonia.Resources.vbproj", "{30AB364F-51E4-4255-865D-1D163B5F82BC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Logging.Log4Net.AspNetCore", "Resources\3rd Party\Microsoft.Extensions.Logging.Log4Net.AspNetCore\Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj", "{170F33B5-C7C4-4D97-8DB9-DE508E2DFCD0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Attribute", "Attribute", "{78E23FC2-A337-4FC2-AEBF-CDC517AEA30C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfLoggingAttrDI", "CSharp\Applications\WpfLoggingAttrDI\WpfLoggingAttrDI.csproj", "{06700271-27A1-4236-9AB3-B45CE431FE0C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BB614345-449F-46AD-BE8C-5E2B7616EDE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB614345-449F-46AD-BE8C-5E2B7616EDE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB614345-449F-46AD-BE8C-5E2B7616EDE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB614345-449F-46AD-BE8C-5E2B7616EDE2}.Release|Any CPU.Build.0 = Release|Any CPU + {34F75D8B-6F15-4DE4-8335-FED83557EB8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34F75D8B-6F15-4DE4-8335-FED83557EB8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34F75D8B-6F15-4DE4-8335-FED83557EB8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34F75D8B-6F15-4DE4-8335-FED83557EB8E}.Release|Any CPU.Build.0 = Release|Any CPU + {094DF049-194B-43EE-A4EC-EC647FBB44D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {094DF049-194B-43EE-A4EC-EC647FBB44D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {094DF049-194B-43EE-A4EC-EC647FBB44D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {094DF049-194B-43EE-A4EC-EC647FBB44D5}.Release|Any CPU.Build.0 = Release|Any CPU + {9FF3C1CB-C95B-4660-8DD7-9B367824B67C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FF3C1CB-C95B-4660-8DD7-9B367824B67C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FF3C1CB-C95B-4660-8DD7-9B367824B67C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FF3C1CB-C95B-4660-8DD7-9B367824B67C}.Release|Any CPU.Build.0 = Release|Any CPU + {5EEBF0FE-9A6E-4B01-A8E3-1F21F183DF22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EEBF0FE-9A6E-4B01-A8E3-1F21F183DF22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EEBF0FE-9A6E-4B01-A8E3-1F21F183DF22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EEBF0FE-9A6E-4B01-A8E3-1F21F183DF22}.Release|Any CPU.Build.0 = Release|Any CPU + {82EA3F1F-8267-4920-AD4B-BA7087BBD5DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82EA3F1F-8267-4920-AD4B-BA7087BBD5DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82EA3F1F-8267-4920-AD4B-BA7087BBD5DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82EA3F1F-8267-4920-AD4B-BA7087BBD5DA}.Release|Any CPU.Build.0 = Release|Any CPU + {E17466C3-8CC2-43EA-81C7-85F7EB1A7BAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E17466C3-8CC2-43EA-81C7-85F7EB1A7BAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E17466C3-8CC2-43EA-81C7-85F7EB1A7BAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E17466C3-8CC2-43EA-81C7-85F7EB1A7BAB}.Release|Any CPU.Build.0 = Release|Any CPU + {1688A0C1-1AE6-49F6-972E-C419E2A3B58F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1688A0C1-1AE6-49F6-972E-C419E2A3B58F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1688A0C1-1AE6-49F6-972E-C419E2A3B58F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1688A0C1-1AE6-49F6-972E-C419E2A3B58F}.Release|Any CPU.Build.0 = Release|Any CPU + {622A3C25-28FD-484A-9CA5-E468CE926673}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {622A3C25-28FD-484A-9CA5-E468CE926673}.Debug|Any CPU.Build.0 = Debug|Any CPU + {622A3C25-28FD-484A-9CA5-E468CE926673}.Release|Any CPU.ActiveCfg = Release|Any CPU + {622A3C25-28FD-484A-9CA5-E468CE926673}.Release|Any CPU.Build.0 = Release|Any CPU + {18BA2294-FE64-481F-A86F-F5FD84438B66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18BA2294-FE64-481F-A86F-F5FD84438B66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18BA2294-FE64-481F-A86F-F5FD84438B66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18BA2294-FE64-481F-A86F-F5FD84438B66}.Release|Any CPU.Build.0 = Release|Any CPU + {A221EE8F-EBAD-4016-89EB-F97956BCEC61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A221EE8F-EBAD-4016-89EB-F97956BCEC61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A221EE8F-EBAD-4016-89EB-F97956BCEC61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A221EE8F-EBAD-4016-89EB-F97956BCEC61}.Release|Any CPU.Build.0 = Release|Any CPU + {19CDD4D0-8826-4538-85CE-74ABAF0483B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19CDD4D0-8826-4538-85CE-74ABAF0483B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19CDD4D0-8826-4538-85CE-74ABAF0483B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19CDD4D0-8826-4538-85CE-74ABAF0483B3}.Release|Any CPU.Build.0 = Release|Any CPU + {0EDAAABD-495D-43A4-BDFB-A0506CAAC07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EDAAABD-495D-43A4-BDFB-A0506CAAC07E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EDAAABD-495D-43A4-BDFB-A0506CAAC07E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EDAAABD-495D-43A4-BDFB-A0506CAAC07E}.Release|Any CPU.Build.0 = Release|Any CPU + {59A05E46-AB33-4F81-BDF6-3E8C3A1F5D5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59A05E46-AB33-4F81-BDF6-3E8C3A1F5D5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59A05E46-AB33-4F81-BDF6-3E8C3A1F5D5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59A05E46-AB33-4F81-BDF6-3E8C3A1F5D5C}.Release|Any CPU.Build.0 = Release|Any CPU + {69763AFC-6182-402D-9418-6A48404C89A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69763AFC-6182-402D-9418-6A48404C89A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69763AFC-6182-402D-9418-6A48404C89A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69763AFC-6182-402D-9418-6A48404C89A0}.Release|Any CPU.Build.0 = Release|Any CPU + {E666F716-F9BD-4630-8610-D41E965B28BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E666F716-F9BD-4630-8610-D41E965B28BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E666F716-F9BD-4630-8610-D41E965B28BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E666F716-F9BD-4630-8610-D41E965B28BA}.Release|Any CPU.Build.0 = Release|Any CPU + {EA97953E-1223-40D5-A568-8932FDC3105E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA97953E-1223-40D5-A568-8932FDC3105E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA97953E-1223-40D5-A568-8932FDC3105E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA97953E-1223-40D5-A568-8932FDC3105E}.Release|Any CPU.Build.0 = Release|Any CPU + {C34C889C-4EB3-45F6-83DE-70252D1D67D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C34C889C-4EB3-45F6-83DE-70252D1D67D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C34C889C-4EB3-45F6-83DE-70252D1D67D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C34C889C-4EB3-45F6-83DE-70252D1D67D5}.Release|Any CPU.Build.0 = Release|Any CPU + {BCB0601D-E042-4949-8172-7A35A619519C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCB0601D-E042-4949-8172-7A35A619519C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCB0601D-E042-4949-8172-7A35A619519C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCB0601D-E042-4949-8172-7A35A619519C}.Release|Any CPU.Build.0 = Release|Any CPU + {4E892500-CF59-43A9-9A27-80D8EE028821}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E892500-CF59-43A9-9A27-80D8EE028821}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E892500-CF59-43A9-9A27-80D8EE028821}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E892500-CF59-43A9-9A27-80D8EE028821}.Release|Any CPU.Build.0 = Release|Any CPU + {44282198-F1B3-4C63-A52E-D2E6651D298C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44282198-F1B3-4C63-A52E-D2E6651D298C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44282198-F1B3-4C63-A52E-D2E6651D298C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44282198-F1B3-4C63-A52E-D2E6651D298C}.Release|Any CPU.Build.0 = Release|Any CPU + {4F3E2263-7FDD-4EBF-929D-1013473720BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F3E2263-7FDD-4EBF-929D-1013473720BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F3E2263-7FDD-4EBF-929D-1013473720BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F3E2263-7FDD-4EBF-929D-1013473720BE}.Release|Any CPU.Build.0 = Release|Any CPU + {85C96F55-572A-4FDF-A028-12D27A48FB4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85C96F55-572A-4FDF-A028-12D27A48FB4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85C96F55-572A-4FDF-A028-12D27A48FB4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85C96F55-572A-4FDF-A028-12D27A48FB4D}.Release|Any CPU.Build.0 = Release|Any CPU + {5029989A-051C-4C2D-B119-E237994AC0A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5029989A-051C-4C2D-B119-E237994AC0A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5029989A-051C-4C2D-B119-E237994AC0A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5029989A-051C-4C2D-B119-E237994AC0A7}.Release|Any CPU.Build.0 = Release|Any CPU + {E0E90703-3050-4764-8631-2948CFC18387}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0E90703-3050-4764-8631-2948CFC18387}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0E90703-3050-4764-8631-2948CFC18387}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0E90703-3050-4764-8631-2948CFC18387}.Release|Any CPU.Build.0 = Release|Any CPU + {6C76908A-CA1D-4B73-B4C6-E14FE7AE3405}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C76908A-CA1D-4B73-B4C6-E14FE7AE3405}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C76908A-CA1D-4B73-B4C6-E14FE7AE3405}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C76908A-CA1D-4B73-B4C6-E14FE7AE3405}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C63543-DD66-487D-AE75-07C5CE23F040}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C63543-DD66-487D-AE75-07C5CE23F040}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C63543-DD66-487D-AE75-07C5CE23F040}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C63543-DD66-487D-AE75-07C5CE23F040}.Release|Any CPU.Build.0 = Release|Any CPU + {3B977A58-6F4B-4782-BE1F-82B75A400227}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B977A58-6F4B-4782-BE1F-82B75A400227}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B977A58-6F4B-4782-BE1F-82B75A400227}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B977A58-6F4B-4782-BE1F-82B75A400227}.Release|Any CPU.Build.0 = Release|Any CPU + {FE9FA295-A5C3-48B8-94C4-FDBA97171D63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE9FA295-A5C3-48B8-94C4-FDBA97171D63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE9FA295-A5C3-48B8-94C4-FDBA97171D63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE9FA295-A5C3-48B8-94C4-FDBA97171D63}.Release|Any CPU.Build.0 = Release|Any CPU + {840A16D5-CCF3-4DAA-A767-6AF7A7F3212F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {840A16D5-CCF3-4DAA-A767-6AF7A7F3212F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {840A16D5-CCF3-4DAA-A767-6AF7A7F3212F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {840A16D5-CCF3-4DAA-A767-6AF7A7F3212F}.Release|Any CPU.Build.0 = Release|Any CPU + {6EA556A7-365C-4693-BDC2-2AAB845560B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EA556A7-365C-4693-BDC2-2AAB845560B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EA556A7-365C-4693-BDC2-2AAB845560B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EA556A7-365C-4693-BDC2-2AAB845560B4}.Release|Any CPU.Build.0 = Release|Any CPU + {3AE2C208-0C93-4654-A4A0-A0D0168BC6BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AE2C208-0C93-4654-A4A0-A0D0168BC6BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AE2C208-0C93-4654-A4A0-A0D0168BC6BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AE2C208-0C93-4654-A4A0-A0D0168BC6BB}.Release|Any CPU.Build.0 = Release|Any CPU + {E48DA650-1EA0-4180-AE41-6A1007E5E4A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E48DA650-1EA0-4180-AE41-6A1007E5E4A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E48DA650-1EA0-4180-AE41-6A1007E5E4A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E48DA650-1EA0-4180-AE41-6A1007E5E4A8}.Release|Any CPU.Build.0 = Release|Any CPU + {13F887B6-47E1-4FC9-A8FF-DF542AF05A81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13F887B6-47E1-4FC9-A8FF-DF542AF05A81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13F887B6-47E1-4FC9-A8FF-DF542AF05A81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13F887B6-47E1-4FC9-A8FF-DF542AF05A81}.Release|Any CPU.Build.0 = Release|Any CPU + {6AF44460-53CE-4563-A03E-A7D297FBC729}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AF44460-53CE-4563-A03E-A7D297FBC729}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AF44460-53CE-4563-A03E-A7D297FBC729}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AF44460-53CE-4563-A03E-A7D297FBC729}.Release|Any CPU.Build.0 = Release|Any CPU + {AF87564F-FA75-486C-B35E-C8D29F1A3183}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF87564F-FA75-486C-B35E-C8D29F1A3183}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF87564F-FA75-486C-B35E-C8D29F1A3183}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF87564F-FA75-486C-B35E-C8D29F1A3183}.Release|Any CPU.Build.0 = Release|Any CPU + {F8F1A367-A352-4338-AAA7-31F6E130CF9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8F1A367-A352-4338-AAA7-31F6E130CF9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8F1A367-A352-4338-AAA7-31F6E130CF9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8F1A367-A352-4338-AAA7-31F6E130CF9C}.Release|Any CPU.Build.0 = Release|Any CPU + {30AB364F-51E4-4255-865D-1D163B5F82BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30AB364F-51E4-4255-865D-1D163B5F82BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30AB364F-51E4-4255-865D-1D163B5F82BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30AB364F-51E4-4255-865D-1D163B5F82BC}.Release|Any CPU.Build.0 = Release|Any CPU + {170F33B5-C7C4-4D97-8DB9-DE508E2DFCD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {170F33B5-C7C4-4D97-8DB9-DE508E2DFCD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {170F33B5-C7C4-4D97-8DB9-DE508E2DFCD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {170F33B5-C7C4-4D97-8DB9-DE508E2DFCD0}.Release|Any CPU.Build.0 = Release|Any CPU + {06700271-27A1-4236-9AB3-B45CE431FE0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06700271-27A1-4236-9AB3-B45CE431FE0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06700271-27A1-4236-9AB3-B45CE431FE0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06700271-27A1-4236-9AB3-B45CE431FE0C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BB614345-449F-46AD-BE8C-5E2B7616EDE2} = {A3BEB004-4DF7-4281-9A08-8A7BCD4E3CC9} + {34F75D8B-6F15-4DE4-8335-FED83557EB8E} = {A3BEB004-4DF7-4281-9A08-8A7BCD4E3CC9} + {094DF049-194B-43EE-A4EC-EC647FBB44D5} = {E589E611-C328-4D4F-817D-A91D5A1019FB} + {9FF3C1CB-C95B-4660-8DD7-9B367824B67C} = {8635B709-1D5A-4445-AC45-F99EE264634F} + {5EEBF0FE-9A6E-4B01-A8E3-1F21F183DF22} = {E589E611-C328-4D4F-817D-A91D5A1019FB} + {82EA3F1F-8267-4920-AD4B-BA7087BBD5DA} = {8635B709-1D5A-4445-AC45-F99EE264634F} + {E17466C3-8CC2-43EA-81C7-85F7EB1A7BAB} = {8635B709-1D5A-4445-AC45-F99EE264634F} + {1688A0C1-1AE6-49F6-972E-C419E2A3B58F} = {A3BEB004-4DF7-4281-9A08-8A7BCD4E3CC9} + {622A3C25-28FD-484A-9CA5-E468CE926673} = {8635B709-1D5A-4445-AC45-F99EE264634F} + {18BA2294-FE64-481F-A86F-F5FD84438B66} = {0CDEA51D-46FE-4767-BA2E-8F14582A926D} + {A221EE8F-EBAD-4016-89EB-F97956BCEC61} = {A3BEB004-4DF7-4281-9A08-8A7BCD4E3CC9} + {19CDD4D0-8826-4538-85CE-74ABAF0483B3} = {A3BEB004-4DF7-4281-9A08-8A7BCD4E3CC9} + {0EDAAABD-495D-43A4-BDFB-A0506CAAC07E} = {23CB559B-2361-4ED6-8A26-D1B1C2005D65} + {23CB559B-2361-4ED6-8A26-D1B1C2005D65} = {A3BEB004-4DF7-4281-9A08-8A7BCD4E3CC9} + {8635B709-1D5A-4445-AC45-F99EE264634F} = {42E99803-0A95-4172-9079-3B8BF8CBDE9F} + {578FF757-F837-4C23-B2CA-3CF8B016F6A9} = {42E99803-0A95-4172-9079-3B8BF8CBDE9F} + {59A05E46-AB33-4F81-BDF6-3E8C3A1F5D5C} = {578FF757-F837-4C23-B2CA-3CF8B016F6A9} + {69763AFC-6182-402D-9418-6A48404C89A0} = {23CB559B-2361-4ED6-8A26-D1B1C2005D65} + {E666F716-F9BD-4630-8610-D41E965B28BA} = {578FF757-F837-4C23-B2CA-3CF8B016F6A9} + {EA97953E-1223-40D5-A568-8932FDC3105E} = {8635B709-1D5A-4445-AC45-F99EE264634F} + {C34C889C-4EB3-45F6-83DE-70252D1D67D5} = {E589E611-C328-4D4F-817D-A91D5A1019FB} + {BCB0601D-E042-4949-8172-7A35A619519C} = {578FF757-F837-4C23-B2CA-3CF8B016F6A9} + {4E892500-CF59-43A9-9A27-80D8EE028821} = {578FF757-F837-4C23-B2CA-3CF8B016F6A9} + {44282198-F1B3-4C63-A52E-D2E6651D298C} = {578FF757-F837-4C23-B2CA-3CF8B016F6A9} + {4F3E2263-7FDD-4EBF-929D-1013473720BE} = {578FF757-F837-4C23-B2CA-3CF8B016F6A9} + {85C96F55-572A-4FDF-A028-12D27A48FB4D} = {8635B709-1D5A-4445-AC45-F99EE264634F} + {D1F38E69-F9D3-4B88-A5E1-D452C276EF93} = {42E99803-0A95-4172-9079-3B8BF8CBDE9F} + {5029989A-051C-4C2D-B119-E237994AC0A7} = {D1F38E69-F9D3-4B88-A5E1-D452C276EF93} + {E0E90703-3050-4764-8631-2948CFC18387} = {D1F38E69-F9D3-4B88-A5E1-D452C276EF93} + {6C76908A-CA1D-4B73-B4C6-E14FE7AE3405} = {D1F38E69-F9D3-4B88-A5E1-D452C276EF93} + {B2C63543-DD66-487D-AE75-07C5CE23F040} = {D1F38E69-F9D3-4B88-A5E1-D452C276EF93} + {3B977A58-6F4B-4782-BE1F-82B75A400227} = {D1F38E69-F9D3-4B88-A5E1-D452C276EF93} + {FE9FA295-A5C3-48B8-94C4-FDBA97171D63} = {D1F38E69-F9D3-4B88-A5E1-D452C276EF93} + {840A16D5-CCF3-4DAA-A767-6AF7A7F3212F} = {23CB559B-2361-4ED6-8A26-D1B1C2005D65} + {A0B29205-19C3-4FF8-B3A0-28E9E9182E86} = {7CD1D21E-CCCF-4458-B5E5-63ED2081E439} + {6EA556A7-365C-4693-BDC2-2AAB845560B4} = {23CB559B-2361-4ED6-8A26-D1B1C2005D65} + {96DDE55E-63E1-4803-BFC5-72D6D38B4746} = {42E99803-0A95-4172-9079-3B8BF8CBDE9F} + {3AE2C208-0C93-4654-A4A0-A0D0168BC6BB} = {96DDE55E-63E1-4803-BFC5-72D6D38B4746} + {E48DA650-1EA0-4180-AE41-6A1007E5E4A8} = {96DDE55E-63E1-4803-BFC5-72D6D38B4746} + {13F887B6-47E1-4FC9-A8FF-DF542AF05A81} = {96DDE55E-63E1-4803-BFC5-72D6D38B4746} + {6AF44460-53CE-4563-A03E-A7D297FBC729} = {96DDE55E-63E1-4803-BFC5-72D6D38B4746} + {AF87564F-FA75-486C-B35E-C8D29F1A3183} = {96DDE55E-63E1-4803-BFC5-72D6D38B4746} + {F8F1A367-A352-4338-AAA7-31F6E130CF9C} = {96DDE55E-63E1-4803-BFC5-72D6D38B4746} + {30AB364F-51E4-4255-865D-1D163B5F82BC} = {7CD1D21E-CCCF-4458-B5E5-63ED2081E439} + {170F33B5-C7C4-4D97-8DB9-DE508E2DFCD0} = {A0B29205-19C3-4FF8-B3A0-28E9E9182E86} + {78E23FC2-A337-4FC2-AEBF-CDC517AEA30C} = {8635B709-1D5A-4445-AC45-F99EE264634F} + {06700271-27A1-4236-9AB3-B45CE431FE0C} = {78E23FC2-A337-4FC2-AEBF-CDC517AEA30C} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D6A9B467-ED50-40DB-9FFB-5BE745F08DDB} + EndGlobalSection +EndGlobal diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Appenders/IAppenderServiceProvider.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Appenders/IAppenderServiceProvider.cs new file mode 100644 index 0000000..17380ea --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Appenders/IAppenderServiceProvider.cs @@ -0,0 +1,8 @@ +using System; + +namespace log4net.Appender; + +internal interface IAppenderServiceProvider +{ + IServiceProvider ServiceProvider { set; } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Appenders/ServiceAppenderSkeleton.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Appenders/ServiceAppenderSkeleton.cs new file mode 100644 index 0000000..f85cb69 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Appenders/ServiceAppenderSkeleton.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace log4net.Appender; + +public abstract class ServiceAppenderSkeleton : AppenderSkeleton, IAppenderServiceProvider, IDisposable +{ + private IServiceProvider _serviceProvider; + IServiceProvider IAppenderServiceProvider.ServiceProvider { set => _serviceProvider = value; } + + protected T ResolveService() where T : class + { + if (_serviceProvider == null) + return default; + + return _serviceProvider.GetService(); + } + + public void Dispose() => _serviceProvider = null; +} diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/EventIDLogImpl.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/EventIDLogImpl.cs new file mode 100644 index 0000000..b8929e9 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/EventIDLogImpl.cs @@ -0,0 +1,22 @@ +using log4net.Core; + +namespace Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities; + +// ref: http://svn.apache.org/viewvc/logging/log4net/trunk/examples/net/2.0/Extensibility/EventIDLogApp/cs/src/ +public class EventIDLogImpl : LogImpl, IEventIDLog +{ + public EventIDLogImpl(log4net.Core.ILogger logger) : base(logger) { /* skip */ } + + #region Implementation of IEventIDLog + + public void Log(EventId eventId, LoggingEvent loggingEvent) + { + // is the EventId empty? + if (!(eventId.Id == 0 && string.IsNullOrWhiteSpace(eventId.Name))) + loggingEvent.Properties[nameof(EventId)] = eventId; + + Logger.Log(loggingEvent); + } + + #endregion +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/IEventIDLog .cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/IEventIDLog .cs new file mode 100644 index 0000000..966ff7c --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/IEventIDLog .cs @@ -0,0 +1,9 @@ +using log4net; +using log4net.Core; + +namespace Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities; + +public interface IEventIDLog : ILog +{ + void Log(EventId eventId, LoggingEvent loggingEvent); +} diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/MessageCandidate.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/MessageCandidate.cs new file mode 100644 index 0000000..af2fb62 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/MessageCandidate.cs @@ -0,0 +1,54 @@ +using System; + +namespace Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities +{ + /// + /// Represents a candidate for a log message that should be printed. This candidate will either be accepted or denied by the logger that is trying to print it. + /// + /// + /// + /// This is a readonly struct to reduce memory pressure, but because it is quite large (definitly larger than the recommended 16 bytes) + /// it needs to be passed as a reference (with the in keyword) to make a difference. + /// + /// + /// See for more information. + /// + /// + /// Type of the state that is used to format the error message. + public readonly struct MessageCandidate + { + public MessageCandidate(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + State = state; + LogLevel = logLevel; + EventId = eventId; + Exception = exception; + Formatter = formatter; + } + + /// + /// The log level the message should be printed with. + /// + public LogLevel LogLevel { get; } + + /// + /// The event id of the message. + /// + public EventId EventId { get; } + + /// + /// The message state. Can be provided to the formatter to generate the string representation of the error message. + /// + public TState State { get; } + + /// + /// Exception that should be printed with the message. Null if the log message has no corrosponding exception. + /// + public Exception Exception { get; } + + /// + /// The message formatter. Can be called with the state and exception to generate the string representation of the error message. + /// + public Func Formatter { get; } + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/NodeInfo.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/NodeInfo.cs new file mode 100644 index 0000000..fa5ceb5 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Entities/NodeInfo.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities +{ + /// + /// Class to store information of a log4net xml config file node. + /// + public class NodeInfo + { + /// + /// Gets or sets the x path to find the node to override. + /// + /// + /// The x path. + /// + public string XPath { get; set; } + + /// + /// Gets or sets the content of the node. + /// + /// + /// The content of the node. + /// + public string NodeContent { get; set; } + + /// + /// Gets or sets the attributes. + /// + /// + /// The attributes. + /// + public Dictionary Attributes { get; set; } + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Extensions/DocumentExtensions.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Extensions/DocumentExtensions.cs new file mode 100644 index 0000000..34b3b78 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Extensions/DocumentExtensions.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Xml; +using System.Xml.Linq; + +namespace Microsoft.Extensions.Logging.Log4Net.AspNetCore.Extensions +{ + /// + /// Class with XmlDocument and XDocument extensions. + /// + internal static class DocumentExtensions + { + /// + /// Converts a XmlDocument object into xDocument. + /// + /// The XML document. + /// The XmlDocument converted to XDocument + public static XDocument ToXDocument(this XmlDocument xmlDocument) + { + using (var memoryStream = new MemoryStream()) + { + xmlDocument.Save(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + return XDocument.Load(memoryStream); + } + } + + /// + /// Converts a XDocument object into XmlDocument + /// + /// The x document. + /// The XDocument converted to XmlDocument + public static XmlDocument ToXmlDocument(this XDocument xDocument) + { + using (var memoryStream = new MemoryStream()) + { + xDocument.Save(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + var xmlDoc = new XmlDocument(); + xmlDoc.Load(memoryStream); + return xmlDoc; + } + } + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Extensions/Log4NetProviderExtensions.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Extensions/Log4NetProviderExtensions.cs new file mode 100644 index 0000000..2fbabb9 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Extensions/Log4NetProviderExtensions.cs @@ -0,0 +1,33 @@ +using log4net; +using System; +using System.Reflection; + +namespace Microsoft.Extensions.Logging.Extensions +{ + /// + /// Log4Net provider extensions. + /// + public static class Log4NetProviderExtensions + { + /// + /// Creates a logger with the name of the given type. + /// + /// The type of the class to be used as name of the logger. + /// An ILoggerProvider instance. + /// An instance of the . + public static ILogger CreateLogger(this ILoggerProvider self) where TName : class + { + if (self == null) + { + throw new ArgumentNullException(nameof(self)); + } + + if (!self.GetType().IsAssignableFrom(typeof(Log4NetProvider))) + { + throw new ArgumentOutOfRangeException(nameof(self), "The ILoggerProvider should be of type Log4NetProvider."); + } + + return self.CreateLogger(typeof(TName).FullName); + } + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Extensions/LogExtensions.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Extensions/LogExtensions.cs new file mode 100644 index 0000000..5ea259f --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Extensions/LogExtensions.cs @@ -0,0 +1,29 @@ +using log4net; +using System; + +namespace Microsoft.Extensions.Logging.Log4Net.AspNetCore.Extensions +{ + /// + /// The class. + /// + public static class LogExtensions + { + /// + /// Criticals the specified message. + /// + /// The log. + /// The message. + /// The exception. + public static void Critical(this ILog log, object message, Exception exception) + => log.Logger.Log(null, log4net.Core.Level.Critical, message, exception); + + /// + /// Traces the specified message. + /// + /// The log. + /// The message. + /// The exception. + public static void Trace(this ILog log, object message, Exception exception) + => log.Logger.Log(null, log4net.Core.Level.Trace, message, exception); + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/ILog4NetLogLevelTranslator.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/ILog4NetLogLevelTranslator.cs new file mode 100644 index 0000000..9bd80d8 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/ILog4NetLogLevelTranslator.cs @@ -0,0 +1,16 @@ +namespace Microsoft.Extensions.Logging +{ + /// + /// Represents a log level translator between the different logging systems. + /// + public interface ILog4NetLogLevelTranslator + { + /// + /// Translates a to a log4net based on the provided options. + /// + /// The log level to translate. + /// The log4net provider options influencing the translation. + /// The corresponding log level for log4net. + log4net.Core.Level TranslateLogLevel(LogLevel logLevel, Log4NetProviderOptions options); + } +} diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/ILog4NetLoggingEventFactory.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/ILog4NetLoggingEventFactory.cs new file mode 100644 index 0000000..7e3dd42 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/ILog4NetLoggingEventFactory.cs @@ -0,0 +1,25 @@ +using log4net.Core; +using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities; + +namespace Microsoft.Extensions.Logging +{ + /// + /// Represents a factory that creates the log4net from a . + /// + public interface ILog4NetLoggingEventFactory + { + /// + /// Create the . + /// + /// Type of the state object that is used to format the log message. + /// The message information that should be logged. + /// The logger the event is created for. + /// The options of the log4net logging provider. + /// A that is ready to be logged with the provided logger or null if the candidate should be dropped. + LoggingEvent CreateLoggingEvent( + in MessageCandidate messageCandidate, + log4net.Core.ILogger logger, + Log4NetProviderOptions options, + IExternalScopeProvider scopeProvider); + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/LICENSE b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetExtensions.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetExtensions.cs new file mode 100644 index 0000000..3d7250e --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetExtensions.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.Logging +{ + /// + /// The log4net extensions class. + /// + public static class Log4NetExtensions + { + /// + /// Adds the log4net. + /// + /// The factory. + /// The with added Log4Net provider + public static ILoggerFactory AddLog4Net(this ILoggerFactory factory) + => factory.AddLog4Net(new Log4NetProviderOptions()); + + /// + /// Adds the log4net. + /// + /// The factory. + /// The log4net Config File. + /// The after adding the log4net provider. + public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, string log4NetConfigFile) + => factory.AddLog4Net(log4NetConfigFile, false); + + /// + /// Adds the log4net logging provider. + /// + /// The factory. + /// The log4 net configuration file. + /// if set to true [watch]. + /// The after adding the log4net provider. + public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, string log4NetConfigFile, bool watch) + => factory.AddLog4Net(new Log4NetProviderOptions(log4NetConfigFile, watch)); + + /// + /// Adds the log4net logging provider. + /// + /// The logger factory. + /// The options for log4net provider. + /// The after adding the log4net provider. + public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, Log4NetProviderOptions options) + { + factory.AddProvider(new Log4NetProvider(options)); + return factory; + } + + /// + /// Adds the log4net logging provider. + /// + /// The logging builder instance. + /// The passed as parameter with the new provider registered. + public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder) + { + var options = new Log4NetProviderOptions(); + return builder.AddLog4Net(options); + } + + /// + /// Adds the log4net logging provider. + /// + /// The logging builder instance. + /// The log4net Config File. + /// The passed as parameter with the new provider registered. + public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder, string log4NetConfigFile) + { + var options = new Log4NetProviderOptions(log4NetConfigFile); + return builder.AddLog4Net(options); + } + + /// + /// Adds the log4net logging provider. + /// + /// The logging builder instance. + /// The log4net Config File. + /// if set to true, the configuration will be reloaded when the xml configuration file changes. + /// + /// The passed as parameter with the new provider registered. + /// + public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder, string log4NetConfigFile, bool watch) + { + var options = new Log4NetProviderOptions(log4NetConfigFile, watch); + return builder.AddLog4Net(options); + } + + /// + /// Adds the log4net logging provider. + /// + /// The logging builder instance. + /// The options. + /// The passed as parameter with the new provider registered. + public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder, Log4NetProviderOptions options) + { + builder.Services.Replace(ServiceDescriptor.Singleton(sp => new Log4NetProvider(options, sp))); + return builder; + } + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetLogLevelTranslator.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetLogLevelTranslator.cs new file mode 100644 index 0000000..7da1de2 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetLogLevelTranslator.cs @@ -0,0 +1,42 @@ +using log4net.Core; +using System; + +namespace Microsoft.Extensions.Logging +{ + /// + public sealed class Log4NetLogLevelTranslator : ILog4NetLogLevelTranslator + { + /// + public Level TranslateLogLevel(LogLevel logLevel, Log4NetProviderOptions options) + { + Level log4NetLevel = null; + switch (logLevel) + { + case LogLevel.Critical: + string overrideCriticalLevelWith = options.OverrideCriticalLevelWith; + log4NetLevel = !string.IsNullOrEmpty(overrideCriticalLevelWith) + && overrideCriticalLevelWith.Equals(LogLevel.Critical.ToString(), StringComparison.OrdinalIgnoreCase) + ? Level.Critical + : Level.Fatal; + break; + case LogLevel.Debug: + log4NetLevel = Level.Debug; + break; + case LogLevel.Error: + log4NetLevel = Level.Error; + break; + case LogLevel.Information: + log4NetLevel = Level.Info; + break; + case LogLevel.Warning: + log4NetLevel = Level.Warn; + break; + case LogLevel.Trace: + log4NetLevel = Level.Trace; + break; + } + + return log4NetLevel; + } + } +} diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetLogger.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetLogger.cs new file mode 100644 index 0000000..cc67b91 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetLogger.cs @@ -0,0 +1,124 @@ +using log4net; +using log4net.Core; +using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities; +using System; + +namespace Microsoft.Extensions.Logging +{ + /// + /// The log4net eventIdLogger class. + /// + public class Log4NetLogger : ILogger + { + private readonly IExternalScopeProvider externalScopeProvider; + + /// + /// The log. + /// + private readonly IEventIDLog eventIdLogger; + + /// + /// The provider options. + /// + private readonly Log4NetProviderOptions options; + + /// + /// Initializes a new instance of the class. + /// + /// The log4net provider options. + public Log4NetLogger(Log4NetProviderOptions options, IExternalScopeProvider externalScopeProvider) + { + this.options = options ?? throw new ArgumentNullException(nameof(options)); + this.externalScopeProvider = externalScopeProvider ?? throw new ArgumentNullException(nameof(externalScopeProvider)); + this.eventIdLogger = new EventIDLogImpl(LogManager.GetLogger(options.LoggerRepository, options.Name).Logger); + } + + /// + /// Gets the name. + /// + public string Name + => this.eventIdLogger.Logger.Name; + + /// + /// A get-only property for accessing the + /// within the instance. + /// + internal Log4NetProviderOptions Options => this.options; + + + /// + /// Begins a logical operation scope. + /// + /// The type of the state. + /// The identifier for the scope. + /// + /// An IDisposable that ends the logical operation scope on dispose. + /// + public IDisposable BeginScope(TState state) + => externalScopeProvider.Push(state); + + /// + /// Determines whether the logging level is enabled. + /// + /// The log level. + /// The value indicating whether the logging level is enabled. + /// Throws when is outside allowed range. + public bool IsEnabled(LogLevel logLevel) + { + Level translatedLogLevel = this.options.LogLevelTranslator.TranslateLogLevel(logLevel, Options); + if (translatedLogLevel != null) + { + return this.eventIdLogger.Logger.IsEnabledFor(translatedLogLevel); + } + + if (logLevel == LogLevel.None) + { + return false; + } + + throw new ArgumentOutOfRangeException(nameof(logLevel)); + } + + /// + /// Logs an exception into the log. + /// + /// The log level. + /// The event Id. + /// The state. + /// The exception. + /// The formatter. + /// The type of the state. + /// Throws when the is null. + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + if (!this.IsEnabled(logLevel)) + { + return; + } + + EnsureValidFormatter(formatter); + + var candidate = new MessageCandidate(logLevel, eventId, state, exception, formatter); + + LoggingEvent loggingEvent = options.LoggingEventFactory.CreateLoggingEvent(in candidate, eventIdLogger.Logger, options, externalScopeProvider); + + if (loggingEvent == null) + return; + + this.eventIdLogger.Log(eventId, loggingEvent); + } + + private static void EnsureValidFormatter(Func formatter) + { + if (formatter == null) + { + throw new ArgumentNullException(nameof(formatter)); + } + } + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetLoggingEventFactory.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetLoggingEventFactory.cs new file mode 100644 index 0000000..94cca77 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetLoggingEventFactory.cs @@ -0,0 +1,140 @@ +using log4net.Core; +using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.Extensions.Logging +{ + /// + public class Log4NetLoggingEventFactory + : ILog4NetLoggingEventFactory + { + /// + /// The default property name for scopes that don't provide their own property name by implementing + /// an where T is and where TKey + /// is . + /// + protected const string DefaultScopeProperty = "scope"; + + /// + public LoggingEvent CreateLoggingEvent( + in MessageCandidate messageCandidate, + log4net.Core.ILogger logger, + Log4NetProviderOptions options, + IExternalScopeProvider scopeProvider) + { + Type callerStackBoundaryDeclaringType = typeof(LoggerExtensions); + string message = messageCandidate.Formatter(messageCandidate.State, messageCandidate.Exception); + Level logLevel = options.LogLevelTranslator.TranslateLogLevel(messageCandidate.LogLevel, options); + + if (logLevel == null || (string.IsNullOrEmpty(message) && messageCandidate.Exception == null)) + return null; + + var loggingEvent = new LoggingEvent( + callerStackBoundaryDeclaringType: callerStackBoundaryDeclaringType, + repository: logger.Repository, + loggerName: logger.Name, + level: logLevel, + message: message, + exception: messageCandidate.Exception); + + // ref: https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore/pull/126/commits/1ad3d429afb4417947863d1bc37231bf7a457f3c + + if (messageCandidate.State is IEnumerable> formattedLogValues) + { + foreach (var pair in formattedLogValues) + { + loggingEvent.Properties[pair.Key] = pair.Value; + } + } + + EnrichWithScopes(loggingEvent, scopeProvider); + + return loggingEvent; + } + + + /// + /// Gets the scopes from the external scope provider and converts them to the properties on the logging event. + /// This function will honor the convention that logging scopes can provide their own property name, by implementing + /// an where T is and where TKey is + /// . + /// + /// + /// The default implementation will call Convert.ToString(scope, CultureInfo.InvariantCulture) on all scope objects. + /// If you want to do this conversion inside the Log4Net Pipeline, e. g. with a custom layout, you can override this + /// method and change the behaviour. + /// + /// The the scope information will be added to. + /// The external provider for the current logging scope. + protected virtual void EnrichWithScopes(LoggingEvent loggingEvent, IExternalScopeProvider scopeProvider) + { + scopeProvider.ForEachScope((scope, @event) => + { + // This function will add the scopes in the legacy way they were added before the IExternalScopeProvider was introduced, + // to maintain backwards compatibility. + // This pretty much means that we are emulating a LogicalThreadContextStack, which is a stack, that allows pushing + // strings on to it, which will be concatenated with space as a separator. + // See: https://github.com/apache/logging-log4net/blob/47aaf46d5f031ea29d781bac4617bd1bb9446215/src/log4net/Util/LogicalThreadContextStack.cs#L343 + + // Because string implements IEnumerable we first need to check for string. + if (scope is string) + { + string previousValue = @event.Properties[DefaultScopeProperty] as string; + + @event.Properties[DefaultScopeProperty] = JoinOldAndNewValue(previousValue, scope.ToString()); + return; + } + + if (scope is IEnumerable col) + { + foreach (var item in col) + { + if (item is KeyValuePair) + { + var keyValuePair = (KeyValuePair)item; + string previousValue = @event.Properties[keyValuePair.Key] as string; + @event.Properties[keyValuePair.Key] = JoinOldAndNewValue(previousValue, keyValuePair.Value); + continue; + } + + if (item is KeyValuePair) + { + var keyValuePair = (KeyValuePair)item; + string previousValue = @event.Properties[keyValuePair.Key] as string; + + // The current culture should not influence how integers/floats/... are displayed in logging, + // so we are using Convert.ToString which will convert IConvertible and IFormattable with + // the specified IFormatProvider. + string additionalValue = Convert.ToString(keyValuePair.Value, CultureInfo.InvariantCulture); + @event.Properties[keyValuePair.Key] = JoinOldAndNewValue(previousValue, additionalValue); + continue; + } + } + return; + } + + if (scope is object) + { + string previousValue = @event.Properties[DefaultScopeProperty] as string; + string additionalValue = Convert.ToString(scope, CultureInfo.InvariantCulture); + @event.Properties[DefaultScopeProperty] = JoinOldAndNewValue(previousValue, additionalValue); + return; + } + + }, loggingEvent); + } + + private static string JoinOldAndNewValue(string previousValue, string newValue) + { + if (string.IsNullOrEmpty(previousValue)) + { + return newValue; + } + + return previousValue + " " + newValue; + } + } +} diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetProvider.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetProvider.cs new file mode 100644 index 0000000..11e9905 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetProvider.cs @@ -0,0 +1,469 @@ +using log4net; +using log4net.Config; +using log4net.Repository; +using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities; +using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Extensions; +using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Scope; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +using log4net.Appender; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Logging +{ + /// + /// The log4net provider class. + /// + /// + public class Log4NetProvider : ILoggerProvider, ISupportExternalScope + { + /// + /// The loggers collection. + /// + private readonly ConcurrentDictionary loggers = new ConcurrentDictionary(); + + /// + /// Prevents to dispose the object more than single time. + /// + private bool disposedValue = false; + + /// + /// The log4net repository. + /// + public ILoggerRepository Repository; + + /// + /// The provider options. + /// + private Log4NetProviderOptions options; + + /// + /// The external logging scope provider. + /// + /// + /// Reading the offical logging implementations, it seems like we need to handle the case that this might never be set. + /// We handle it with a NullScopeProvider instead of null checks, to make the process of implementing interfaces like + /// less error prone for consumers. + /// + public IExternalScopeProvider ExternalScopeProvider { get; private set; } = NullScopeProvider.Instance; + + /// + /// Initializes a new instance of the class. + /// + public Log4NetProvider() + : this(new Log4NetProviderOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The log4NetConfigFile. + public Log4NetProvider(string log4NetConfigFileName) + : this(new Log4NetProviderOptions(log4NetConfigFileName)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The options. + /// options + /// Wach cannot be true when you are overwriting config file values with values from configuration section. + public Log4NetProvider(Log4NetProviderOptions options) + { + this.SetOptionsIfValid(options); + + Assembly loggingAssembly = GetLoggingReferenceAssembly(); + + this.CreateLoggerRepository(loggingAssembly) + .ConfigureLog4NetLibrary(loggingAssembly); + } + + #region IOC implementation + + /// + /// Initializes a new instance of the class. + /// + /// A reference to the IOC service collection. + public Log4NetProvider(IServiceProvider serviceCollection) + : this(new Log4NetProviderOptions(), serviceCollection) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The log4NetConfigFile. + /// A reference to the IOC service collection. + public Log4NetProvider(string log4NetConfigFileName, IServiceProvider serviceProvider) + : this(new Log4NetProviderOptions(log4NetConfigFileName), serviceProvider) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The options. + /// A reference to the IOC service collection. + /// options + /// Watch cannot be true when you are overwriting config file values with values from configuration section. + public Log4NetProvider(Log4NetProviderOptions options, IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + + this.SetOptionsIfValid(options); + + Assembly loggingAssembly = GetLoggingReferenceAssembly(); + + this.CreateLoggerRepository(loggingAssembly) + .ConfigureLog4NetLibrary(loggingAssembly); + } + + /// + /// Holds a reference to the IOC Service Provider + /// + private IServiceProvider serviceProvider; + + #endregion + + /// + /// Finalizes the instance of the object. + /// + ~Log4NetProvider() + { + Dispose(false); + } + + /// + /// Creates the logger. + /// + /// An instance of the . + public ILogger CreateLogger() + => this.CreateLogger(this.options.Name); + + /// + /// Creates the logger. + /// + /// The category name. + /// An instance of the . + public ILogger CreateLogger(string categoryName) + => this.loggers.GetOrAdd(categoryName, this.CreateLoggerImplementation); + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.Repository.Shutdown(); + this.loggers.Clear(); + + serviceProvider = null; + } + + disposedValue = true; + } + } + + /// + /// Updates configuration nodes overriding values if required. + /// + /// The configuration file XML document. + /// The overriding values available + /// An within the overriding values replaced. + private static XmlDocument UpdateNodesWithOverridingValues(XmlDocument configXmlDocument, IEnumerable overridingNodes) + { + var additionalConfig = overridingNodes; + if (additionalConfig != null) + { + var configXDoc = configXmlDocument.ToXDocument(); + foreach (var nodeInfo in additionalConfig) + { + var node = configXDoc.XPathSelectElement(nodeInfo.XPath); + if (node != null) + { + if (nodeInfo.NodeContent != null) + { + node.Value = nodeInfo.NodeContent; + } + + AddOrUpdateAttributes(node, nodeInfo); + } + } + + return configXDoc.ToXmlDocument(); + } + + return configXmlDocument; + } + + /// + /// Adds or updates the attributes specified in the node information. + /// + /// The node. + /// The node information. + private static void AddOrUpdateAttributes(XElement node, NodeInfo nodeInfo) + { + if (nodeInfo?.Attributes != null) + { + foreach (var attribute in nodeInfo.Attributes) + { + var nodeAttribute = node.Attributes() + .FirstOrDefault(a => a.Name.LocalName.Equals(attribute.Key, StringComparison.OrdinalIgnoreCase)); + if (nodeAttribute != null) + { + nodeAttribute.Value = attribute.Value; + } + else + { + node.SetAttributeValue(attribute.Key, attribute.Value); + } + } + } + } + + /// + /// Parses log4net config file. + /// + /// The filename. + /// The with the log4net XML element. + private static XmlDocument ParseLog4NetConfigFile(string filename) + { + using (FileStream stream = File.OpenRead(filename)) + { + var settings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit + }; + + var log4netConfig = new XmlDocument(); + using (var reader = XmlReader.Create(stream, settings)) + { + log4netConfig.Load(reader); + } + + return log4netConfig; + } + } + + /// + /// Tries to retrieve the assembly from a "Startup" type found in the stack trace. + /// + /// Null for NetCoreApp 1.1, otherwise, Assembly of Startup type if found in stack trace. + private static Assembly GetCallingAssemblyFromStartup() + { + var stackTrace = new System.Diagnostics.StackTrace(2); + + for (int i = 0; i < stackTrace.FrameCount; i++) + { + var frame = stackTrace.GetFrame(i); + var type = frame.GetMethod()?.DeclaringType; + + if (string.Equals(type?.Name, "Startup", StringComparison.OrdinalIgnoreCase)) + { + return type.Assembly; + } + } + + return null; + } + + /// + /// Creates the logger implementation. + /// + /// The name. + /// The instance. + private Log4NetLogger CreateLoggerImplementation(string name) + { + var loggerOptions = new Log4NetProviderOptions + { + Name = name, + LoggerRepository = this.Repository.Name, + OverrideCriticalLevelWith = this.options.OverrideCriticalLevelWith, + LoggingEventFactory = this.options.LoggingEventFactory ?? new Log4NetLoggingEventFactory(), + LogLevelTranslator = this.options.LogLevelTranslator ?? new Log4NetLogLevelTranslator(), + }; + + return new Log4NetLogger(loggerOptions, ExternalScopeProvider); + } + + /// + /// Gets the current executing assembly considering the target framework. + /// + /// The assembly to be used as the reference logging assembly. + private static Assembly GetLoggingReferenceAssembly() + { + Assembly assembly = null; + + assembly = Assembly.GetExecutingAssembly(); + + return assembly ?? GetCallingAssemblyFromStartup(); + } + + /// + /// Ensures that provided options combinations are valid, and sets the class field if everything is ok. + /// + /// The options to validate. + /// + /// Throws when the Watch option is set and there are properties to override. + /// + /// + /// Throws when the options parameter is null. + /// + private void SetOptionsIfValid(Log4NetProviderOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.Watch + && options.PropertyOverrides.Any()) + { + throw new NotSupportedException("Wach cannot be true when you are overwriting config file values with values from configuration section."); + } + + this.options = options; + } + + /// + /// Configures the log4net library using the available configuration data. + /// + /// The assembly to be used on the configuration. + private Log4NetProvider ConfigureLog4NetLibrary(Assembly assembly) + { + if (this.options.UseWebOrAppConfig) + { + XmlConfigurator.Configure(this.Repository); + return this; + } + + if (!this.options.ExternalConfigurationSetup) + { + string fileNamePath = CreateLog4NetFilePath(assembly); + if (this.options.Watch) + { + XmlConfigurator.ConfigureAndWatch( + this.Repository, + new FileInfo(fileNamePath)); + } + else + { + var configXml = ParseLog4NetConfigFile(fileNamePath); + if (this.options.PropertyOverrides != null + && this.options.PropertyOverrides.Any()) + { + configXml = UpdateNodesWithOverridingValues( + configXml, + this.options.PropertyOverrides); + } + + XmlConfigurator.Configure(this.Repository, configXml.DocumentElement); + } + } + + this.InjectServices(); + + return this; + } + + /// + /// Wires up specific adapters for IOC support + /// + private void InjectServices() + { + if (this.Repository is null) + return; + + IEnumerable adapters = + this.Repository + .GetAppenders() + .OfType(); + + foreach (IAppenderServiceProvider adapter in adapters) + adapter.ServiceProvider = serviceProvider; + } + + /// + /// Creates the log4net.config file path. + /// + /// The assembly to be used when the configuration indicate to use the current assembly. + /// The full path to the log4net.config file. + private string CreateLog4NetFilePath(Assembly assembly) + { + string fileNamePath = this.options.Log4NetConfigFileName; + if (!Path.IsPathRooted(fileNamePath)) + { + fileNamePath = Path.Combine(AppContext.BaseDirectory, fileNamePath); + } + + return Path.GetFullPath(fileNamePath); + } + + /// + /// Gets or creates the logger repository using the given assembly. + /// + /// The assembly to be used to create de repository. + private Log4NetProvider CreateLoggerRepository(Assembly assembly) + { + Type repositoryType = typeof(log4net.Repository.Hierarchy.Hierarchy); + + if (!string.IsNullOrEmpty(this.options.LoggerRepository)) + { + try + { + this.Repository = LogManager.GetRepository(this.options.LoggerRepository); + if (this.options.ExternalConfigurationSetup) + { + // The logger repository is already configured. We can exit here. + return this; + } + } + catch (log4net.Core.LogException) + { + // The logger repository is not defined outside the extension. + this.Repository = null; + } + + if (this.Repository == null) + { + this.Repository = + LogManager.CreateRepository(this.options.LoggerRepository, repositoryType); + } + } + else + { + this.Repository = + LogManager.CreateRepository(assembly, repositoryType); + } + + return this; + } + + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + ExternalScopeProvider = scopeProvider; + } + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetProviderOptions.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetProviderOptions.cs new file mode 100644 index 0000000..6c020a7 --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Log4NetProviderOptions.cs @@ -0,0 +1,99 @@ +using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Entities; +using Microsoft.Extensions.Logging.Log4Net.AspNetCore.Scope; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Logging +{ + /// + /// The log4Net provider options. + /// + public sealed class Log4NetProviderOptions + { + /// + /// The default log4 net file name + /// + private const string DefaultLog4NetFileName = "log4net.config"; + + /// + /// Initializes a new instance of the class. + /// + public Log4NetProviderOptions() + : this(DefaultLog4NetFileName) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the log4 net configuration file. + public Log4NetProviderOptions(string log4NetConfigFileName) + : this(log4NetConfigFileName, false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the log4net configuration file. + public Log4NetProviderOptions(string log4NetConfigFileName, bool watch) + { + this.Log4NetConfigFileName = log4NetConfigFileName; + this.Watch = watch; + + this.OverrideCriticalLevelWith = string.Empty; + this.Name = string.Empty; + this.PropertyOverrides = new List(); + this.ExternalConfigurationSetup = false; + } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the name of the log file. + /// + public string Log4NetConfigFileName { get; set; } + + /// + /// Gets or sets the logger repository. + /// + public string LoggerRepository { get; set; } + + /// + /// Gets or sets the level value that should be used to override default's critical level. + /// + public string OverrideCriticalLevelWith { get; set; } + + /// + /// Gets or sets the property overrides. + /// + public List PropertyOverrides { get; set; } + + /// + /// Gets or sets a value indicating whether this is watch. + /// + public bool Watch { get; set; } + + /// + /// Let user setup log4net externally + /// + public bool ExternalConfigurationSetup { get; set; } + + /// + /// Let user setup log4net from web.config / app.config. + /// + public bool UseWebOrAppConfig { get; set; } + + /// + /// Gets or sets the factory for the log4net ."/>. + /// + public ILog4NetLoggingEventFactory LoggingEventFactory { get; set; } + + /// + /// Gets or sets the translator between the and the log4net . + /// + public ILog4NetLogLevelTranslator LogLevelTranslator { get; set; } + } +} \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj new file mode 100644 index 0000000..e4568dd --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj @@ -0,0 +1,59 @@ + + + net7.0 + + + True + False + Microsoft.Extensions.Logging.Log4Net.AspNetCore + Huor Swords + + Microsoft.Extensions.Logging.Log4Net.AspNetCore + + Allows to configure Log4net as Microsoft Extensions Logging handler on any ASP.NET Core application. + + Original code proposal by @anuraj --> https://dotnetthoughts.net/how-to-use-log4net-with-aspnetcore-for-logging/ + + + Microsoft Extensions Logging Log4Net AspNet Core + Microsoft.Extensions.Logging.Log4Net.AspNetCore + Microsoft.Extensions.Logging.Log4Net.AspNetCore + #113 - BeginScope don't work as expected +# BREAKING CHANGES +* External scope can't be used with the scope factories anymore. +* Conversion of numeric values now are managed through CultureInfo.InvariantCulture + + https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore + https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore + 7.1.0 + 7.1.0.0 + 7.1.0.0 + true + Microsoft.Extensions.Logging.Log4Net.AspNetCoreKey.snk + + LICENSE + + + true + snupkg + + + + + + + + + + + + + + + + + True + + + + \ No newline at end of file diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Scope/NullScope.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Scope/NullScope.cs new file mode 100644 index 0000000..1c1198f --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Scope/NullScope.cs @@ -0,0 +1,28 @@ +using System; + +namespace Microsoft.Extensions.Logging.Log4Net.AspNetCore.Scope +{ + /// + /// A logger scope that does not save any information and does not need to be disposed. + /// + internal class NullScope : IDisposable + { + /// + /// The singleton instance that represent every . + /// + internal static NullScope Instance { get; } = new NullScope(); + + /// + /// Constructor that prevents external instantiation. + /// + private NullScope() + { + } + + /// + public void Dispose() + { + // This is a null scope so we need to dispose nothing. + } + } +} diff --git a/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Scope/NullScopeProvider.cs b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Scope/NullScopeProvider.cs new file mode 100644 index 0000000..18ea0dc --- /dev/null +++ b/Resources/3rd Party/Microsoft.Extensions.Logging.Log4Net.AspNetCore/Scope/NullScopeProvider.cs @@ -0,0 +1,34 @@ +using System; + +namespace Microsoft.Extensions.Logging.Log4Net.AspNetCore.Scope +{ + /// + /// A that will not save nor return scopes. + /// + internal class NullScopeProvider : IExternalScopeProvider + { + /// + /// The singleton instance that represents every . + /// + internal static NullScopeProvider Instance { get; } = new NullScopeProvider(); + + /// + /// Constructor that prevents external instantiation. + /// + private NullScopeProvider() + { + } + + /// + public void ForEachScope(Action callback, TState state) + { + // All scopes are null scopes so do nothing. + } + + /// + public IDisposable Push(object state) + { + return NullScope.Instance; + } + } +} diff --git a/Resources/Avalonia.Resources/Assets/avalonia-logo.ico b/Resources/Avalonia.Resources/Assets/avalonia-logo.ico new file mode 100644 index 0000000..da8d49f Binary files /dev/null and b/Resources/Avalonia.Resources/Assets/avalonia-logo.ico differ diff --git a/Resources/Avalonia.Resources/Avalonia.Resources.vbproj b/Resources/Avalonia.Resources/Avalonia.Resources.vbproj new file mode 100644 index 0000000..1178ae0 --- /dev/null +++ b/Resources/Avalonia.Resources/Avalonia.Resources.vbproj @@ -0,0 +1,18 @@ + + + + Avalonia.Resources + net7.0 + true + app.manifest + + + + + + + + + + + diff --git a/Resources/Avalonia.Resources/app.manifest b/Resources/Avalonia.Resources/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/Resources/Avalonia.Resources/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/Screenshots/Architecture_v1.10.png b/Screenshots/Architecture_v1.10.png new file mode 100644 index 0000000..961df98 Binary files /dev/null and b/Screenshots/Architecture_v1.10.png differ diff --git a/Screenshots/Console Logging 640.png b/Screenshots/Console Logging 640.png new file mode 100644 index 0000000..fff5e4f Binary files /dev/null and b/Screenshots/Console Logging 640.png differ diff --git a/Screenshots/Design_640.png b/Screenshots/Design_640.png new file mode 100644 index 0000000..f6e713d Binary files /dev/null and b/Screenshots/Design_640.png differ diff --git a/Screenshots/LogViewerControl - Avalonia 640.png b/Screenshots/LogViewerControl - Avalonia 640.png new file mode 100644 index 0000000..7a5d14b Binary files /dev/null and b/Screenshots/LogViewerControl - Avalonia 640.png differ diff --git a/Screenshots/LogViewerControl - WPF 640.png b/Screenshots/LogViewerControl - WPF 640.png new file mode 100644 index 0000000..aafd5d9 Binary files /dev/null and b/Screenshots/LogViewerControl - WPF 640.png differ diff --git a/Screenshots/LogViewerControl - Winforms 640.png b/Screenshots/LogViewerControl - Winforms 640.png new file mode 100644 index 0000000..9984883 Binary files /dev/null and b/Screenshots/LogViewerControl - Winforms 640.png differ diff --git a/Screenshots/Logger Factory Flow 600.png b/Screenshots/Logger Factory Flow 600.png new file mode 100644 index 0000000..6a0a8f0 Binary files /dev/null and b/Screenshots/Logger Factory Flow 600.png differ diff --git a/Screenshots/Loggers_DI_640.png b/Screenshots/Loggers_DI_640.png new file mode 100644 index 0000000..770732a Binary files /dev/null and b/Screenshots/Loggers_DI_640.png differ diff --git a/Screenshots/Loggers_NoDI_596.png b/Screenshots/Loggers_NoDI_596.png new file mode 100644 index 0000000..069c21d Binary files /dev/null and b/Screenshots/Loggers_NoDI_596.png differ diff --git a/Screenshots/Serilog - DI_507.png b/Screenshots/Serilog - DI_507.png new file mode 100644 index 0000000..21c3657 Binary files /dev/null and b/Screenshots/Serilog - DI_507.png differ diff --git a/Screenshots/Serilog - noDI_516.png b/Screenshots/Serilog - noDI_516.png new file mode 100644 index 0000000..f68835e Binary files /dev/null and b/Screenshots/Serilog - noDI_516.png differ diff --git a/Screenshots/Solution_Layout_v1.20.png b/Screenshots/Solution_Layout_v1.20.png new file mode 100644 index 0000000..f5b2eb0 Binary files /dev/null and b/Screenshots/Solution_Layout_v1.20.png differ diff --git a/Screenshots/logging Provider Flow 394.png b/Screenshots/logging Provider Flow 394.png new file mode 100644 index 0000000..6a6f0fa Binary files /dev/null and b/Screenshots/logging Provider Flow 394.png differ diff --git a/Screenshots/mac_avalonia_logging_vb.gif b/Screenshots/mac_avalonia_logging_vb.gif new file mode 100644 index 0000000..d6ce304 Binary files /dev/null and b/Screenshots/mac_avalonia_logging_vb.gif differ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..d2615ba --- /dev/null +++ b/readme.md @@ -0,0 +1,4124 @@ +Dot Net Core LogViewer Control for WinForms, Wpf, and Avalonia using the ILogger Framework with Microsoft Logger, Serilog, NLog, and Log4Net in C# & VB on Windows, Mac OS, and Linux + +**WinForms**, **WPF**, and **Avalonia** LogViewer controls for live viewing of ILogger entries with full colorization support, and more for **C#** and **VB** on **Windows**, **MacOS** and **Linux** using **Microsoft Logger**, **Serilog**, **NLog**, and **Log4Net** + Logging Demystified + +# Introduction + +I was working on a solution that required a Viewer for **Logger** entries in the app itself for live viewing of what was happening behind the scene. + +I wanted something prettier than the console output and something that could be added to a **Winforms**, **WPF**, or **Avalonia** application that felt part of the application, and possibly something that a user may need to view - ie: User Friendly, not the following: + +![](Screenshots/Console%20Logging%20640.png) + +The requirements for the LoggerViewer are: + +- Defined as a **control** that could be added or injected via dependency injection +- **Native** for **WinForms**, **WPF**, and **Avalonia** applications +- Support multiple Operating Systems - **Windows**, **MacOS**, **Linux** +- Support multiple Logging Frameworks - **Microsoft** (default), **Serilog**, **NLog**, and **Log4Net** +- Support **colorization** (custom colors as a bonus) +- **Dependency Injection** (DI) and non-DI usage +- **MVVM** (Model View ViewModel design pattern) and non-MVVM usage +- **History** viewable in any list control, a `ListView` / `DataGrid` control +- Selectable **auto-scrolling** to keep the latest entry visible +- **AppSettings.Json** file support for configurable logging +- Capture **framework API logging** +- Work in parallel with other Loggers + +We will be looking into Logging - how it works and look at the framework code that makes it work. + +As we will be covering **WPF**, **WinForms**, and **Avalonia** project types, **Microsoft**, **Serilog**, and **NLog** loggers, and also using / not using Dependency Injection, this article will be a bit lengthy. + +If you are not interested in how it all works, then see the animations in the Preview section below, download the code, and run the application(s) that are applicable to your use case in the language that you work in. + +## Preview + +Before we get started, let's look at what we want to achieve. The **WPF**, **WinForms**, and **Avalonia** versions of the `LogViewerControl` look almost identical and work the same for both the **C#** & **VB** versions. + +Here is a GIF with **default** colorization for the **WinForms** version in **C#**, using Dependency Injection and data-binding: + +![](Screenshots/WinForm%20LogViewer.gif) + +Here is a GIF with **custom** colorization for the **WPF** version, minimal implementation in **VB**, no Dependency injection, 3 lines of code: + +![](Screenshots/Wpf%20LogViewer.gif) + +Lastly, here is proof that you can develop an application for **Mac OS** using **VB**, yes Visual Basic, using the **Avalonia** Framework! Whilst VB is not supported out-of-the-box, as there are no included Application, Class, or Control library templates with the exception of a [Github repository](https://github.com/mevdschee/avalonia-vb-template-app) that is not complete, I will cover how to get **VB** to use the **Avalonia** framework for both application and control project types. + +![](Screenshots/mac_avalonia_logging_vb.gif) + +*Note: The 3 animated GIFs may take a moment to load...* + +## Contents + + +* [Introduction](#introduction) + * [Preview](#preview) + * [Contents](#contents) + * [Prerequisites](#prerequisites) + * [Solution Setup](#solution-setup) + * [Logging Flow](#logging-flow) + * [Application Architecture](#application-architecture) + * [Solution Architecture](#solution-architecture) + * [How Does Logging Work?](#how-does-logging-work) + * [Logger Internals](#logger-internals) + * [Custom Loggers](#custom-loggers) + * [Shared Logging Data](#shared-logging-data) + * [Storage - `LogDataStore` and `LogModel` classes](#storage---logdatastore-and-logmodel-classes) + * [Configuration - `DataStoreLoggerConfiguration` class and `LogEntryColor` class](#configuration---datastoreloggerconfiguration-class-and-logentrycolor-class) + * [Custom Microsoft Logger Implementation](#custom-microsoft-logger-implementation) + * [Logger - `DataStoreLogger` class](#logger---datastorelogger-class) + * [Logger Provider - `DataStoreLoggerProvider` class](#logger-provider---datastoreloggerprovider-class) + * [Registering Microsoft Loggers](#registering-microsoft-loggers) + * [Registration - `ServicesExtension` class](#registration---servicesextension-class) + * [Dependency Injection](#dependency-injection) + * [Manually (without Dependency Injection)](#manually-without-dependency-injection) + * [Custom Serilog Logger Implementation](#custom-serilog-logger-implementation) + * [Logger - `DataStoreLoggerSink` class](#logger---datastoreloggersink-class) + * [Configuring the Custom Sink - `DataStoreLoggerSinkExtensions` class](#configuring-the-custom-sink---datastoreloggersinkextensions-class) + * [Registering Sinks (Loggers)](#registering-sinks-loggers) + * [Dependency Injection](#dependency-injection-1) + * [Manually (without Dependency Injection)](#manually-without-dependency-injection-1) + * [Custom NLog Target Logger Implementation](#custom-nlog-target-logger-implementation) + * [Logger - `DataStoreLoggerTarget` class](#logger---datastoreloggertarget-class) + * [Configuring the Custom Target - `ServicesExtension` class](#configuring-the-custom-target---servicesextension-class) + * [Registering Targets (Loggers)](#registering-targets-loggers) + * [Dependency Injection](#dependency-injection-2) + * [Manually (without Dependency Injection)](#manually-without-dependency-injection-2) + * [Custom Apache Log4Net Appender Logger Implementation](#custom-apache-log4net-appender-logger-implementation) + * [Adding missing parts to Microsoft.Extensions.Logging.Log4Net.AspNetCore](#adding-missing-parts-to-microsoftextensionslogginglog4netaspnetcore) + * [Adding EventID support](#adding-eventid-support) + * [Adding Dependency Injection support for the Appender support](#adding-dependency-injection-support-for-the-appender-support) + * [Logger - `DataStoreLoggerAppender` class](#logger---datastoreloggerappender-class) + * [Configuring the Custom Appender - `ServicesExtension` class](#configuring-the-custom-appender---servicesextension-class) + * [Registering Appenders (Loggers)](#registering-appenders-loggers) + * [Dependency Injection](#dependency-injection-3) + * [Manually (without Dependency Injection)](#manually-without-dependency-injection-3) + * [Processing Log Entries](#processing-log-entries) + * [Dependency Injection](#dependency-injection-4) + * [Manually (without Dependency Injection)](#manually-without-dependency-injection-4) + * [Listening for new Entries](#listening-for-new-entries) + * [Manual Handling of the CollectionChanged events](#manual-handling-of-the-collectionchanged-events) + * [LogViewerControl Implementation](#logviewercontrol-implementation) + * [`DispatcherHelper` Class](#dispatcherhelper-class) + * [WinForms Implementation](#winforms-implementation) + * [WPF Implementation](#wpf-implementation) + * [Common code - `LogViewer.Core` project](#common-code---logviewercore-project) + * [`LoggerExtensions` class](#loggerextensions-class) + * [ViewModel: `LogViewerControlViewModel` Class](#viewmodel-logviewercontrolviewmodel-class) + * [WinForms - LogViewerControl](#winforms---logviewercontrol) + * [Code Behind](#code-behind) + * [WPF - LogViewerControl](#wpf---logviewercontrol) + * [Code-behind](#code-behind-1) + * [User Interface](#user-interface) + * [Avalonia - LogViewerControl](#avalonia---logviewercontrol) + * [Code-behind](#code-behind-2) + * [User Interface](#user-interface-1) + * [Using the LogViewControl](#using-the-logviewcontrol) + * [WinForms - Dependency Injection](#winforms---dependency-injection) + * [Registration - `ServicesExtension` class](#registration---servicesextension-class-1) + * [`MainForm` Code-Behind](#mainform-code-behind) + * [Registration - `Bootstrapper` class](#registration---bootstrapper-class) + * [Usage](#usage) + * [WinForms - Manually (without Dependency Injection)](#winforms---manually-without-dependency-injection) + * [`MainForm` Code-Behind](#mainform-code-behind-1) + * [WPF - Dependency Injection](#wpf---dependency-injection) + * [Registration - `ServicesExtension` class](#registration---servicesextension-class-2) + * [`MainWindow` - `LogViewerControl` Host](#mainwindow---logviewercontrol-host) + * [`MainViewModel` class](#mainviewmodel-class) + * [Registration - App (C#) / Application (VB) class](#registration---app-c--application-vb-class) + * [Usage](#usage-1) + * [WPF - Manually (without Dependency Injection)](#wpf---manually-without-dependency-injection) + * [`MainWindow` XAML - `LogViewerControl` Host](#mainwindow-xaml---logviewercontrol-host) + * [`MainWindow` Code-behind](#mainwindow-code-behind) + * [Avalonia - Dependency Injection](#avalonia---dependency-injection) + * [Registration - `ServicesExtension` class](#registration---servicesextension-class-3) + * [`MainWindow` - `LogViewerControl` Host](#mainwindow---logviewercontrol-host-1) + * [`MainViewModel` class](#mainviewmodel-class-1) + * [Registration - App (C#) / Application (VB) class](#registration---app-c--application-vb-class-1) + * [Usage](#usage-2) + * [Avalonia - Manually (without Dependency Injection)](#avalonia---manually-without-dependency-injection) + * [`MainWindow` XAML - `LogViewerControl` Host](#mainwindow-xaml---logviewercontrol-host-1) + * [`MainWindow` Code-behind](#mainwindow-code-behind-1) + * [Generating Sample Log Messages](#generating-sample-log-messages) + * [Background Service - `RandomLoggingService` class](#background-service---randomloggingservice-class) + * [Dependency Injection](#dependency-injection-5) + * [Registration](#registration) + * [Usage](#usage-3) + * [Manually (without Dependency Injection)](#manually-without-dependency-injection-5) + * [LoggerMessageAttribute (C# only)](#loggermessageattribute) + * [Dedicated Application Logging method](#dedicated-application-logging-method) + * [Dedicated RandomLoggingService Logging method](#dedicated-randomloggingservice-logging-method) + * [RandomLoggingService](#randomloggingservice) + * [Summary](#summary) + * [References](#references) + * [Documentation, Articles, etc](#documentation-articles-etc) + * [Nuget Packages](#nuget-packages) + * [History](#history) + +## Prerequisites + +The code that accompanies this article is for Dot Net Core only. Version 7.03 was used and Nullable is enabled. However, if required, it can be modified to support Dot Net 3.1 or later. + +The Solution was built using **Visual Studio 2022 v17.4.5** and fully tested with **Rider 2022.3.2**. + +The Nuget Packages that were used for this article are listed in the Nuget Packages reference section at the end of this article. + +The `AppSettings` helper class was used to simplify reading the configuration settings from the `appsettings*.json` files. There is an article that deep-dives into how this works: [.NET App Settings Demystified (C# & VB | CodeProject)](https://www.codeproject.com/Articles/5354478/NET-App-Settings-Demystified-Csharp-VB). + +If you are not familiar with Logging then take a moment to read this [Logging in .NET | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) which covers the fundamentals. + +As we are implementing a Custom Logger and Provider, and you're not familiar with creating a custom logger and provider, please take a moment to read [Implement a custom logging provider in .NET | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/extensions/custom-logging-provider). + +We will also be covering Dependency Injection (DI). I provide solutions that use and do not use DI, so DI is not essential. If you are interested in learning more, please read this: [Dependency injection in .NET | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection). + +Lastly, we will be covering MVVM (Model View ViewModel design pattern). I provide solutions that use and do not use MVVM, so MVVM is not essential. If you are interested in learning more, please read this: [Model-View-ViewModel (MVVM) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/architecture/maui/mvvm#the-mvvm-pattern). + +## Solution Setup + +As we are covering 3 project types, The structure of the solution attempts to minimize duplication of code. Also, the projects are broken into 4 parts: Application, Controls, Core, and Background Service: + +1. Application demonstrates how to implement in your own applications +2. Controls are what you add to your own applications for the UI component +3. Core contains common code, application type-specific code, and the custom logger implementations. The custom logger implementations are independent of the controls, and choose which one or roll your own for another logger framework. +4. The Background Service is simply a dummy service to simulate the generation of logging messages. The Service is common to all application types. + +### Logging Flow + +We can simplify the design concept with the diagram below: + +![](Screenshots/Design_640.png) + +The logic flow, as per the diagram above, is as follows: + +1. Application logs an event (Trace, Debug, Information, Warning, Error, or Critical) with the appropriate information +2. The Logger Framework passes the Log Event to all registered Loggers, including our custom logger(s) +3. The Loggers store the Log Event in the DataStore +4. The LogViewer control receives a data-binding notification and displays the Log Event + +### Application Architecture + +The application architecture is the same for all application types: + +![](Screenshots/Architecture_v1.10.png) + +**NOTES:** +* Application, Controls, and Common parts are UI & application type dependant +* Logger Providers are Logging Framework specific +* Controls and Common parts are application type specific +* Logger Providers, Random Logging Service, and Controls are all independent of each other + +### Solution Architecture + +Both **VB** and **C#** solutions are included and have identical layouts. The only difference is the **VB** version has **VB** at the end of the project name. + +![](Screenshots/Solution_Layout_v1.20.png) + +**NOTES:** + +- The application project names are made up of 3 parts: **[Application Type][Logger][Implementation]** + 1. Application Type: **Avalonia**, **WinForms**, **Wpf** + 2. Logger: Logger (Default Dot Net Implementation) or Serilog + 3. Implementation: DI = Dependency Injection; NoDI = Manual / No Dependency Injection +- For supporting Projects the Name Suffix identifies the project type + 1. **.Core** for common code + 2. **.Avalonia**, **.WinForms**, **.Wpf** for application-specific types + +## How Does Logging Work? + +Before we dig into the solutions, let us quickly look at how the Dot Net Logging Framework works. + +There are three parts: + +1. Logger +2. Registering Loggers +3. Processing Log Entries + +We will be using the [Microsoft Logger Framework](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging). This will allow us to not only capture the application's logging, but all Dot Net (Core) and 3rd-party library logging. + +The implementation in this article will be using a singleton DataStore for storage, Custom Logger, and Logging Provider. There is also a Configuration class for custom options, like custom colorization. + +This is just a brief summation and look at the internal code. If you require more information, please the links provided above and in the Reference section at the end of this article. + +### Logger Internals + +Loggers are made up of 4 parts: + +1. Logger - logging implementation +2. LoggingProvider - Generates the Logger instance +3. Processor / Storage - where the logger outputs the logging to +4. Configuration (optional) - parameters for generating output + +![](Screenshots/logging%20Provider%20Flow%20394.png) + +Every time the `LoggingFactory` creates a `Logger` instance, the `LoggingFactory` will cycle through all of the registered `Logger Providers` and generate internal `Logger` instances for the returned concrete `Logger`. All calls to the `Log` method on the concrete `Logger` will cycle through all of the internal `Logger` instances. + +![](Screenshots/Logger%20Factory%20Flow%20600.png) + +To understand this better, let's look at the code in the **Dot Net Framework** `LoggerFactory` class that creates the `Logger` instance that we use: + +```csharp +public ILogger CreateLogger(string categoryName) +{ + if (CheckDisposed()) + { + throw new ObjectDisposedException(nameof(LoggerFactory)); + } + + lock (_sync) + { + if (!_loggers.TryGetValue(categoryName, out Logger? logger)) + { + logger = new Logger(CreateLoggers(categoryName)); + + (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers); + + _loggers[categoryName] = logger; + } + + return logger; + } +} + +private LoggerInformation[] CreateLoggers(string categoryName) +{ + var loggers = new LoggerInformation[_providerRegistrations.Count]; + for (int i = 0; i < _providerRegistrations.Count; i++) + { + loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider, categoryName); + } + return loggers; +} + +internal readonly struct LoggerInformation +{ + public LoggerInformation(ILoggerProvider provider, string category) : this() + { + ProviderType = provider.GetType(); + Logger = provider.CreateLogger(category); + Category = category; + ExternalScope = provider is ISupportExternalScope; + } + + public ILogger Logger { get; } + + public string Category { get; } + + public Type ProviderType { get; } + + public bool ExternalScope { get; } +} +``` + +Here we see everything being wired up, including the `LoggerProvier` generating the internal `Loggers` via the `CreateLoggers` method. + +Then every time we Log an entry via our `Logger`, the information is passed to every internal `Logger`. + +Here is the concrete **Dot Net Framework** internal `Logger` that is substantiated by the `LoggerFactory`. We will look specifically at the `Log` method: + +```csharp + internal sealed class Logger : ILogger + { + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + MessageLogger[]? loggers = MessageLoggers; + if (loggers == null) + { + return; + } + + List? exceptions = null; + for (int i = 0; i < loggers.Length; i++) + { + ref readonly MessageLogger loggerInfo = ref loggers[i]; + if (!loggerInfo.IsEnabled(logLevel)) + { + continue; + } + + LoggerLog(logLevel, eventId, loggerInfo.Logger, exception, formatter, ref exceptions, state); + } + + if (exceptions != null && exceptions.Count > 0) + { + ThrowLoggingError(exceptions); + } + + static void LoggerLog(LogLevel logLevel, EventId eventId, ILogger logger, Exception? exception, Func formatter, ref List? exceptions, in TState state) + { + try + { + logger.Log(logLevel, eventId, state, exception, formatter); + } + catch (Exception ex) + { + exceptions ??= new List(); + exceptions.Add(ex); + } + } + } + // trimmed + } +``` + +Here we can see it passes the information to all registered internal `Loggers`. + +## Custom Loggers + +The **Dot Net Framework** has a default Microsoft Logger Framework that can be used. There are also many 3rd-party Logging Framework. This article will look at two (2) Logging Frameworks: + +1. Microsoft Logger Framework (built-in) +2. Serilog Logger Framework for structured logging + +The **LogViewerControl** uses the built in logging framework. For [Serilog](https://serilog.net/), we will look at how to create a custom sink (logger) and hook into the built in logging framework. + +### Shared Logging Data + +Before we look at implementing custom loggers, we need to set up log entry storage and logger configuration. + +#### Storage - `LogDataStore` and `LogModel` classes + +```csharp +public interface ILogDataStore +{ + ObservableCollection Entries { get; } + void AddEntry(LogModel logModel); +} + +public class LogDataStore : ILogDataStore +{ + #region Fields + + private static readonly SemaphoreSlim _semaphore = new(initialCount: 1); + + #endregion + + #region Properties + + public ObservableCollection Entries { get; } = new(); + + #endregion + + #region Methods + + public virtual void AddEntry(LogModel logModel) + { + // ensure only one operation at time from multiple threads + _semaphore.Wait(); + + Entries.Add(logModel); + + _semaphore.Release(); + } + + #endregion +} +``` + +The data model to hold each log entry: + +```csharp +public class LogModel +{ + #region Properties + + public DateTime Timestamp { get; set; } + + public LogLevel LogLevel { get; set; } + + public EventId EventId { get; set; } + + public object? State { get; set; } + + public string? Exception { get; set; } + + public LogEntryColor? Color { get; set; } + + #endregion +} +``` + +**NOTES:** The `LogDataStore` class is initialized as a singleton. To process any entries added to the `LogDataStore` class, an `ObservableCollection` is used. For the application to process entries, all that is required is listening to the `CollectionChanged` event for this collection. This will be covered later in the article in the section ???. + +#### Configuration - `DataStoreLoggerConfiguration` class and `LogEntryColor` class + +The `DataStoreLoggerConfiguration` class is for optional customization. + +```csharp +public class DataStoreLoggerConfiguration +{ + #region Properties + + public EventId EventId { get; set; } + + public Dictionary Colors { get; } = new() + { + [LogLevel.Trace] = new() { Foreground = Color.DarkGray }, + [LogLevel.Debug] = new() { Foreground = Color.Gray }, + [LogLevel.Information] = new(), + [LogLevel.Warning] = new() { Foreground = Color.Orange}, + [LogLevel.Error] = new() { Foreground = Color.White, Background = Color.OrangeRed }, + [LogLevel.Critical] = new() { Foreground=Color.White, Background = Color.Red }, + [LogLevel.None] = new(), + }; + + #endregion +} +``` + +The data model to hold each log level display colors: + +```csharp +public class LogEntryColor +{ + public Color Foreground { get; set; } = Color.Black; + public Color Background { get; set; } = Color.Transparent; + +} +``` + +### Custom Microsoft Logger Implementation + +**Microsoft Loggers** are made up of 2 parts, in this case: + +1. Logger - `DataStoreLogger` +2. LoggingProvider - `DataStoreLoggerProvider` which will generate the `DataStoreLogger` instance + +#### Logger - `DataStoreLogger` class + +```csharp +public class DataStoreLogger: ILogger +{ + #region Constructor + + public DataStoreLogger( + string name, + Func getCurrentConfig, + ILogDataStore dataStore) + { + (_name, _getCurrentConfig) = (name, getCurrentConfig); + _dataStore = dataStore; + } + + #endregion + + #region Fields + + private readonly ILogDataStore _dataStore; + private readonly string _name; + private readonly Func _getCurrentConfig; + + #endregion + + #region methods + + public IDisposable BeginScope(TState state) where TState : notnull => default!; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + // check if we are logging for passed log level + if (!IsEnabled(logLevel)) + return; + + DataStoreLoggerConfiguration config = _getCurrentConfig(); + + _dataStore.AddEntry(new() + { + Timestamp = DateTime.UtcNow, + LogLevel = logLevel, + // do we override the default EventId if it exists? + EventId = eventId.Id == 0 && config.EventId != 0 ? config.EventId : eventId, + State = state, + Exception = exception?.Message ?? (logLevel == LogLevel.Error ? state?.ToString() ?? "" : ""), + Color = config.Colors[logLevel], + }); + + Debug.WriteLine($"--- [{logLevel.ToString()[..3]}] {_name} - {formatter(state, exception!)}"); + } + + #endregion +} +``` + +**NOTES:** The `Log` method in the custom `DataStoreLogger` adds the log to our `LogDataStore`. + +#### Logger Provider - `DataStoreLoggerProvider` class + +```csharp +public class DataStoreLoggerProvider: ILoggerProvider +{ + + #region Constructor + + public DataStoreLoggerProvider( + IOptionsMonitor config, + ILogDataStore dataStore) + { + _dataStore = dataStore; + _currentConfig = config.CurrentValue; + _onChangeToken = config.OnChange( + updatedConfig => _currentConfig = updatedConfig); + } + + #endregion + + #region fields + + private DataStoreLoggerConfiguration _currentConfig; + + private readonly IDisposable? _onChangeToken; + protected readonly ILogDataStore _dataStore; + + protected readonly ConcurrentDictionary _loggers = new(); + + #endregion + + #region Methods + + public ILogger CreateLogger(string categoryName) + => _loggers.GetOrAdd(categoryName, name + => new DataStoreLogger(name, GetCurrentConfig, _dataStore)); + + protected DataStoreLoggerConfiguration GetCurrentConfig() + => _currentConfig; + + public void Dispose() + { + _loggers.Clear(); + _onChangeToken?.Dispose(); + } + + #endregion +} +``` + +**NOTES:** When the `DataStoreLogger` is created, the `DataStoreLoggerConfiguration` and `LogDataStore` are injected. + +#### Registering Microsoft Loggers + +Microsoft Loggers are Registered as a Framework `HostApplicationBuilder` service via the `ILoggingBuilder`. + +Here is the trimmed code for the **Dot Net Framework** `HostApplicationBuilder` class: + +```csharp +/// +/// A builder for hosted applications and services which helps manage configuration, logging, lifetime and more. +/// +public sealed class HostApplicationBuilder +{ + private readonly ServiceCollection _serviceCollection = new(); + + // trimmed + + public HostApplicationBuilder(HostApplicationBuilderSettings? settings) + { + // trimmed + Logging = new LoggingBuilder(Services); + // trimmed + } + + // trimmed + + /// + /// A collection of services for the application to compose. This is useful for adding user provided or framework provided services. + /// + public IServiceCollection Services => _serviceCollection; + + /// + /// A collection of services for the application to compose. This is useful for adding user provided or framework provided services. + /// + public IServiceCollection Services => _serviceCollection; + + /// + /// A collection of logging providers for the application to compose. This is useful for adding new logging providers. + /// + public ILoggingBuilder Logging { get; } + + // trimmed + + private sealed class LoggingBuilder : ILoggingBuilder + { + public LoggingBuilder(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + } + + // trimmed +} +``` + +#### Registration - `ServicesExtension` class + +The registration of the `LogDataStore`, `DataStoreLoggerConfiguration`, and `DataStoreLoggerProvider` classes are abstracted to an extension method in the `ServicesExtension` class: + +```csharp +public static class ServicesExtension +{ + public static ILoggingBuilder AddDefaultDataStoreLogger(this ILoggingBuilder builder) + { + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + return builder; + } + + public static ILoggingBuilder AddDefaultDataStoreLogger(this ILoggingBuilder builder, Action configure) + { + builder.AddDefaultDataStoreLogger(); + builder.Services.Configure(configure); + return builder; + } +} +``` + +#### Dependency Injection + +Here is an example of wiring up the Dependency Injection with the default configuration: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.AddLogViewer(); +builder.Logging.AddDefaultDataStoreLogger(); + +_host = builder.Build(); +``` + +Or, if a custom configuration is to be used: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.AddLogViewer(); +builder.Logging.AddDefaultDataStoreLogger(options => +{ + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; +}); + +_host = builder.Build(); +``` + +To create a logger, you can Inject an instance into a class constructor: + +```csharp +public class RandomLoggingService : BackgroundService +{ + #region Constructors + + public RandomLoggingService(ILogger logger) + => _logger = logger; + + #endregion + + #region Fields + + private readonly ILogger _logger; + + #endregion +} +``` + +Or request an instance manually: + +```csharp +ILogger logger = _host.Services.GetRequiredService>(); +``` + +And here is a sample screenshot of the logger instance with substantiated logger internals: + +![](Screenshots/Loggers_DI_640.png) + +#### Manually (without Dependency Injection) + +If not using Dependency Injection, it is still possible to register one or more loggers. We will require a singleton class to hold the registration and Factory method for generating Logger instances. + +Here is the `LoggingHelper` class used with the sample applications in this article: + +```csharp +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + // retrieve the log level from 'appsettings' + string value = AppSettings.Current("Logging:LogLevel", "Default") ?? "Information"; + Enum.TryParse(value, out LogLevel logLevel); + + // wire up the loggers + Factory = LoggerFactory.Create(builder => builder + + // visual debugging tools + .AddDataStoreLogger() + + // examples of adding other loggers... + .AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + }) + + // set minimum log level from 'appsettings' + .SetMinimumLevel(logLevel)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion +} +``` + +Or, if a custom configuration is to be used: + +```csharp +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + // retrieve the log level from 'appsettings' + string value = AppSettings.Current("Logging:LogLevel", "Default") ?? "Information"; + Enum.TryParse(value, out LogLevel logLevel); + + // wire up the loggers + Factory = LoggerFactory.Create(builder => builder + + // visual debugging tools + .AddDataStoreLogger(options => + { + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; + }) + + // examples of adding other loggers... + .AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + }) + + // set minimum log level from 'appsettings' + .SetMinimumLevel(logLevel)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion +} +``` + +To create a logger, use the `Factory` method of the `LoggingHelper` class above: + +```csharp +Logger logger = new Logger(LoggingHelper.Factory); +``` + +**NOTE:** + +When creating Loggers, the class needs to be substantiated/Created. If the class is not, an error will be thrown. + +Creating the logger as a constructor parameter is acceptable. For example, the following is acceptable: + +```csharp +RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); +``` + +And here is a sample screenshot of the logger instance with substantiated logger internals: + +![](Screenshots/Loggers_NoDI_596.png) + +### Custom Serilog Logger Implementation + +**Serilog Sinks (Loggers)** have a different implementation to the Microsoft Logger implementation. However, to work with the Microsoft Logging Framework, Serilog implements the Logger Provider so the Microsoft Logging Framework can pass data to the Serilog sinks (Logger implementations). + +#### Logger - `DataStoreLoggerSink` class + +```csharp +public class DataStoreLoggerSink : ILogEventSink +{ + protected readonly Func _dataStoreProvider; + + private readonly IFormatProvider? _formatProvider; + private readonly Func? _getCurrentConfig; + + public DataStoreLoggerSink(Func dataStoreProvider, + Func? getCurrentConfig = null, + IFormatProvider? formatProvider = null) + { + _formatProvider = formatProvider; + _dataStoreProvider = dataStoreProvider; + _getCurrentConfig = getCurrentConfig; + } + + public void Emit(LogEvent logEvent) + { + LogLevel logLevel = logEvent.Level switch + { + LogEventLevel.Verbose => LogLevel.Trace, + LogEventLevel.Debug => LogLevel.Debug, + LogEventLevel.Warning => LogLevel.Warning, + LogEventLevel.Error => LogLevel.Error, + LogEventLevel.Fatal => LogLevel.Critical, + _ => LogLevel.Information + }; + + DataStoreLoggerConfiguration config = + _getCurrentConfig?.Invoke() ?? new DataStoreLoggerConfiguration(); + + EventId eventId = EventIdFactory(logEvent); + if (eventId.Id == 0 && config.EventId != 0) + eventId = config.EventId; + + string message = logEvent.RenderMessage(_formatProvider); + + string exception = + logEvent.Exception?.Message ?? (logEvent.Level >= LogEventLevel.Error + ? message + : string.Empty); + + LogEntryColor color = config.Colors[logLevel]; + + AddLogEntry(logLevel, eventId, message, exception, color); + } + + protected virtual void AddLogEntry( + LogLevel logLevel, + EventId eventId, + string message, + string exception, + LogEntryColor color) + { + ILogDataStore? dataStore = _dataStoreProvider.Invoke(); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (dataStore == null) + return; // app is shutting down + + dataStore.AddEntry(new() + { + Timestamp = DateTime.UtcNow, + LogLevel = logLevel, + EventId = eventId, + State = message, + Exception = exception, + Color = color + }); + } + + private static EventId EventIdFactory(LogEvent logEvent) + { + EventId eventId; + if (!logEvent.Properties.TryGetValue("EventId", out LogEventPropertyValue? src)) + return new(); + + int? id = null; + string? eventName = null; + + StructureValue? value = src as StructureValue; + + LogEventProperty? idProperty = value!.Properties.FirstOrDefault(x => x.Name.Equals("Id")); + if (idProperty is not null) + id = int.Parse(idProperty.Value.ToString()); + + LogEventProperty? nameProperty = value.Properties.FirstOrDefault(x => x.Name.Equals("Name")); + if (nameProperty is not null) + eventName = nameProperty.Value.ToString().Trim('"'); + + eventId = new EventId(id ?? 0, eventName ?? string.Empty); + + return eventId; + } +} +``` + +#### Configuring the Custom Sink - `DataStoreLoggerSinkExtensions` class + +Unlike the Microsoft `ILoggerProvider` implementation, the passing of configuration to the custom sink is done differently. There is no Provider, so we encapsulate the process within an extension method. + +```csharp +public static class DataStoreLoggerSinkExtensions +{ + public static LoggerConfiguration DataStoreLoggerSink + ( + this LoggerSinkConfiguration loggerConfiguration, + Func dataStoreProvider, + Action? configuration = null, + IFormatProvider formatProvider = null! + ) + => loggerConfiguration.Sink( + new DataStoreLoggerSink( + dataStoreProvider, + GetConfig(configuration), + formatProvider)); + + private static Func GetConfig( + Action? configuration) + { + // convert from Action to Func delegate to pass data + DataStoreLoggerConfiguration data = new(); + configuration?.Invoke(data); + return () => data; + } +} +``` + +#### Registering Sinks (Loggers) + +Serilog has two methods of registering Sinks: + +1. Manually in code +2. Via `appsetting*` configuration file + +As we need to inject the Sink configuration, we will be using the first method for the custom sink, however the SeriLog configuration and other sinks will be done via the `appsetting*` configuration file. Below is the configuration used in this article: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + }, + "Serilog": { + "Using": [ "Serilog.Sinks.File" ], + "LevelSwitches": { "controlSwitch": "Information" }, + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Information" + } + }, + + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {EventId.Name} | {Message:lj} {NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.txt", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "outputTemplate": "{Timestamp:G} {Message}{NewLine:1}{Exception:1}" + } + }, + { + "Name": "File", + "Args": { + "path": "c:\\WIP\\LogData\\log-.json", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "formatter": "Serilog.Formatting.Json.JsonFormatter" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ] + } +} +``` + +#### Dependency Injection + +Wiring up Logging with Serilog for use with the Dot Net Logging Framework is different to the Microsoft implementation. We need to manually inject the `LogDataStore` reference after the host service but create the Serilog Logger, and pass the Configuration via Dependency Injection, before the service is built. We do this using a Lambda expression (inline delegate method) that will be called every time a Logger instance is created. + +Here is an example of wiring up the Dependency Injection with the default configuration: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.AddLogViewer(); + +IServiceCollection services = builder.Services; + +services.AddLogging(configure: cfg => +{ + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .WriteTo.DataStoreLoggerSink( + + // Use Default Colors + dataStoreProvider: () => _host!.Services.TryGetService()!) + .CreateLogger(); + + cfg.ClearProviders() + .AddSerilog(Log.Logger); +}); + +_host = builder.Build(); +``` + +Or, if a custom configuration is to be used: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.AddLogViewer(); +IServiceCollection services = builder.Services; + +services.AddLogging(configure: cfg => +{ + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .WriteTo.DataStoreLoggerSink( + + // Use Custom Colors + dataStoreProvider: () => _host!.Services.TryGetService()!, + options => + { + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; + }) + .CreateLogger(); + + cfg.ClearProviders() + .AddSerilog(Log.Logger); +}); + +_host = builder.Build(); +``` + +**NOTE:** + +- We store a reference to the Logger factory instance so that when the application closes, we can flush the buffers for all sinks, like for file or remote storage. + +To create a logger, you can Inject an instance into a class constructor: + +```csharp +public class RandomLoggingService : BackgroundService +{ + #region Constructors + + public RandomLoggingService(ILogger logger) + => _logger = logger; + + #endregion + + #region Fields + + private readonly ILogger _logger; + + #endregion +} +``` + +Or request an instance manually: + +```csharp +ILogger logger = _host.Services.GetRequiredService>(); +``` + +And here is a sample screenshot of the logger instance with substantiated logger internals: + +![](Screenshots/Serilog%20-%20DI_507.png) + +#### Manually (without Dependency Injection) + +If not using Dependency Injection, it is still possible to register one or more loggers. We will require a singleton class to hold the registration and Factory method for generating Logger instances. + +Here is the `LoggingHelper` class used with the sample applications in this article. + +```csharp +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + IConfigurationRoot configuration = new ConfigurationBuilder() + .Initialize() + .Build(); + + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .WriteTo.DataStoreLoggerSink( + + // Use Default Colors + dataStoreProvider: () => MainControlsDataStore.DataStore) + .CreateLogger(); + + // wire up the loggers + Factory = LoggerFactory.Create(loggingBuilder => loggingBuilder.AddSerilog(Log.Logger)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion + + #region Methods + + public static void CloseAndFlush() + => Log.CloseAndFlush(); + + #endregion +} +``` + +Or, if a custom configuration is to be used: + +```csharp +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + IConfigurationRoot configuration = new ConfigurationBuilder() + .Initialize() + .Build(); + + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .WriteTo.DataStoreLoggerSink( + dataStoreProvider: () => MainControlsDataStore.DataStore, + options => + { + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; + } + ) + .CreateLogger(); + + // wire up the loggers + Factory = LoggerFactory.Create(loggingBuilder => loggingBuilder.AddSerilog(Log.Logger)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion + + #region Methods + + public static void CloseAndFlush() + => Log.CloseAndFlush(); + + #endregion +} +``` + +To create a logger, use the `Factory` method of the `LoggingHelper` class above: + +```csharp +Logger logger = new Logger(LoggingHelper.Factory); +``` + +**NOTE:** + +When creating Loggers, the class needs to be substantiated/Created. If the class is not, an error will be thrown. + +Creating the logger as a constructor parameter is acceptable. For example, the following is acceptable: + +```csharp +RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); +``` + +And here is a sample screenshot of the logger instance with substantiated logger internals: + +![](Screenshots/Serilog%20-%20noDI_516.png) + +### Custom NLog Target Logger Implementation + +**NLog** Targets (Loggers) have a different implementation to the Microsoft Logger implementation. However, to work with the Microsoft Logging Framework, **NLog** implements the Logger Provider internally so the Microsoft Logging Framework can pass data to the **NLog** Targets (Logger implementations). + +When implementing a custom NLog target, the target must be registered, then enabled in the configuration file. We will be implementing the NLog configuration in the `appsetting*.json` file. + +#### Logger - `DataStoreLoggerTarget` class +```csharp +[Target("DataStoreLogger")] +public class DataStoreLoggerTarget : TargetWithLayout +{ + #region Fields + + private ILogDataStore? _dataStore; + private DataStoreLoggerConfiguration? _config; + + #endregion + + #region methods + + protected override void InitializeTarget() + { + // we need to inject dependencies + IServiceProvider serviceProvider = ResolveService(); + + // reference the shared instance + _dataStore = serviceProvider.GetRequiredService(); + + // load the config options + IOptionsMonitor? options + = serviceProvider.GetService>(); + + _config = options?.CurrentValue ?? new DataStoreLoggerConfiguration(); + + base.InitializeTarget(); + } + + protected override void Write(LogEventInfo logEvent) + { + // cast NLog Loglevel to Microsoft LogLevel type + MsLogLevel logLevel = (MsLogLevel)Enum.ToObject(typeof(MsLogLevel), logEvent.Level.Ordinal); + + // format the message + string message = RenderLogEvent(Layout, logEvent); + + // retrieve the EventId + EventId eventId = (EventId)logEvent.Properties["EventId"]; + + // add log entry + _dataStore?.AddEntry(new() + { + Timestamp = DateTime.UtcNow, + LogLevel = logLevel, + // do we override the default EventId if it exists? + EventId = eventId.Id == 0 && (_config?.EventId.Id ?? 0) != 0 ? _config!.EventId : eventId, + State = message, + Exception = logEvent.Exception?.Message ?? (logLevel == MsLogLevel.Error ? message : ""), + Color = _config!.Colors[logLevel], + }); + + Debug.WriteLine($"--- [{logLevel.ToString()[..3]}] {message} - {logEvent.Exception?.Message ?? "no error"}"); + } + + #endregion +} +``` + +#### Configuring the Custom Target - `ServicesExtension` class +```csharp +public static class ServicesExtension +{ + public static ILoggingBuilder AddNLogTargets(this ILoggingBuilder builder, IConfiguration config) + { + LogManager + .Setup() + // Register custom Target + .SetupExtensions(extensionBuilder => + extensionBuilder.RegisterTarget("DataStoreLogger")); + + builder + .ClearProviders() + .SetMinimumLevel(MsLogLevel.Trace) + .AddNLog(config, + new NLogProviderOptions + { + IgnoreEmptyEventId = false, + CaptureEventId = EventIdCaptureType.Legacy + }); + + return builder; + } + + public static ILoggingBuilder AddNLogTargets(this ILoggingBuilder builder, IConfiguration config, Action configure) + { + builder.AddNLogTargets(config); + builder.Services.Configure(configure); + return builder; + } +} +``` +#### Registering Targets (Loggers) +NLog has two methods of registering Sinks: +1. Manually in code +2. Via `appsetting*` configuration file + +As we need to inject the Target configuration, we will be using the second method for the custom target, and registering the Custom Target in code, as above. Below is the configuration used in this article: +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Information" + } + }, + "NLog": { + "throwConfigExceptions": true, + "targets": { + "async": true, + "logconsole": { + "type": "Console", + "layout": "${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" + }, + "DataStoreLogger": { + "type": "DataStoreLogger", + "layout": "${message}" + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Info", + "writeTo": "logconsole, DataStoreLogger" + } + ] + } +} +``` +#### Dependency Injection + +The `ServicesExtension` class and `appsetting*` configuration file wires up the registration of the Targets, including our custom target , and configures NLog to work with the Dot Net Logging Framework. Now we need to tell the Host to use NLog Logging. + +Here is an example of wiring up the Dependency Injection with the default configuration: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.AddLogViewer(); + +builder.Logging.AddNLogTargets(builder.Configuration); + +_host = builder.Build(); +``` +Or, if a custom configuration is to be used: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.AddLogViewer(); + +builder.Logging.AddNLogTargets(builder.Configuration, options => +{ + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; +}); + +_host = builder.Build(); +``` + +To create a logger, you can Inject an instance into a class constructor: + +```csharp +public class RandomLoggingService : BackgroundService +{ +#region Constructors + + public RandomLoggingService(ILogger logger) + => _logger = logger; + + #endregion + + #region Fields + + private readonly ILogger _logger; + + #endregion +} +``` +Or request an instance manually: + +```csharp +ILogger logger = _host.Services.GetRequiredService>(); +``` + +And here is a sample screenshot of the logger instance with substantiated logger internals: + +![](Screenshots/NLog_DI_640.png) + +#### Manually (without Dependency Injection) + +If not using Dependency Injection, it is still possible to register one or more loggers. + +We will need to wrap the `ServicesExtension` used for Dependency Injection to use the non-DI version of `LogDataStore` class: + +```csharp +public static class ServicesExtension +{ + public static ILoggingBuilder AddNLogTargetsNoDI(this ILoggingBuilder builder, IConfiguration config) + { + // We need to use a shared instance of the DataStore to pass to the LogViewerControl + builder.Services.AddSingleton(MainControlsDataStore.DataStore); + + // call core NLog ServiceExtension initializer + builder.AddNLogTargets(config); + + return builder; + } + + public static ILoggingBuilder AddNLogTargetsNoDI(this ILoggingBuilder builder, IConfiguration config, Action configure) + { + builder.AddNLogTargetsNoDI(config); + builder.Services.Configure(configure); + return builder; + } +} +``` + +We will also require a singleton class to hold the registration and Factory method for generating Logger instances. Here is the LoggingHelper class used with the sample applications in this article. + +```csharp +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + // retrieve the log level from 'appsettings' + string value = AppSettings.Current("Logging:LogLevel", "Default") ?? "Information"; + Enum.TryParse(value, out LogLevel logLevel); + + IConfigurationRoot configuration = new ConfigurationBuilder() + .Initialize() + .Build(); + + // wire up the loggers + Factory = LoggerFactory.Create(builder => builder + + // visual debugging tools + .AddNLogTargetsNoDI(configuration) + + // set minimum log level from 'appsettings*.json' + .SetMinimumLevel(logLevel)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion +} +``` + +Or, if a custom configuration is to be used: + +```csharp +public static class LoggingHelper +{ + #region Constructors + + static LoggingHelper() + { + // retrieve the log level from 'appsettings' + string value = AppSettings.Current("Logging:LogLevel", "Default") ?? "Information"; + Enum.TryParse(value, out LogLevel logLevel); + + IConfigurationRoot configuration = new ConfigurationBuilder() + .Initialize() + .Build(); + + + // wire up the loggers + Factory = LoggerFactory.Create(builder => builder + + // visual debugging tools + .AddNLogTargets(configuration, options => + { + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; + }) + + // set minimum log level from 'appsettings*.json' + .SetMinimumLevel(logLevel)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion +} +``` + +To create a logger, use the Factory method of the LoggingHelper class above: +```csharp +Logger logger = new Logger(LoggingHelper.Factory); +``` + +**NOTE:** +* When creating Loggers, the class needs to be substantiated/Created. If the class is not, an error will be thrown. + +Creating the logger as a constructor parameter is acceptable. For example, the following is acceptable: + +```csharp +RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); +``` + +And here is a sample screenshot of the logger instance with substantiated logger internals: + +![](Screenshots/NLog_NoDI_640.png) + +!!! WIP START !!! + +### Custom Apache Log4Net Appender Logger Implementation + +Whilst Log4Net supports the Dot Net Framework (.NET Core 1.0 providing .NET Standard 1.3), Log4Net was the most involved to implement as there were: +* No native support for Dependency Injection for both the Logging system and Custom Appenders +* No support for logging with the `EventID` or other custom properties + +Doing research I did find an open source project [huorswords / Microsoft.Extensions.Logging.Log4Net.AspNetCore](https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore) on GitHub that supported Dependency Injection with the Dot Net Framework, however, was missing requirements for: +* No Dependency Injection for Custom Appenders +* No support for logging with the `EventID` or other custom properties + +You can read more about this project here: [How to use Log4Net with ASP.NET Core for logging | DotNetThoughts Blog](https://dotnetthoughts.net/how-to-use-log4net-with-aspnetcore-for-logging/). Please note, the name of the project is a little misleading. It is not specific to just AspNetCore. It will work with other application project types. + +#### Adding missing parts to Microsoft.Extensions.Logging.Log4Net.AspNetCore + +Whilst two (2) key requirements were missing, it is an open-source project, so we can update the project with the missing parts. The following section will cover how we achieved this by adding backward-compatible support with the current implementation to avoid any breaking changes. + +I will be creating a pull request for the missing parts. However, for now, I have included the updated project with the download for this article. + +##### Adding EventID support + +There was no official documentation on how to add features to the internal Log4Net logger. Luckily, I found on the official Log4Net repository an example of how to do this: [http://svn.apache.org/logging/log4net](http://svn.apache.org/viewvc/logging/log4net/trunk/examples/net/2.0/Extensibility/EventIDLogApp/cs/src/). + +There are three (3) parts to adding EventId support: +1. Wrap the base Log4Net `Logger` implementation (Interface + Class) +2. Update `Log4NetLogger` class in `Microsoft.Extensions.Logging.Log4Net.AspNetCore` to use the new logger class + +Following is the implementation used: + +1. `Logger` wrapper + + a`IEventIDLog` Interface + + ```csharp + public interface IEventIDLog : ILog + { + void Log(EventId eventId, LoggingEvent loggingEvent); + } + ``` + + b. `EventIDLogImpl` class + + ```csharp + public class EventIDLogImpl : LogImpl, IEventIDLog + { + public EventIDLogImpl(log4net.Core.ILogger logger) : base(logger) { /* skip */ } + + #region Implementation of IEventIDLog + + public void Log(EventId eventId, LoggingEvent loggingEvent) + { + // is the EventId empty? + if (!(eventId.Id == 0 && string.IsNullOrWhiteSpace(eventId.Name))) + loggingEvent.Properties[nameof(EventId)] = eventId; + + Logger.Log(loggingEvent); + } + + #endregion + } + ``` + +2. Update `Log4NetLogger` class + +I will only be showing the changes made - we change the implementation and now can inject the missing `EventId` reference. + +```csharp +public class Log4NetLogger : ILogger +{ + private readonly IEventIDLog eventIdLogger; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + if (!this.IsEnabled(logLevel)) + { + return; + } + + EnsureValidFormatter(formatter); + + var candidate = new MessageCandidate(logLevel, eventId, state, exception, formatter); + + LoggingEvent loggingEvent = options.LoggingEventFactory.CreateLoggingEvent(in candidate, eventIdLogger.Logger, options, externalScopeProvider); + + if (loggingEvent == null) + return; + + this.eventIdLogger.Log(eventId, loggingEvent); + } +} +``` + +##### Adding Dependency Injection support for the Appender support + +This has 2 parts: +1. Wrapping the base `AppenderSkeleton` class with DI support +2. Updating the `Log4NetProvider` class to prepare the `AppenderSkeleton` class for DI support + +Following is the implementation used: + +1. `ServiceAppenderSkeleton` wrapper class for DI support + +We define and internal explicit method for setting the DI service provider reference and a protected method that can be used from within our custom appender to resolve any required dependencies. + +```csharp +internal interface IAppenderServiceProvider +{ + IServiceProvider ServiceProvider { set; } +} + +public abstract class ServiceAppenderSkeleton + : AppenderSkeleton, IServiceAppenderSkeleton, IDisposable +{ + private IServiceProvider _serviceProvider; + IServiceProvider IAppenderServiceProvider.ServiceProvider + { + set => _serviceProvider = value; + } + + protected T ResolveService() where T : class + { + if (_serviceProvider == null) + return default; + + return _serviceProvider.GetService(); + } + + public void Dispose() => _serviceProvider = null; +} +``` + +2. Updating the `Log4NetProvider` class + +I will only be showing the changes made to add a DI service provider reference to the Appenders that implement the `IAppenderServiceProvider` interface. + +```csharp +public class Log4NetProvider : ILoggerProvider, ISupportExternalScope +{ + #region IOC implementation + + public Log4NetProvider(IServiceProvider serviceCollection) + : this(new Log4NetProviderOptions(), serviceCollection) + { + } + + public Log4NetProvider(string log4NetConfigFileName, IServiceProvider serviceProvider) + : this(new Log4NetProviderOptions(log4NetConfigFileName), serviceProvider) + { + } + + public Log4NetProvider(Log4NetProviderOptions options, IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + + this.SetOptionsIfValid(options); + + Assembly loggingAssembly = GetLoggingReferenceAssembly(); + + this.CreateLoggerRepository(loggingAssembly) + .ConfigureLog4NetLibrary(loggingAssembly); + } + + private IServiceProvider serviceProvider; + + #endregion + + private Log4NetProvider ConfigureLog4NetLibrary(Assembly assembly) + { + if (this.options.UseWebOrAppConfig) + { + XmlConfigurator.Configure(this.Repository); + return this; + } + + if (!this.options.ExternalConfigurationSetup) + { + string fileNamePath = CreateLog4NetFilePath(assembly); + if (this.options.Watch) + { + XmlConfigurator.ConfigureAndWatch( + this.Repository, + new FileInfo(fileNamePath)); + } + else + { + var configXml = ParseLog4NetConfigFile(fileNamePath); + if (this.options.PropertyOverrides != null + && this.options.PropertyOverrides.Any()) + { + configXml = UpdateNodesWithOverridingValues( + configXml, + this.options.PropertyOverrides); + } + + XmlConfigurator.Configure(this.Repository, configXml.DocumentElement); + } + } + + this.InjectServices(); + + return this; + } + + private void InjectServices() + { + if (this.Repository is null) + return; + + IEnumerable adapters = + this.Repository + .GetAppenders() + .OfType(); + + foreach (IAppenderServiceProvider adapter in adapters) + adapter.ServiceProvider = serviceProvider; + } +} +``` +#### Logger - `DataStoreLoggerAppender` class + +```csharp +public class DataStoreLoggerAppender : AppenderServiceProvider +{ + #region Fields + + private ILogDataStore? _dataStore; + private DataStoreLoggerConfiguration? _options; + + private IServiceProvider? _serviceProvider; + + #endregion + + #region Methods + + protected override void Append(LoggingEvent loggingEvent) + { + if (_serviceProvider is null) + Initialize(); + + // cast matching Log4Net Loglevel to Microsoft LogLevel type + LogLevel logLevel = loggingEvent.Level.Value switch + { + int.MaxValue => LogLevel.None, + 120000 => LogLevel.Debug, + 90000 => LogLevel.Critical, + 70000 => LogLevel.Error, + 60000 => LogLevel.Warning, + 20000 => LogLevel.Trace, + _ => LogLevel.Information + }; + + DataStoreLoggerConfiguration config = _options ?? new DataStoreLoggerConfiguration(); + + EventId? eventId = (EventId?)loggingEvent.LookupProperty(nameof(EventId)); + eventId = eventId is null && config.EventId.Id != 0 ? config.EventId : eventId; + + string message = loggingEvent.RenderedMessage ?? string.Empty; + + string exceptionMessage = loggingEvent.GetExceptionString(); + + _dataStore!.AddEntry(new() + { + Timestamp = DateTime.UtcNow, + LogLevel = logLevel, + EventId = eventId ?? new(), + State = message, + Exception = exceptionMessage, + Color = config.Colors[logLevel], + }); + + Debug.WriteLine($"--- [{logLevel.ToString()[..3]}] {message} - {exceptionMessage ?? "no error"}"); + } + + private void Initialize() + { + _serviceProvider = ResolveService(); + _dataStore = _serviceProvider.GetRequiredService(); + _options = _serviceProvider.GetService(); + } + + #endregion +} +``` + +#### Configuring the Custom Appender - `ServicesExtension` class + +```csharp +public static class Log4NetExtensions +{ + public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder, IConfiguration config) + => builder + .ClearProviders() + .AddLog4Net(config.GetLog4NetConfiguration()); + + public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder, IConfiguration config, Action configure) + { + builder + .AddLog4Net(config) + .Services.Configure(configure); + + return builder; + } + + public static Log4NetProviderOptions? GetLog4NetConfiguration(this IConfiguration configuration) + => configuration + .GetSection("Log4NetCore") + .Get(); +} +``` + +#### Registering Appenders (Loggers) + +Log4Net is restricted to using an XML config file. Default name is `log4net.config`. It is possible to change the name of this file. However, for the purpose of this article, we will not be focusing on this. + +```xml + + + + + + + + + + + + + + + + + + + + + + + +``` + +Luckily, the `Microsoft.Extensions.Logging.Log4Net.AspNetCore` project includes support for overriding values in the `log4net.config` file. This allows us to support different configurations for each launch profile using `appsettings*.json` file(s). + +Here is our `appsettings.Production.json` file: +```json +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "System.Net.Http.HttpClient": "Trace" + } + }, + "Log4NetCore": { + "Name": "Log4NetLogViewer_Prod", + "LoggerRepository": "LogViewerRepository", + "OverrideCriticalLevelWith": "Critical", + "Watch": false, + "UseWebOrAppConfig": false, + "PropertyOverrides": [ + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/layout/conversionPattern", + "Attributes": { + "Value": "%date [%thread] %-5level | %logger | %message%newline" + } + }, + { + "XPath": "/log4net/appender[@name='ConsoleAppender']/threshold", + "Attributes": { + "Value": "Warn" + } + }, + { + "XPath": "/log4net/appender[@name='DataStoreLogger']/threshold", + "Attributes": { + "Value": "Warn" + } + } + ] + } +} +``` + +**NOTES:** +* The default logging levels in the `log4net.config` is for all levels, however, for Production/Release, the `appsettings.Production.json` file overrides with `Warn` for `Warning`, `Error`, and `Critical` levels. + +#### Dependency Injection + +The `ServicesExtension` class and `log4net.config` configuration file wires up the registration of the Appenders, including our custom appender, and configures **Log4Het** to work with the **Dot Net Logging Framework**. Now we need to tell the Host to use Log4Net Logging. + +Here is an example of wiring up the Dependency Injection with the default configuration: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.AddLogViewer(); + +builder.Logging.AddLog4Net(builder.Configuration); + +_host = builder.Build(); +``` + +Or, if a custom configuration is to be used: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.AddLogViewer(); + +builder.Logging.AddLog4Net(builder.Configuration, options => +{ +options.Colors[LogLevel.Trace] = new() +{ +Foreground = Color.White, +Background = Color.DarkGray +}; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; +}); + +_host = builder.Build(); +``` + +To create a logger, you can Inject an instance into a class constructor: + +```csharp +public class RandomLoggingService : BackgroundService +{ +#region Constructors + + public RandomLoggingService(ILogger logger) + => _logger = logger; + + #endregion + + #region Fields + + private readonly ILogger _logger; + + #endregion +} +``` +Or request an instance manually: + +```csharp +ILogger logger = _host.Services.GetRequiredService>(); +``` + +And here is a sample screenshot of the logger instance with substantiated logger internals: + +![](Screenshots/Log4Net_DI_640.png) + +#### Manually (without Dependency Injection) + +If not using Dependency Injection, it is still possible to register one or more loggers. + +We will need to wrap the ServicesExtension used for Dependency Injection to use the non-DI version of LogDataStore class: + +```csharp +public static class LoggerExtension +{ + public static ILoggingBuilder AddLog4NetNoDI(this ILoggingBuilder builder, IConfiguration config) + { + // We need to use a shared instance of the DataStore to pass to the LogViewerControl + builder.Services.AddSingleton(MainControlsDataStore.DataStore); + + // call core NLog ServiceExtension initializer + builder.AddLog4Net(config); + + return builder; + } + + public static ILoggingBuilder AddLog4NetNoDI(this ILoggingBuilder builder, IConfiguration config, Action configure) + { + builder.AddLog4NetNoDI(config); + builder.Services.Configure(configure); + return builder; + } +} +``` + +We will also require a singleton class to hold the registration and Factory method for generating Logger instances. Here is the LoggingHelper class used with the sample applications in this article. + +```csharp +public static class LoggingHelper +{ +#region Constructors + + static LoggingHelper() + { + // retrieve the log level from 'appsettings' + string value = AppSettings.Current("Logging:LogLevel", "Default") ?? "Information"; + Enum.TryParse(value, out LogLevel logLevel); + + IConfigurationRoot configuration = new ConfigurationBuilder() + .Initialize() + .Build(); + + // wire up the loggers + Factory = LoggerFactory.Create(builder => builder + + // visual debugging tools + .AddLog4NetNoDI(configuration) + + // set minimum log level from 'appsettings*.json' + .SetMinimumLevel(logLevel)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion +} +``` +Or, if a custom configuration is to be used: + +```csharp +public static class LoggingHelper +{ +#region Constructors + + static LoggingHelper() + { + // retrieve the log level from 'appsettings' + string value = AppSettings.Current("Logging:LogLevel", "Default") ?? "Information"; + Enum.TryParse(value, out LogLevel logLevel); + + IConfigurationRoot configuration = new ConfigurationBuilder() + .Initialize() + .Build(); + + + // wire up the loggers + Factory = LoggerFactory.Create(builder => builder + + // visual debugging tools + .AddLog4NetNoDI(configuration, options => + { + options.Colors[LogLevel.Trace] = new() + { + Foreground = Color.White, + Background = Color.DarkGray + }; + + options.Colors[LogLevel.Debug] = new() + { + Foreground = Color.White, + Background = Color.Gray + }; + + options.Colors[LogLevel.Information] = new() + { + Foreground = Color.White, + Background = Color.DodgerBlue + }; + + options.Colors[LogLevel.Warning] = new() + { + Foreground = Color.White, + Background = Color.Orchid + }; + }) + + // set minimum log level from 'appsettings*.json' + .SetMinimumLevel(logLevel)); + } + + #endregion + + #region Properties + + public static ILoggerFactory Factory { get; } + + #endregion +} +``` + +To create a logger, use the Factory method of the LoggingHelper class above: + +```csharp +Logger logger = new Logger(LoggingHelper.Factory); +``` + +**NOTE:** + +When creating Loggers, the class needs to be substantiated/Created. If the class is not, an error will be thrown. + +Creating the logger as a constructor parameter is acceptable. For example, the following is acceptable: + +```csharp +RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); +``` + +And here is a sample screenshot of the logger instance with substantiated logger internals: + +![](Screenshots/Log4Net_NoDI_638.png) + +!!! WIP END !!! + +### Processing Log Entries + +We have our `LogDataStore` class storing all the Log Entries from all libraries and the application based on the minimal `LogLevel` retrieved from the `appsettings*.json` configuration file. + +#### Dependency Injection + +The `LogDataStore` class is registered as a singleton. it can be injected into the class: + +```csharp +public class MyConsumer +{ + public MyConsumer(LogDataStore dataStore) + => _dataStore = dataStore; + + private LogDataStore? _dataStore; +} +``` + +Or request an instance manually: + +```csharp +public class MyConsumer +{ + public MyConsumer(IServiceProvider serviceProvider) + => _dataStore = serviceProvider.GetRequiredService(); + + private LogDataStore? _dataStore; +} +``` + +We need to register `MyConsumer` class for dependency Injection to wire everything up: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.Services.AddSingleton(); // from `ServicesExtension` class above +builder.Services.AddTransient(); + +_host = builder.Build(); +``` + +#### Manually (without Dependency Injection) + +The Data Store needs to be held in a singleton class so that it can be shared between the logger (producer) and the consumer class. + +Here is the `MainControlsDataStore` class that will hold the shared Data Store: + +```csharp +public static class MainControlsDataStore +{ + public static LogDataStore DataStore { get; } = new(); +} +``` + +we can pass an instance on the Data Store to the consumer class for IOC (inversion of control) allowing for future upgrading the application/library for Dependency Injection or a different implementation: + +```csharp +public class MyConsumer +{ + public MyConsumer(LogDataStore dataStore) + => _dataStore = dataStore; + + private LogDataStore? _dataStore; +} +``` + +To use the `MyConsumer` class, we inject the `DataStore`: + +```csharp +MyConsumer instance = new MyConsumer(MainControlsDataStore.DataStore); +``` + +#### Listening for new Entries + +When we substance the `MyConsumer` class, and reference the `LogDataStore` class, we need to listen to the `Entries` property `CollectionChanged` event manually or let data binding do all of the work. + +#### Manual Handling of the CollectionChanged events + +```csharp +public class MyConsumer +{ + public MyConsumer(LogDataStore dataStore) + { + _dataStore = dataStore; + _dataStore.Entries.CollectionChanged += OnCollectionChanged; + } + + private LogDataStore? _dataStore; + + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // any new items? + if (e.NewItems?.Count > 0) + { + // process new items + } + + // any to remove? ... not required for this purpose. + if (e.OldItems?.Count > 0) + { + // remove items + } + } +} +``` + +## LogViewerControl Implementation + +The Logger code is in 2 parts: + +1. Common code - `LogViewer.Core` project = shared code +2. Application type-specific control implementation + - **WinForms** specific - `LogViewer.WinForms` project = **WinForm** wrapper code for the Common Code + - **Wpf** specific - `LogViewer.Wpf` project = **Wpf** wrapper code for the Common Code + - **Avalonia** specific - `LogViewer.Avalonia` project = **Avalonia** wrapper code for the Common Code + +The reason for this is that we need to marshall back to the UI thread. The method to do this for all application types is slightly different. A `DispatcherHelper` class is included for **Wpf** and **WinForms**. **Avalonia** does not require the same, they have a simple to use implementation. Below you can see the differences in the implementation: + +### `DispatcherHelper` Class + +The **Logger** framework utilizes a thread separate to the UI thread to maintain performance. Consuming Log Entries and showing on the UI requires marshalling to the UI thread. The abstraction of marshalling will be handled by a `DispatcherHelper` class. The `DispatcherHelper` class `Execute` method takes a delegate and will identify if it is on the UI thread or not and will switch, if required, before invoking the delegate. + +Usage for **Wpf** and **WinForms** is very simple: + +```csharp +DispatcherHelper.Execute(() => delegate_method()); +``` + +or you can inline the `delegate_method()`: + +```csharp +DispatcherHelper.Execute(() => +{ + // do work here +}); +``` + +Usage in **Avalonia** is very similar: + +```csharp +await Dispatcher.UIThread.InvokeAsync(() => delegate_method()); +``` + +or you can inline the `delegate_method()`: + +```csharp +await Dispatcher.UIThread.InvokeAsync(() => +{ + // do work here +}); +``` + +#### WinForms Implementation + +```csharp +public static class DispatcherHelper +{ + public static void Execute(Action action) + { + // no cross-thread concerns + if (Application.OpenForms.Count == 0) + { + action.Invoke(); + return; + } + + try + { + if (Application.OpenForms[0]!.InvokeRequired) + // Marshall to Main Thread + Application.OpenForms[0]!.Invoke(action); + else + // We are already on the Main Thread + action.Invoke(); + } + catch (Exception) + { + // ignore as might be thrown on shutting down + } + } +} +``` + +#### WPF Implementation + +```csharp +public static class DispatcherHelper +{ + public static void Execute(Action action) + { + if (Application.Current is null || Application.Current.Dispatcher is null) + // We are already on the Main Thread + return; + + // Marshall to Main Thread + Application.Current.Dispatcher.BeginInvoke( DispatcherPriority.Background, action); + } +} +``` + +### Common code - `LogViewer.Core` project + +This was covered in the sections above for the `DataStoreLogger`, `DataStoreLoggerProvider`, `DataStoreLoggerConfiguration`, `LogDataStore`, `LogModel`, and `LogEntryColor` classes. For **WPF**, we will cover the `LogViewerControlViewModel` class and the `ILogDataStoreImpl` interface in the WPF LogViewerControl implementation section. + +#### `LoggerExtensions` class + +Two methods are included: + +- `Emit` method - a performant wrapper for the `Log` method +- `TestPattern` method - a helper method for viewing the output formatting of all `LogLevel` types (for debugging purposes only) + +```csharp +public static class LoggerExtensions +{ + public static void Emit(this ILogger logger, EventId eventId, + LogLevel logLevel, string message, Exception? exception = null, params object?[] args) + { + if (logger is null) + return; + + //if (!logger.IsEnabled(logLevel)) + // return; + + switch (logLevel) + { + case LogLevel.Trace: + logger.LogTrace(eventId, message, args); + break; + + case LogLevel.Debug: + logger.LogDebug(eventId, message, args); + break; + + case LogLevel.Information: + logger.LogInformation(eventId, message, args); + break; + + case LogLevel.Warning: + logger.LogWarning(eventId, exception, message, args); + break; + + case LogLevel.Error: + logger.LogError(eventId, exception, message, args); + break; + + case LogLevel.Critical: + logger.LogCritical(eventId, exception, message, args); + break; + } + } + + public static void TestPattern(this ILogger logger, EventId eventId) + { + Exception exception = new Exception("Test Error Message"); + + logger.Emit(eventId, LogLevel.Trace, "Trace Test Pattern"); + logger.Emit(eventId, LogLevel.Debug, "Debug Test Pattern"); + logger.Emit(eventId, LogLevel.Information, "Information Test Pattern"); + logger.Emit(eventId, LogLevel.Warning, "Warning Test Pattern"); + logger.Emit(eventId, LogLevel.Error, "Error Test Pattern", exception); + logger.Emit(eventId, LogLevel.Critical, "Critical Test Pattern", exception); + } +} +``` + +### ViewModel: `LogViewerControlViewModel` Class + +For the Dependency Injection implementations for **WinForms**, **WPF**, and **Avalonia** a common `LogViewerControlViewModel` class to reference the singleton `LogDataStore` instance for monitoring manually (**WinForms**) or via Data Binding (**WPF**) in the `LogViewControl` control. + +```csharp +public class LogViewerControlViewModel : ViewModel, ILogDataStoreImpl +{ + #region Constructor + + public LogViewerControlViewModel(ILogDataStore dataStore) + { + DataStore = dataStore; + } + + #endregion + + #region Properties + + public ILogDataStore DataStore { get; set; } + + #endregion +} +``` + +### WinForms - LogViewerControl + +![](Screenshots/LogViewerControl%20-%20Winforms%20640.png) + +Now we can create the control itself. For **WinForms**, the code-behind will be looked at. If you want to see the UserControl design, download and inspect the designer code. + +#### Code Behind + +```csharp +public partial class LogViewerControl : UserControl +{ + #region Constructors + + // supports DI and non-DI usage + + public LogViewerControl() + { + InitializeComponent(); + + // Stop the flickering! + ListView.SetDoubleBuffered(); + + Disposed += OnDispose; + } + + public LogViewerControl(LogViewerControlViewModel viewModel) : this() + => RegisterLogDataStore(viewModel.DataStore); + + #endregion + + #region Fields + + private ILogDataStore? _dataStore; + + private static readonly SemaphoreSlim _semaphore = new(initialCount: 1); + + #endregion + + #region Methods + + public void RegisterLogDataStore(ILogDataStore dataStore) + { + _dataStore = dataStore; + + // As we are manually handling the DataBinding, we need to add existing log entries + AddListViewItems(_dataStore.Entries); + + // Simple way to DataBind the ObservableCollection to the ListView is to listen to the CollectionChanged event + _dataStore.Entries.CollectionChanged += OnCollectionChanged; + } + + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // any new items? + if (e.NewItems?.Count > 0) + { + AddListViewItems(e.NewItems.Cast()); + + ExclusiveDispatcher(() => + { + // auto-scroll if required + if (CanAutoScroll.Checked) + ListView.Items[^1].EnsureVisible(); + }); + } + + // any to remove? ... not required for this purpose. + if (e.OldItems?.Count > 0) + { + // remove from ListView.Items + } + } + + private void AddListViewItems(IEnumerable logEntries) + { + ExclusiveDispatcher(() => + { + foreach (LogModel item in logEntries) + { + ListViewItem lvi = new ListViewItem + { + Font = new(ListView.Font, FontStyle.Regular), + Text = item.Timestamp.ToString("G"), + ForeColor = item.Color!.Foreground, + BackColor = item.Color.Background + }; + + lvi.SubItems.Add(item.LogLevel.ToString()); + lvi.SubItems.Add(item.EventId.ToString()); + lvi.SubItems.Add(item.State?.ToString() ?? string.Empty); + lvi.SubItems.Add(item.Exception ?? string.Empty); + ListView.Items.Add(lvi); + } + }); + } + + private void ExclusiveDispatcher(Action action) + { + // ensure only one operation at time from multiple threads + _semaphore.Wait(); + + // delegate to UI thread + DispatcherHelper.Execute(action.Invoke); + + _semaphore.Release(); + } + + // cleanup time ... + private void OnDispose(object? sender, EventArgs e) + { + Disposed -= OnDispose; + if (_dataStore is null) + return; + + _dataStore.Entries.CollectionChanged -= OnCollectionChanged; + } + + #endregion +} +``` + +The `LogViewerControl` has 2 controls: + +- `ListView` control - main display of log entries +- `CheckBox` control - toggles auto-scrolling of the `ListView` control + +The code simply references the `LogDataStore` instance, and listens to the `Entries` collection for changes. As Items are added, a `ListViewItem` is created, formatted, and added to the `ListView` control. + +It also listens for when the `LogViewerControl` is disposed and dereferences all events to avoid memory leaks. + +Here is a GIF with **default** colorization in action: + +![](Screenshots/WinForm%20LogViewer.gif) + +### WPF - LogViewerControl + +![](Screenshots/LogViewerControl%20-%20WPF%20640.png) + +We will be using Data-Binding to manage the event handling for when new Log Entries are added. + +#### Code-behind + +```csharp +public partial class LogViewerControl +{ + public LogViewerControl() => InitializeComponent(); + + private void OnLayoutUpdated(object? sender, EventArgs e) + { + if (!CanAutoScroll.IsChecked == true) + return; + + // design time + if (DataContext is null) + return; + + // Okay, we can now get the item and scroll into view + LogModel? item = (DataContext as ILogDataStoreImpl)?.DataStore.Entries.LastOrDefault(); + + if (item is null) + return; + + ListView.ScrollIntoView(item); + } +} +``` + +We need to support: + +- Dependency Injection with MVVM +- No Dependency Injection and MVVM +- No Dependency Injection and manual data binding in code behind + +For MVVM, the `LogDataStore` will be on a `Model` or `ViewModel`. The last option may have the `LogDataStore` exposed as a property on the Window or a UserControl. The control requires access to the `LogDataStore` for both scenarios. The `LogViewControl` requires a common Interface to the property: + +```csharp +public interface ILogDataStoreImpl +{ + public LogDataStore DataStore { get; } +} +``` + +#### User Interface + +The XAML focusing on the Data-Binding in the `ListView` control: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**NOTE:** Download the solution to see the full implementation of the UI. + +As the `DataStoreLogger` is being used by both **WinForms** and **WPF** project types, `System.Drawing.Color` class was used in the `DataStoreLoggerConfiguration` class. So for **WPF**, we need to convert the Color type class `System.Windows.Media.Color` and return a `SolidColorBrush`. + +```csharp +public class ChangeColorTypeConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + SysDrawColor sysDrawColor = (SysDrawColor)value; + return new SolidColorBrush(Color.FromArgb( + sysDrawColor.A, + sysDrawColor.R, + sysDrawColor.G, + sysDrawColor.B)); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); +} +``` + +Here is a GIF with **custom** colorization in action: + +![](Screenshots/Wpf%20LogViewer.gif) + +### Avalonia - LogViewerControl + +![](Screenshots/LogViewerControl%20-%20Avalonia%20640.png) + +We will be using Data-Binding to manage the event handling for when new Log Entries are added. + +The implementation is different for **Avalonia** as the controls are not identical. Here we use a `DataGrid` whereas we use a `ListView` for WPF. For the auto-scroll, there are subtle differences from **WPF**. Below you can see how we handle the Scrolling into view differently from **WPF** as items are added. + +#### Code-behind + +```csharp +public partial class LogViewerControl : UserControl +{ + public LogViewerControl() + => InitializeComponent(); + + private ILogDataStoreImpl? vm; + private LogModel? item; + + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (DataContext is null) + return; + + vm = (ILogDataStoreImpl)DataContext; + vm.DataStore.Entries.CollectionChanged += OnCollectionChanged; + } + + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + => item = MyDataGrid.Items.Cast().LastOrDefault(); + + private void OnLayoutUpdated(object? sender, EventArgs e) + { + if (CanAutoScroll.IsChecked != true || item is null) + return; + + MyDataGrid.ScrollIntoView(item, null); + item = null; + } + + private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e) + { + if (vm is null) return; + vm.DataStore.Entries.CollectionChanged -= OnCollectionChanged; + } +} +``` + +We need to support: + +- Dependency Injection with MVVM +- No Dependency Injection and MVVM +- No Dependency Injection and manual data binding in code behind + +For MVVM, the `LogDataStore` will be on a `Model` or `ViewModel`. The last option may have the `LogDataStore` exposed as a property on the Window or a UserControl. The control requires access to the `LogDataStore` for both scenarios. The `LogViewControl` requires a common Interface to the property: + +```csharp +public interface ILogDataStoreImpl +{ + public LogDataStore DataStore { get; } +} +``` + +#### User Interface + +The XAML focusing on the Data-Binding in the `ListView` control: + +```xml + + + + + + + + + Black + Transparent + + + + + + + + + + + + + + + +``` + +**NOTE:** Download the solution to see the full implementation of the UI. + +As the `DataStoreLogger` is being used by both **WinForms** and **WPF** project types, `System.Drawing.Color` class was used in the `DataStoreLoggerConfiguration` class. So for **WPF**, we need to convert the Color type class `System.Windows.Media.Color` and return a `SolidColorBrush`. + +```csharp +public class ChangeColorTypeConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + SysDrawColor sysDrawColor = (SysDrawColor)value; + return new SolidColorBrush(Color.FromArgb( + sysDrawColor.A, + sysDrawColor.R, + sysDrawColor.G, + sysDrawColor.B)); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); +} +``` + +Unlike in **WPF**, we need to extract as String value from the `EventId` class as **Avalonia** data-binding for the `DataGrid` control does not use the `ToString()` method of the class. + +```csharp +public class EventIdConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) + return "0"; + + EventId eventId = (EventId)value; + + return eventId.ToString(); + } + + // If not implemented, an error is thrown + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => new EventId(0, value?.ToString() ?? string.Empty); +} +``` + +Here is a GIF with **custom** colorization in action: + +![](Screenshots/mac_avalonia_logging_vb.gif) + +## Using the LogViewControl + +We have created the custom Logger, we have a common Data Store to share all of the log entries, and created a LogViewerControl, now we can use. + +### WinForms - Dependency Injection + +#### Registration - `ServicesExtension` class + +The registration of the `LogViewerControl` and `LogViewerControlViewModel` are abstracted to an extension method in the `ServicesExtension` class: + +```csharp +public static class ServicesExtension +{ + public static HostApplicationBuilder AddLogViewer(this HostApplicationBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + + return builder; + } +} +``` + +**NOTES:** + +- The `LogViewerControlViewModel` class is registered as a singleton for the shared `LogDataStore` instance required for the `DataStoreLogger` to share log entries with the `LogViewerControl`. +- Each time the `LogViewerControl` is substantiated, the shared `LogViewerControlViewModel` instance will be manually wired up in the host `LogViewerControl` control. + +#### `MainForm` Code-Behind + +The `MainForm` Designer has a `Panel` control named `HostPanel` for hosting the `LogViewerControl`. Below we can see that the `LogViewerControl` is injected into `MainForm` and it is added to the `HostPanel` + +```csharp +public partial class MainForm : Form +{ + #region Constructors + + public MainForm(MainControlsDataStore controlsDataStore) + { + InitializeComponent(); + + // wire up the control + HostPanel.AddControl(controlsDataStore.LogViewer); + } + + #endregion +} +``` + +The `AddControl` is an extension method encapsulating the code to do the task: + +```csharp +public static class ControlsExtension +{ + public static void AddControl(this Panel panel, Control control) + { + panel.Controls.Add(control); + control.Dock = DockStyle.Fill; + control.BringToFront(); + } +} +``` + +#### Registration - `Bootstrapper` class + +We can not use Dependency Injection in a static class, in this case, the `Program` class in a **WinForms** application. So we add a `Bootstrapper` class and point to an instance: + +```csharp +internal static class Program +{ + #region Bootstrap + + [STAThread] + static void Main() => _ = new Bootstrapper(); + + #endregion +} +``` + +Then in the `Bootstrapper` class we can wire up the Dependencies: + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.Logging.AddDefaultDataStoreLogger(); + +builder.Services + .AddSingleton() + .AddSingleton(); + +_host = builder.Build(); +``` + +#### Usage + +Once registered, we can show the `MainForm` from the `Bootstrapper` class: + +```csharp +// set and show +Application.Run(_host.Services.GetRequiredService()); +``` + +### WinForms - Manually (without Dependency Injection) + +For Manual with no Dependency Injection, we Add the `LogViewControl` control directly on the Form then we register the `LogDataStore` instance manually with the `LogViewControl` control. + +#### `MainForm` Code-Behind + +```csharp +public partial class MainForm : Form +{ + public Form1() + { + InitializeComponent(); + + // Initialize service and pass in the Logger + RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); + + // Start generating log entries + _ = service.StartAsync(CancellationToken.None); + + // manually wire up the logging to the view ... the control will show backlog entries... + LogViewerControl.RegisterLogDataStore(MainControlsDataStore.DataStore); + } +} +``` + +### WPF - Dependency Injection + +There is a lot of overlap with how the **WinForms** implementation. + +#### Registration - `ServicesExtension` class + +The registration of the `LogViewerControl` and `LogViewerControlViewModel` are abstracted to an extension method in the `ServicesExtension` class. The Setting of the DataContext is also done at the time of substantiation by Dependency Injection: + +```csharp +public static class ServicesExtension +{ + public static HostApplicationBuilder AddLogViewer(this HostApplicationBuilder builder, Action? configure = null) + { + builder.Services.AddSingleton(); + builder.Services.AddTransient(service => new LogViewerControl + { + DataContext = service.GetRequiredService() + }); + + // trimmed + + return builder; + } +} +``` + +**NOTES:** + +- The `LogViewerControlViewModel` class is registered as a singleton for the shared `LogDataStore` instance required for the `DataStoreLogger` to share log entries with the `LogViewerControl`. +- Each time the `LogViewerControl` is substantiated, the DataContext will be automatically set to the shared `LogViewerControlViewModel` instance. + +#### `MainWindow` - `LogViewerControl` Host + +The are many different ways to Host a UserControl. The method that I use is the `ContentControl`. + +```xml + + + + + + + + + + + +``` + +We register the `MainWindow` for Dependency Injection to inject the `LogViewerControl` via the `MainViewModel` class. The `MainViewModel` will expose the `LogViewerControlViewModel`, data binding using a template will initialize the `LogViewControl`. + +#### `MainViewModel` class + +```csharp +public class MainViewModel : ViewModel +{ + #region Constructor + + public MainViewModel(LogViewerControlViewModel logViewer) + { + LogViewer = logViewer; + } + + #endregion + + #region Properties + + public LogViewerControlViewModel LogViewer { get; } + + #endregion +} +``` + +#### Registration - App (C#) / Application (VB) class + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.Logging.AddDataStoreLogger(); + +builder.Services. + .AddSingleton() + .AddSingleton(service => new MainWindow + { + DataContext = service.GetRequiredService() + }); + +_host = builder.Build(); +``` + +#### Usage + +Once registered, we can show the `MainWindow` from the `App` class: + +```csharp +MainWindow = _host.Services.GetRequiredService(); +MainWindow.Show(); +``` + +### WPF - Manually (without Dependency Injection) + +For Manual with no Dependency Injection, we Add the `LogViewControl` control directly on the Window, store a reference to the `LogDataStore` instance manually as a Property on the Window, then set the `DataContext` of the `LogViewControl` control to the Window and let Data Binding wire up the `LogViewControl` control. + +#### `MainWindow` XAML - `LogViewerControl` Host + +```xml + + + + + +``` + +#### `MainWindow` Code-behind + +```csharp +public partial class MainWindow : ILogDataStoreImpl +{ + public MainWindow() + { + InitializeComponent(); + + // bare minimum to get the service running and wire up logging + + // Initialize service and pass in the Logger + RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); + + // Start generating log entries + _ = service.StartAsync(CancellationToken.None); + + // manually wire up the logging to the view ... the control will show backlog entries... + DataStore = MainControlsDataStore.DataStore; + + // we can't bind the controls' DataContext to a static object, so assign the DataStore to the Window + // and pass a reference to the Window itself + LogViewerControl.DataContext = this; + } + + // Passed to the LogViewerControl via the DataContext property as ILogDataStoreImpl + public LogDataStore DataStore { get; init; } +} +``` + +### Avalonia - Dependency Injection + +There is a lot of overlap with the **WPF** implementation, so if you are familiar with **WPF**, then this should feel very familiar to you. + +#### Registration - `ServicesExtension` class + +The registration of the `LogViewerControlViewModel` is abstracted to an extension method in the `ServicesExtension` class. The Setting of the DataContext is also done at the time of substantiation by Dependency Injection: + +```csharp +public static class ServicesExtension +{ + public static HostApplicationBuilder AddLogViewer(this HostApplicationBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder; + } +} +``` + +**NOTES:** + +- The `LogViewerControlViewModel` class is registered as a singleton for the shared `LogDataStore` instance required for the `DataStoreLogger` to share log entries with the `LogViewerControl`. +- Each time the `LogViewerControl` is substantiated, the DataContext will be manually set to the shared `LogViewerControlViewModel` instance. + +#### `MainWindow` - `LogViewerControl` Host + +The are many different ways to Host a UserControl. The method that I use is the `ContentControl`. + +```xml + + + + + +``` + +#### `MainViewModel` class + +```csharp +public class MainViewModel : ViewModelBase +{ + #region Constructor + + public MainViewModel(LogViewerControlViewModel logViewer) + { + LogViewer = logViewer; + } + + #endregion + + #region Properties + + public LogViewerControlViewModel LogViewer { get; } + + #endregion +} +``` + +#### Registration - App (C#) / Application (VB) class + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +builder.Logging.AddDefaultDataStoreLogger(); + +builder.Services. + .AddSingleton() + .AddSingleton() + .AddSingleton(service => new MainWindow + { + DataContext = service.GetRequiredService() + }); + +_host = builder.Build(); +``` + +#### Usage + +Once registered, we can show the `MainWindow` from the `App` class: + +```csharp +desktop.MainWindow = _host.Services.GetRequiredService(); +``` + +### Avalonia - Manually (without Dependency Injection) + +For Manual with no Dependency Injection, we Add the `LogViewControl` control directly on the Window, store a reference to the `LogDataStore` instance manually as a Property on the Window, then set the `DataContext` of the `LogViewControl` control to the Window and let Data Binding wire up the `LogViewControl` control. + +#### `MainWindow` XAML - `LogViewerControl` Host + +```xml + + + + + +``` + +#### `MainWindow` Code-behind + +```csharp +public partial class MainWindow : ILogDataStoreImpl +{ + public MainWindow() + { + InitializeComponent(); + + // bare minimum to get the service running and wire up logging + + // Initialize service and pass in the Logger + RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); + + // Start generating log entries + _ = service.StartAsync(CancellationToken.None); + + // manually wire up the logging to the view ... the control will show backlog entries... + DataStore = MainControlsDataStore.DataStore; + + // we can't bind the controls' DataContext to a static object, so assign the DataStore to the Window + // and pass a reference to the Window itself + LogViewerControl.DataContext = this; + } + + // Passed to the LogViewerControl via the DataContext property as ILogDataStoreImpl + public LogDataStore DataStore { get; init; } +} +``` + +## Generating Sample Log Messages + +The last thing that we need to do is generate Log messages to simulate a live application. For this, I will be using a [BackgroundService](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice?view=dotnet-plat-ext-7.0). The `BackgroundService` service class is used for creating long running tasks for **Asp.Net** background tasks, or Windows Services. We can use also in desktop applications however, unlike **ASP.NET**, requires manual activation and shutting down. + +We will take advantage of the **Dot Net Framework** HostedServices. HostedServices can manage one or more as background tasks in our application. + +### Background Service - `RandomLoggingService` class + +```csharp +public class RandomLoggingService : BackgroundService +{ + #region Constructors + + public RandomLoggingService(ILogger logger) + => _logger = logger; + + #endregion + + #region Fields + + #region Injected + + private readonly ILogger _logger; + + #endregion + + // ChatGPT generated lists + + private readonly List _messages = new() + { + "Bringing your virtual world to life!", + "Preparing a new world of adventure for you.", + "Calculating the ideal balance of work and play.", + "Generating endless possibilities for you to explore.", + "Crafting the perfect balance of life and love.", + "Assembling a world of endless exploration.", + "Bringing your imagination to life one pixel at a time.", + "Creating a world of endless creativity and inspiration.", + "Designing the ultimate dream home for you to live in.", + "Preparing for the ultimate life simulation experience.", + "Loading up your personalized world of dreams and aspirations.", + "Building a new neighborhood full of excitement and adventure.", + "Creating a world full of surprise and wonder.", + "Generating the ultimate adventure for you to embark on.", + "Assembling a community full of life and energy.", + "Crafting the perfect balance of laughter and joy.", + "Bringing your digital world to life with endless possibilities.", + "Calculating the perfect formula for happiness and success.", + "Generating a world of endless imagination and creativity.", + "Designing a world that's truly one-of-a-kind for you." + }; + + readonly List _eventNames = new() + { + "OnButtonClicked", + "OnMenuItemSelected", + "OnWindowResized", + "OnDataLoaded", + "OnFormSubmitted", + "OnTabChanged", + "OnItemSelected", + "OnValidationFailed", + "OnNotificationReceived", + "OnApplicationStarted", + "OnUserLoggedIn", + "OnUploadStarted", + "OnDownloadCompleted", + "OnProgressUpdated", + "OnNetworkErrorOccurred", + "OnPaymentSuccessful", + "OnProfileUpdated", + "OnSearchCompleted", + "OnFilterChanged", + "OnLanguageChanged" + }; + + readonly List _errorMessages = new() + { + "Error: Could not connect to the server. Please check your internet connection.", + "Warning: Your computer's operating system is not compatible with this software.", + "Error: Insufficient memory. Please close other programs and try again.", + "Warning: Your graphics card drivers may be outdated. Please update them before playing.", + "Error: The installation file is corrupt. Please download a new copy.", + "Warning: Your computer may be running too hot. Please check the temperature and cooling system.", + "Error: The required DirectX version is not installed on your computer.", + "Warning: Your sound card may not be supported. Please check the system requirements.", + "Error: The installation directory is full. Please free up space and try again.", + "Warning: Your computer's power supply may not be sufficient. Please check the requirements.", + "Error: The installation process was interrupted. Please restart the setup.", + "Warning: Your antivirus software may interfere with the game. Please add it to the exception list.", + "Error: The required Microsoft library is not installed.", + "Warning: Your input devices may not be compatible. Please check the system requirements.", + "Error: The installation process failed. Please contact support for assistance.", + "Warning: Your network speed may cause lag and disconnections.", + "Error: The setup file is not compatible with your operating system.", + "Warning: Your computer's resolution may cause display issues.", + "Error: The required Microsoft .NET Framework is not installed on your computer.", + "Warning: Your keyboard layout may cause input errors. Please check the settings." + }; + + private readonly Random _random = new(); + private static readonly EventId EventId = new(id: 0x1A4, name: "RandomLoggingService"); + + #endregion + + #region BackgroundService + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.Emit(EventId, LogLevel.Information, "Started"); + + while (!stoppingToken.IsCancellationRequested) + { + // wait for a pre-determined interval + await Task.Delay(1000, stoppingToken).ConfigureAwait(false); + + if (stoppingToken.IsCancellationRequested) + return; + + // heartbeat logging + GenerateLogEntry(); + } + + _logger.Emit(EventId, LogLevel.Information, "Stopped"); + } + + public Task StartAsync() + => StartAsync(CancellationToken.None); + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await Task.Yield(); + + _logger.Emit(EventId, LogLevel.Information, "Starting"); + + await base.StartAsync(cancellationToken).ConfigureAwait(false); + } + + public Task StopAsync() + => StopAsync(CancellationToken.None); + + public override async Task StopAsync(CancellationToken cancellationToken) + { + _logger.Emit(EventId, LogLevel.Information, "Stopping"); + await base.StopAsync(cancellationToken).ConfigureAwait(false); + } + + #endregion + + #region Methods + + private void GenerateLogEntry() + { + LogLevel level = _random.Next(0, 100) switch + { + < 50 => LogLevel.Information, + < 65 => LogLevel.Debug, + < 75 => LogLevel.Trace, + < 85 => LogLevel.Warning, + < 95 => LogLevel.Error, + _ => LogLevel.Critical + }; + + if (level < LogLevel.Error) + { + _logger.Emit(GenerateEventId(), level, GetMessage()); + return; + } + + _logger.Emit(GenerateEventId(), level, GetMessage(), + new Exception(_errorMessages[_random.Next(0, _errorMessages.Count)])); + } + + private EventId GenerateEventId() + { + int index = _random.Next(0, _eventNames.Count); + return new EventId(id: 0x1A4 + index, name: _eventNames[index]); + } + + private string GetMessage() + => _messages[_random.Next(0, _messages.Count)]; + + #endregion +} +``` + +### Dependency Injection + +Using the Background Service is a two-part process: + +1. We need to set the scope of the the class and register the service +2. Manually start the hosting service that manages all registered Background Services + +#### Registration + +```csharp +public static class ServicesExtension +{ + public static HostApplicationBuilder AddRandomBackgroundService(this HostApplicationBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddHostedService(service => service.GetRequiredService()); + + return builder; + } +} +``` + +#### Usage + +```csharp +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +// Register the Random Logging Service +builder.AddRandomBackgroundService(); + +_host = builder.Build(); + +// startup one or more registered background services +_ = _host.StartAsync(_cancellationTokenSource.Token); +``` + +### Manually (without Dependency Injection) + +```csharp +// Initialize service and pass in the Logger +RandomLoggingService service = new(new Logger(LoggingHelper.Factory)); + +// Start generating log entries +_ = service.StartAsync(_cancellationTokenSource.Token); +``` + +## LoggerMessageAttribute (C# only) + +In .Net 6.0, support for compile-time source generated performant logging APIs via the [LoggerMessageAttribute](https://devblogs.microsoft.com/dotnet/announcing-net-6/#microsoft-extensions-logging-compile-time-source-generator). + +Microsoft has documentation that covers usage called [Compile-time logging source generation](learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator). The logging constraints listed that must be followed are: +- Logging methods must be partial and return void. +- Logging method names must not start with an underscore. +- Parameter names of logging methods must not start with an underscore. +- Logging methods may not be defined in a nested type. +- Logging methods cannot be generic. +- If a logging method is static, the ILogger instance is required as a parameter. +Other constraints not listed are: +- An Event Id is required and is a static parameter +- The optional Event Name is a static parameter +- Exceptions must be included in the message and is not a separate field + +The coming .Net 8.0 (as of the time of writing this article) has added more flexibility with the constructor parameters that can be passed however the static fields remain. You can read more here: [Expanding LoggerMessageAttribute Constructor Overloads for Enhanced Functionality](https://github.com/dotnet/core/issues/8437#issuecomment-1605698272). + +With the above constrains in mind, we can now update the code: +1. Each application project required a dedicated Logging method with a `LoggerMessageAttribute` decorator +2. Every Event Name requires its own dedicated Logging method + +### Dedicated Application Logging method + +```csharp +public static partial class ApplicationLog +{ + private const string AppName = "WpfLoggingAttrDI"; + + [LoggerMessage (EventId = 0, EventName = AppName, Message = "{msg}")] + public static partial void Emit(ILogger logger, LogLevel level, string msg); + + public static void Emit(ILogger logger, LogLevel level, string msg, Exception exception) + => Emit(logger, level, $"{msg} - {exception}"); +} +``` + +To call, we simply use: + +```csharp + ApplicationLog.Emit(logger, logLevel, message); +``` + +IF there is an exception, then: + +```csharp + ApplicationLog.Emit(logger, logLevel, message, exception); +``` + +### Dedicated RandomLoggingService Logging method + +As we have multiple Event Names, each requires it's own dedicated Logging method. Below I set up a Lookup table to simplify calling the correct method and also share the Event Names. + +```csharp +public static partial class RandomServiceLog +{ + public static Dictionary> Events = new() + { + ["OnButtonClicked"] = LogOnButtonClicked, + ["OnMenuItemSelected"] = LogOnMenuItemSelected, + ["OnWindowResized"] = LogOnWindowResized, + ["OnDataLoaded"] = LogOnDataLoaded, + ["OnFormSubmitted"] = LogOnFormSubmitted, + ["OnTabChanged"] = LogOnTabChanged, + ["OnItemSelected"] = LogOnItemSelected, + ["OnValidationFailed"] = LogOnValidationFailed, + ["OnNotificationReceived"] = LogOnNotificationReceived, + ["OnApplicationStarted"] = LogOnApplicationStarted, + ["OnUserLoggedIn"] = LogOnUserLoggedIn, + ["OnUploadStarted"] = LogOnUploadStarted, + ["OnDownloadCompleted"] = LogOnDownloadCompleted, + ["OnProgressUpdated"] = LogOnProgressUpdated, + ["OnNetworkErrorOccurred"] = LogOnNetworkErrorOccurred, + ["OnPaymentSuccessful"] = LogOnPaymentSuccessful, + ["OnProfileUpdated"] = LogOnProfileUpdated, + ["OnSearchCompleted"] = LogOnSearchCompleted, + ["OnFilterChanged"] = LogOnFilterChanged, + ["OnLanguageChanged"] = LogOnLanguageChanged + }; + + public static void Emit(ILogger logger, EventId eventId, LogLevel level, string message, Exception? exception = null) + => Events[eventId.Name!].Invoke(logger, level, exception is null ? message : $"{message} - {exception}"); + + [LoggerMessage (EventId = 101, EventName = "OnButtonClicked", Message = "{msg}")] + private static partial void LogOnButtonClicked(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 102, EventName = "OnMenuItemSelected", Message = "{msg}")] + private static partial void LogOnMenuItemSelected(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 103, EventName = "OnWindowResized", Message = "{msg}")] + private static partial void LogOnWindowResized(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 104, EventName = "OnDataLoaded", Message = "{msg}")] + private static partial void LogOnDataLoaded(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 105, EventName = "OnFormSubmitted", Message = "{msg}")] + private static partial void LogOnFormSubmitted(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 106, EventName = "OnTabChanged", Message = "{msg}")] + private static partial void LogOnTabChanged(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 107, EventName = "OnItemSelected", Message = "{msg}")] + private static partial void LogOnItemSelected(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 108, EventName = "OnValidationFailed", Message = "{msg}")] + private static partial void LogOnValidationFailed(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 109, EventName = "OnNotificationReceived", Message = "{msg}")] + private static partial void LogOnNotificationReceived(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 110, EventName = "OnApplicationStarted", Message = "{msg}")] + private static partial void LogOnApplicationStarted(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 111, EventName = "OnUserLoggedIn", Message = "{msg}")] + private static partial void LogOnUserLoggedIn(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 112, EventName = "OnUploadStarted", Message = "{msg}")] + private static partial void LogOnUploadStarted(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 113, EventName = "OnDownloadCompleted", Message = "{msg}")] + private static partial void LogOnDownloadCompleted(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 114, EventName = "OnProgressUpdated", Message = "{msg}")] + private static partial void LogOnProgressUpdated(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 115, EventName = "OnNetworkErrorOccurred", Message = "{msg}")] + private static partial void LogOnNetworkErrorOccurred(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 116, EventName = "OnPaymentSuccessful", Message = "{msg}")] + private static partial void LogOnPaymentSuccessful(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 117, EventName = "OnProfileUpdated", Message = "{msg}")] + private static partial void LogOnProfileUpdated(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 118, EventName = "OnSearchCompleted", Message = "{msg}")] + private static partial void LogOnSearchCompleted(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 119, EventName = "OnFilterChanged", Message = "{msg}")] + private static partial void LogOnFilterChanged(ILogger logger, LogLevel level, string msg); + + [LoggerMessage (EventId = 120, EventName = "OnLanguageChanged", Message = "{msg}")] + private static partial void LogOnLanguageChanged(ILogger logger, LogLevel level, string msg); +} +``` + +**Note:** Above each unique Event Name has a unique Event Id. This is not compulsory, but highly recommended. + +To call, we simply use: + +```csharp +RandomServiceLog.Emit("Event_Name", logger, LogLevel.Information, "message goes here"); +``` + +### RandomLoggingService + +We can now update the `RandomLoggingService` class: + +```csharp +public class RandomLoggingService : BackgroundService +{ + #region Constructors + + public RandomLoggingService(ILogger logger) + { + _logger = logger; + _eventNames = RandomServiceLog.Events.Keys.ToList(); + } + + #endregion + + #region Fields + + #region Injected + + private readonly ILogger _logger; + + #endregion + + // ChatGPT generated lists + + private readonly List _messages = new() + { + "Bringing your virtual world to life!", + "Preparing a new world of adventure for you.", + "Calculating the ideal balance of work and play.", + "Generating endless possibilities for you to explore.", + "Crafting the perfect balance of life and love.", + "Assembling a world of endless exploration.", + "Bringing your imagination to life one pixel at a time.", + "Creating a world of endless creativity and inspiration.", + "Designing the ultimate dream home for you to live in.", + "Preparing for the ultimate life simulation experience.", + "Loading up your personalized world of dreams and aspirations.", + "Building a new neighborhood full of excitement and adventure.", + "Creating a world full of surprise and wonder.", + "Generating the ultimate adventure for you to embark on.", + "Assembling a community full of life and energy.", + "Crafting the perfect balance of laughter and joy.", + "Bringing your digital world to life with endless possibilities.", + "Calculating the perfect formula for happiness and success.", + "Generating a world of endless imagination and creativity.", + "Designing a world that's truly one-of-a-kind for you." + }; + + private readonly IReadOnlyList _eventNames; + + private readonly List _errorMessages = new() + { + "Error: Could not connect to the server. Please check your internet connection.", + "Warning: Your computer's operating system is not compatible with this software.", + "Error: Insufficient memory. Please close other programs and try again.", + "Warning: Your graphics card drivers may be outdated. Please update them before playing.", + "Error: The installation file is corrupt. Please download a new copy.", + "Warning: Your computer may be running too hot. Please check the temperature and cooling system.", + "Error: The required DirectX version is not installed on your computer.", + "Warning: Your sound card may not be supported. Please check the system requirements.", + "Error: The installation directory is full. Please free up space and try again.", + "Warning: Your computer's power supply may not be sufficient. Please check the requirements.", + "Error: The installation process was interrupted. Please restart the setup.", + "Warning: Your antivirus software may interfere with the game. Please add it to the exception list.", + "Error: The required Microsoft library is not installed.", + "Warning: Your input devices may not be compatible. Please check the system requirements.", + "Error: The installation process failed. Please contact support for assistance.", + "Warning: Your network speed may cause lag and disconnections.", + "Error: The setup file is not compatible with your operating system.", + "Warning: Your computer's resolution may cause display issues.", + "Error: The required Microsoft .NET Framework is not installed on your computer.", + "Warning: Your keyboard layout may cause input errors. Please check the settings." + }; + + private readonly Random _random = new(); + private static readonly EventId EventId = new(id: 0x1A4, name: "RandomLoggingService"); + + #endregion + + #region BackgroundService + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + ApplicationLog.Emit(_logger, LogLevel.Information, "Started"); + + while (!stoppingToken.IsCancellationRequested) + { + // wait for a pre-determined interval + await Task.Delay(1000, stoppingToken).ConfigureAwait(false); + + if (stoppingToken.IsCancellationRequested) + return; + + // heartbeat logging + GenerateLogEntry(); + } + + ApplicationLog.Emit(_logger, LogLevel.Information, "Stopped"); + } + + public Task StartAsync() + => StartAsync(CancellationToken.None); + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await Task.Yield(); + + ApplicationLog.Emit(_logger, LogLevel.Information, "Starting"); + + await base.StartAsync(cancellationToken).ConfigureAwait(false); + } + + public Task StopAsync() + => StopAsync(CancellationToken.None); + + public override async Task StopAsync(CancellationToken cancellationToken) + { + ApplicationLog.Emit(_logger, LogLevel.Information, "Stopping"); + await base.StopAsync(cancellationToken).ConfigureAwait(false); + } + + #endregion + + #region Methods + + private void GenerateLogEntry() + { + LogLevel level = _random.Next(0, 100) switch + { + < 50 => LogLevel.Information, + < 65 => LogLevel.Debug, + < 75 => LogLevel.Trace, + < 85 => LogLevel.Warning, + < 95 => LogLevel.Error, + _ => LogLevel.Critical + }; + + if (level < LogLevel.Error) + { + RandomServiceLog.Emit(_logger, GenerateEventId(), level, message: GetMessage()); + return; + } + + RandomServiceLog.Emit(_logger, GenerateEventId(), level, message: GetMessage(), + new Exception(_errorMessages[_random.Next(0, _errorMessages.Count)])); + } + + private EventId GenerateEventId() + { + int index = _random.Next(0, _eventNames.Count); + return new EventId(id: 0x1A4 + index, name: _eventNames[index]); + } + + private string GetMessage() + => _messages[_random.Next(0, _messages.Count)]; + + #endregion +} +``` + +To see the updated `RandomLoggingService` in action, download the code and run the `WpfLoggingAttrDI` project. in the `MSlogger/Attribute` solution folder. + +## Summary + +We covered how logging works; how to create, register, and use a custom logger & provider with customization for **WinForms** **WPF**, and **Avalonia** application types in both **C#** & **VB**. We looked at the internal code of Dot Net for working with loggers & providers. We created custom controls for **WinForms** **WPF**, and **Avalonia** application types in both **C#** & **VB**, to consume the logs from a custom logger, using Microsoft's Default Logger and a 3rd-party SeriLog structured logger. We also covered how to use the custom loggers and the custom control for both Dependency Injection and manually wiring up. Lastly, we created the **Dot Net Framework** Background Service for emulating an application generating log entries. + +Whilst this article was long and thorough, creating Custom Loggers and consuming the content generate is not complicated, regardless of application type and how the application is wired up, either manually or via Dependency Injection. + +All source code, both **C#** and **VB**, is provided in the link at the top of this article. To use in your own project, copy all of the required libraries for the application type, add a reference to the LogViewer control project + the type of logger project, then follow the guidelines for usage. + +If you have any questions, please post below and I would be more than happy to answer. + +## References + +### Documentation, Articles, etc + +- **Dot Net (core) 7.0 Framework** + - [.NET App Settings Demystified (C# & VB) | CodeProject](https://www.codeproject.com/Articles/5354478/NET-App-Settings-Demystified-Csharp-VB) + - [Logging in .NET | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) + - [Implement a custom logging provider in .NET | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/extensions/custom-logging-provider) + - [Dependency injection in .NET | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) + - [Model-View-ViewModel (MVVM) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/architecture/maui/mvvm#the-mvvm-pattern) + - [Data binding overview (Windows Forms .NET)](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/data/overview?view=netdesktop-7.0) + - [Data binding overview (WPF .NET) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/?view=netdesktop-7.0) + - [BackgroundService | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice?view=dotnet-plat-ext-7.0) + - [Background tasks with hosted services in ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-7.0&tabs=visual-studio) +- **Avalonia UI** + - [Avalonia UI](https://avaloniaui.net/) + - [Comparison of Avalonia with WPF and UWP | Avalonia UI](https://docs.avaloniaui.net/guides/developer-guides/comparison-of-avalonia-with-wpf-and-uwp) + - [The Missing Avalonia Templates for VB | Code Project](https://www.codeproject.com/Articles/5357284/Avalonia-for-VB) +- **Serilog** + - [Serilog](https://serilog.net/) +- **NLOG** + - [NLOG](https://nlog-project.org/) + - [Getting started with .NET Core 2 Console application | NLOG](https://github.com/NLog/NLog/wiki/Getting-started-with-.NET-Core-2---Console-application#a-minimal-example) + - [Getting started with ASP.NET Core 6 | nlog](https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-6) + - [How to write a custom target | NLOG](https://github.com/NLog/NLog/wiki/How-to-write-a-custom-target) + - [Register your custom component | NLOG](https://github.com/NLog/NLog/wiki/Register-your-custom-component) + - [NLog configuration with appsettings.json | NLOG](https://github.com/NLog/NLog.Extensions.Logging/wiki/NLog-configuration-with-appsettings.json) + - [NLog properties with Microsoft Extension Logging | NLOG](https://github.com/NLog/NLog.Extensions.Logging/wiki/NLog-properties-with-Microsoft-Extension-Logging) + - [NLog.Extensions.Logging changes capture of EventId | NLOG](https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready.html#nlogextensionslogging-changes-capture-of-eventid) +- **Log4Net** + - [Apache Log4Net | Apache](https://logging.apache.org/log4net/) + - [Apache log4net Manual - Configuration | Apache](https://logging.apache.org/log4net/release/manual/configuration.html) + - [http://svn.apache.org/logging/log4net | Apache Repository](http://svn.apache.org/viewvc/logging/log4net/trunk/examples/net/2.0/Extensibility/EventIDLogApp/cs/src/) + - [How to use Log4Net with ASP.NET Core for logging | DotNetThoughts Blog](https://dotnetthoughts.net/how-to-use-log4net-with-aspnetcore-for-logging/) + - [huorswords / Microsoft.Extensions.Logging.Log4Net.AspNetCore | GitHub](https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore) + +### Nuget Packages + +- **Dot Net (core) 7.0 Framework** + - [Microsoft.Extensions.Configuration](https://www.nuget.org/packages/Microsoft.Extensions.Configuration) + - [Microsoft.Extensions.Configuration.EnvironmentVariables](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.EnvironmentVariables) + - [Microsoft.Extensions.Configuration.Json](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.Json) + - [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) + - [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging) + - [Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions) + - [Microsoft.Extensions.Options.ConfigurationExtensions](https://www.nuget.org/packages/Microsoft.Extensions.Options.ConfigurationExtensions) + - [CommunityToolkit.Mvvm 8.1.0](https://www.nuget.org/packages/CommunityToolkit.Mvvm) (used by Avalonia) + - [XamlNameReferenceGenerator 1.6.1](https://www.nuget.org/packages/XamlNameReferenceGenerator) (used by Avalonia) +- **Avalonia** + - [Avalonia 0.10.18](https://www.nuget.org/packages/Avalonia/) + - [Avalonia.Desktop 0.10.18](https://www.nuget.org/packages/Avalonia.Desktop) + - [Avalonia.Controls.DataGrid 0.10.18](https://www.nuget.org/packages/Avalonia.Controls.DataGrid/) + - [MessageBox.Avalonia 2.2.0](https://www.nuget.org/packages/MessageBox.Avalonia) +- **Serilog** + - [Serilog 2.12.0](https://www.nuget.org/packages/Serilog) + - [Serilog.Enrichers.Environment 2.2.0](https://www.nuget.org/packages/Serilog.Enrichers.Environment) + - [Serilog.Enrichers.Process 2.0.2](https://www.nuget.org/packages/Serilog.Enrichers.Process) + - [Serilog.Enrichers.Thread 3.1.0](https://www.nuget.org/packages/Serilog.Enrichers.Thread) + - [Serilog.Extensions.Hosting 5.0.1](https://www.nuget.org/packages/Serilog.Extensions.Hosting) + - [Serilog.Extensions.Logging 3.1.0](https://www.nuget.org/packages/Serilog.Extensions.Logging) + - [Serilog.Settings.Configuration 3.4.0](https://www.nuget.org/packages/Serilog.Settings.Configuration) + - [Serilog.Sinks.Console 4.1.0](https://www.nuget.org/packages/Serilog.Sinks.Console) + - [Serilog.Sinks.Debug 2.0.0](https://www.nuget.org/packages/Serilog.Sinks.Debug) + - [Serilog.Sinks.File 5.0.0](https://www.nuget.org/packages/Serilog.Sinks.File) +- **NLOG** + - [NLog 5.1.2](https://www.nuget.org/packages/NLog/) + - [NLog.Extensions.Logging 5.2.2](https://www.nuget.org/packages/NLog.Extensions.Logging) +- **Log4Net** + - [log4net 2.0.15](https://www.nuget.org/packages/log4net/) + +## History + +- 23rd March, 2023 - v1.0 - Initial release +- 28th March, 2023 - v1.10 - Added support for [NLOG](https://nlog-project.org/) logging platform + **WinForms**, **WPF**, and **Avalonia** sample DI & no-DI applications (x6); fixed an issue in `LogViewer.Winforms` project where possible "index out of range" exception occasionally occurs +- 29th March, 2023 - v1.20 = Added support for [Apache Log4Net](https://logging.apache.org/log4net/) logging Services + **WinForms**, **WPF**, and **Avalonia** sample DI & no-DI applications (x6); various code cleanup and optimizations +- 20th April, 2023 - v1.20a - rezipped project using Microsoft's File Explorer "Compress to Zip" +- 12th September, 2023 - v1.30 - Added [LoggerMessageAttribute](#loggermessageattribute) section \ No newline at end of file