65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于 WinForms、WPF 和 Avalonia 的 C# & VB 日志查看器控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (35投票s)

2023年3月23日

CPOL

33分钟阅读

viewsIcon

55434

downloadIcon

4082

适用于 WinForms、WPF 和 Avalonia 的 .NET Core 日志查看器控件,使用 ILogger 框架,支持 Microsoft Logger、Serilog、NLog 和 Log4Net,适用于 C# & VB,运行于 Windows、Mac OS 和 Linux

引言

我正在开发一个解决方案,需要一个应用程序内置的查看器来实时查看 **Logger** 条目,以便了解后台发生的情况。

我想要比控制台输出更漂亮的东西,并且能够添加到 **Winforms**、**WPF** 或 **Avalonia** 应用程序中,感觉像是应用程序的一部分,并且可能需要用户查看的东西——即用户友好,而不是下面的样子。

LoggerViewer 的要求如下:

  • 定义为一个 **控件**,可以通过依赖注入添加或注入。
  • **原生** 支持 **WinForms**、**WPF** 和 **Avalonia** 应用程序。
  • 支持多个操作系统 - **Windows**、**MacOS**、**Linux**。
  • 支持多个日志框架 - **Microsoft**(默认)、**Serilog** 和 **NLog**。
  • 支持 **着色**(附加自定义颜色)。
  • **依赖注入** (DI) 和非 DI 使用。
  • **MVVM**(模型-视图-视图模型设计模式)和非 MVVM 使用。
  • **历史记录** 可在任何列表控件中查看,如 ListView / DataGrid 控件。
  • 可选择的 **自动滚动** 以保持最新条目可见。
  • 支持 **AppSettings.Json** 文件进行可配置日志记录。
  • 捕获 **框架 API 日志记录**。
  • 与其他 Logger 并行工作。

我们将探讨日志记录 - 它的工作原理,并查看使其工作的框架代码。

由于我们将涵盖 **WPF**、**WinForms** 和 **Avalonia** 项目类型,**Microsoft** 和 **Serilog** 日志记录器,以及使用/不使用依赖注入,因此本文将比较长。

如果您对它的工作原理不感兴趣,请参阅下面的“预览”部分的动画,下载代码,并在适合您用例的语言中运行适用的应用程序。

预览

在开始之前,让我们看看我们想要实现的目标。LogViewerControl 的 **WPF**、**WinForms** 和 **Avalonia** 版本看起来几乎相同,并且对于 **C#** 和 **VB** 版本都工作相同。

这是一个使用依赖注入和数据绑定在 **C#** 中、用于 **WinForms** 版本的 **默认** 着色 GIF。

这是一个使用 **VB**、极简实现、无依赖注入、三行代码的 **WPF** 版本、**自定义** 着色 GIF。

最后,这里证明了您可以使用 **VB**(是的,Visual Basic)使用 **Avalonia** 框架为 **Mac OS** 开发应用程序!虽然 VB 不支持开箱即用,因为除了一个不完整的 Github 仓库 之外,没有包含应用程序、类或控件库模板,但我将介绍如何让 **VB** 使用 **Avalonia** 框架来开发应用程序和控件项目类型。

注意:三个动画 GIF 可能需要一些时间加载...

目录

必备组件

本文的代码仅适用于 .NET Core。使用了 7.03 版本并启用了 Nullable。但是,如果需要,可以修改以支持 .NET 3.1 或更高版本。

该解决方案使用 **Visual Studio 2022 v17.4.5** 构建,并使用 **Rider 2022.3.2** 完全测试。

本文使用的 Nuget 包列在本文章末尾的“Nuget 包” 参考部分

AppSettings 辅助类用于简化从 *appsettings*.json 文件读取配置设置。有一篇文章深入介绍了它的工作原理:.NET App Settings Demystified (C# & VB | CodeProject)

如果您不熟悉日志记录,请花点时间阅读 Logging in .NET | Microsoft Learn,它涵盖了基础知识。

由于我们正在实现自定义 Logger 和 Provider,并且您不熟悉创建自定义 Logger 和 Provider,请花点时间阅读 Implement a custom logging provider in .NET | Microsoft Learn

我们还将涵盖依赖注入 (DI)。我提供了使用和不使用 DI 的解决方案,因此 DI 不是必需的。如果您有兴趣了解更多信息,请阅读:Dependency injection in .NET | Microsoft Learn

最后,我们将涵盖 MVVM(模型-视图-视图模型设计模式)。我提供了使用和不使用 MVVM 的解决方案,因此 MVVM 不是必需的。如果您有兴趣了解更多信息,请阅读:Model-View-ViewModel (MVVM) | Microsoft Learn

解决方案设置

由于我们涵盖了 3 种项目类型,因此解决方案的结构试图最大限度地减少代码重复。此外,项目分为 4 个部分:应用程序、控件、核心和后台服务。

  1. 该应用程序演示了如何在您自己的应用程序中实现。
  2. 控件是您添加到自己的应用程序中的 UI 组件。
  3. 核心包含公共代码、特定于应用程序类型的代码和自定义 Logger 实现。自定义 Logger 实现独立于控件,您可以选择其中一个或自己编写另一个 Logger 框架。
  4. 后台服务只是一个虚拟服务,用于模拟生成日志消息。该服务对于所有应用程序类型都是通用的。

日志记录流程

我们可以使用下面的图表来简化设计概念。

根据上面的图表,逻辑流程如下:

  1. 应用程序以适当的信息记录一个事件(TraceDebugInformationWarningErrorCritical)。
  2. Logger 框架将 Log 事件传递给所有已注册的 Logger,包括我们的自定义 Logger。
  3. LoggerLog 事件存储在 DataStore 中。
  4. LogViewer 控件接收到数据绑定通知并显示 Log 事件。

应用程序架构

应用程序架构对于所有应用程序类型都相同。

注释

  • 应用程序、控件和公共部分依赖于 UI 和应用程序类型。
  • Logger Providers 特定于日志记录框架。
  • 控件和公共部分特定于应用程序类型。
  • Logger Providers、Random Logging Service 和 Controls 彼此独立。

解决方案架构

同时包括 **VB** 和 **C#** 解决方案,并且布局相同。唯一的区别是 **VB** 版本项目名称的末尾有 **VB**。

注释

  • 应用程序项目名称由 3 部分组成:[应用程序类型][Logger][实现]
    1. 应用程序类型:AvaloniaWinFormsWpf
    2. Logger:Logger(默认 .NET 实现)或 Serilog
    3. 实现:DI = 依赖注入;NoDI = 手动 / 无依赖注入
  • 对于支持项目,名称后缀标识项目类型:
    1. .Core 用于公共代码
    2. .Avalonia.WinForms.Wpf 用于特定于应用程序的类型

日志记录是如何工作的?

在我们深入研究解决方案之前,让我们快速了解一下 .NET 日志记录框架的工作原理。

有三个部分:

  1. Logger
  2. 注册 Loggers
  3. 处理日志条目

我们将使用 Microsoft Logger Framework。这将使我们不仅能够捕获应用程序的日志记录,还能捕获所有 **.NET Framework** 和第三方库的日志记录。

本文中的实现将使用一个单例 `DataStore` 进行存储,自定义 Logger 和 Logging Provider。还有一个 `Configuration` 类用于自定义选项,例如自定义着色。

这只是一个简要的总结和内部代码的查看。如果您需要更多信息,请参阅上面提供的链接以及本文末尾的“参考”部分。

Logger 内部

Loggers 由四部分组成:

  1. Logger - 日志实现
  2. LoggingProvider - 生成 Logger 实例
  3. Processor / Storage - Logger 输出日志到的地方
  4. Configuration (可选) - 生成输出的参数

每次 LoggingFactory 创建 Logger 实例时,LoggingFactory 都会遍历所有已注册的 Logger Providers 并为返回的具体 Logger 生成内部 Logger 实例。对具体 Logger 上的 Log 方法的所有调用都将遍历所有内部 Logger 实例。

为了更好地理解这一点,让我们看一下 **.NET Framework** LoggerFactory 类中创建我们使用的 Logger 实例的代码。

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; }
}

在这里,我们看到了所有连接起来的内容,包括 LoggerProvier 通过 CreateLoggers 方法生成内部 Loggers

然后,每次我们通过我们的 Logger 记录条目时,信息都会被传递给每一个内部 Logger

这是由 LoggerFactory 实例化的具体 **.NET Framework** 内部 Logger。我们将特别关注 Log 方法。

  internal sealed class Logger : ILogger
  {
       public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
                               Exception? exception, Func<TState, Exception?, 
                               string> formatter)
      {
          MessageLogger[]? loggers = MessageLoggers;
          if (loggers == null)
          {
              return;
          }

          List<Exception>? 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<TState, 
                                Exception?, string> formatter,
                                ref List<Exception>? exceptions, in TState state)
          {
              try
              {
                  logger.Log(logLevel, eventId, state, exception, formatter);
              }
              catch (Exception ex)
              {
                  exceptions ??= new List<Exception>();
                  exceptions.Add(ex);
              }
          }
      }
      // trimmed
  }

在这里,我们可以看到它将信息传递给所有已注册的内部 Loggers

自定义 Logger

**.NET Framework** 有一个默认的 Microsoft Logger Framework 可供使用。还有许多第三方日志框架。本文将探讨两种日志框架:

  1. Microsoft Logger Framework(内置)
  2. Serilog Logger Framework 用于结构化日志记录

LogViewerControl 使用内置日志记录框架。对于 Serilog,我们将探讨如何创建自定义 Sink(Logger)并将其挂钩到内置日志记录框架。

共享日志记录数据

在查看自定义 Logger 的实现之前,我们需要设置日志条目存储和 Logger 配置。

存储 - LogDataStore 和 LogModel 类

public interface ILogDataStore
{
    ObservableCollection<LogModel> 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<LogModel> 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
}
Public Interface ILogDataStore

  ReadOnly Property Entries As ObservableCollection(Of LogModel)

  Sub AddEntry(logModel As LogModel)

End Interface

Public Class LogDataStore : Implements ILogDataStore

#Region "Fields"

  Private Shared ReadOnly _semaphore = New SemaphoreSlim(initialCount:=1)

#End Region

#Region "Properties"

  Public ReadOnly Property Entries As ObservableCollection(Of LogModel) _
    = New ObservableCollection(Of LogModel) _
    Implements ILogDataStore.Entries

#End Region

#Region "Methods"

  Public Overridable Sub AddEntry(logModel As LogModel) _
         Implements ILogDataStore.AddEntry

    ' ensure only one operation at time from multiple threads
    _semaphore.Wait()

    Entries.Add(logModel)

    _semaphore.Release()

  End Sub

#End Region

End Class

用于保存每个日志条目的数据模型。

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
}
Public Class LogModel

#Region "Properties"

    Public Property Timestamp As Date

    Public Property LogLevel As LogLevel

    Public Property EventId As EventId

    Public Property State As Object

    Public Property Exception As String

    Public Property Color As LogEntryColor

#End Region

End Class

注意LogDataStore 类被初始化为单例。为了处理添加到 LogDataStore 类中的任何条目,使用了 ObservableCollection<T>。应用程序要处理条目,只需要监听此集合的 CollectionChanged 事件。这将在文章后面的 ??? 部分介绍。

配置 - DataStoreLoggerConfiguration 类和 LogEntryColor 类

DataStoreLoggerConfiguration 类用于可选的自定义。

public class DataStoreLoggerConfiguration
{
    #region Properties
    
    public EventId EventId { get; set; }

    public Dictionary<LogLevel, LogEntryColor> 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
}
Public Class DataStoreLoggerConfiguration

#Region "Properties"

    Public Property EventId As EventId

    Public Property Colors As Dictionary(Of LogLevel, LogEntryColor) = _
        New Dictionary(Of LogLevel, LogEntryColor) From
    {
        {LogLevel.Trace, New LogEntryColor() With {.Foreground = Color.DarkGray}},
        {LogLevel.Debug, New LogEntryColor() With {.Foreground = Color.Gray}},
        {LogLevel.Information, New LogEntryColor()},
        {LogLevel.Warning, New LogEntryColor() With {.Foreground = Color.Orange}},
        {LogLevel.Error, New LogEntryColor() With _
          {.Foreground = Color.White, .Background = Color.OrangeRed}},
        {LogLevel.Critical, New LogEntryColor() With _
          {.Foreground = Color.White, .Background = Color.Red}},
        {LogLevel.None, New LogEntryColor()}
    }

#End Region

End Class

用于保存每个日志级别显示颜色的数据模型。

public class LogEntryColor
{
    public Color Foreground { get; set; } = Color.Black;
    public Color Background { get; set; } = Color.Transparent;
}
Public Class LogEntryColor

    Property Foreground As Color = Color.Black

    Property Background As Color = Color.Transparent

End Class

自定义 Microsoft Logger 实现

Microsoft Loggers 在本例中由两部分组成:

  1. Logger - DataStoreLogger
  2. LoggingProvider - DataStoreLoggerProvider,它将生成 DataStoreLogger 实例。

Logger - DataStoreLogger 类

public class DataStoreLogger: ILogger
{
    #region Constructor

    public DataStoreLogger(
        string name,
        Func<DataStoreLoggerConfiguration> getCurrentConfig,
        ILogDataStore dataStore)
    {
        (_name, _getCurrentConfig) = (name, getCurrentConfig);
        _dataStore = dataStore;
    }

    #endregion

    #region Fields

    private readonly ILogDataStore _dataStore;
    private readonly string _name;
    private readonly Func<DataStoreLoggerConfiguration> _getCurrentConfig;

    #endregion

    #region methods

    public IDisposable BeginScope<TState>(TState state)  
                       where TState : notnull => default!;

    public bool IsEnabled(LogLevel logLevel) => true;

    public void Log<TState>(
        LogLevel logLevel,
        EventId eventId,
        TState state,
        Exception? exception,
        Func<TState, Exception, string> 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
}
Public Class DataStoreLogger : Implements ILogger

#Region "Constructors"

  Public Sub New(name As String, getCurrentConfig _
  As Func(Of DataStoreLoggerConfiguration), dataStore As ILogDataStore)
    _name = name
    _getCurrentConfig = getCurrentConfig
    _dataStore = dataStore
  End Sub

#End Region

#Region "Fields"

  Private ReadOnly _dataStore As ILogDataStore
  Private ReadOnly _name As String
  Private ReadOnly _getCurrentConfig As Func(Of DataStoreLoggerConfiguration)

#End Region

#Region "Methods"

  Public Function BeginScope(Of TState)(state As TState) As IDisposable
      Implements ILogger.BeginScope
    Return Nothing
  End Function

  Public Function IsEnabled(logLevel As LogLevel) As Boolean
      Implements ILogger.IsEnabled
    Return True
  End Function

  Public Overridable Sub Log(Of TState)(
      logLevel As LogLevel, eventId As EventId,
      state As TState, exception As Exception,
      formatter As Func(Of TState, Exception, String))
      Implements ILogger.Log

    If Not IsEnabled(logLevel) Then
      Return
    End If

    Dim exMessage As String = String.Empty

    If exception IsNot Nothing Then
      If String.IsNullOrEmpty(exception.Message) Then
        If logLevel = LogLevel.Error AndAlso state IsNot Nothing Then
          exMessage = state.ToString()
        End If
      Else
        exMessage = exception.Message
      End If
    End If

    Dim internalEventId As EventId = eventId
    Dim config As DataStoreLoggerConfiguration = _getCurrentConfig()

    If eventId.Id = 0 AndAlso config.EventId.Id <> 0 Then
      internalEventId = config.EventId
    End If

    _dataStore.AddEntry(New LogModel() With
    {
      .Timestamp = Now,
      .LogLevel = logLevel,
      .EventId = internalEventId,
      .State = state,
      .Exception = exMessage,
      .Color = config.Colors(logLevel)
    })

    Debug.WriteLine(
      $"--- [{logLevel.ToString()(0.3)}] {_name} - {formatter(state, exception)}")

  End Sub

#End Region

End Class

注意:自定义 DataStoreLogger 中的 Log 方法将日志添加到我们的 LogDataStore

Logger Provider - DataStoreLoggerProvider 类

public class DataStoreLoggerProvider: ILoggerProvider
{
    #region Constructor
    
    public DataStoreLoggerProvider(
        IOptionsMonitor<DataStoreLoggerConfiguration> 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<string, DataStoreLogger> _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
}
Public Class DataStoreLoggerProvider : Implements ILoggerProvider

#Region "Constructors"

  Public Sub New(config As IOptionsMonitor(Of DataStoreLoggerConfiguration),
                 dataStore As ILogDataStore)

    _dataStore = dataStore
    _currentConfig = config.CurrentValue
    _onChangeToken = config.OnChange(Sub(updatedConfig) _currentConfig = updatedConfig)

  End Sub

#End Region

#Region "Fields"

  Private _currentConfig As DataStoreLoggerConfiguration

  Private ReadOnly _onChangeToken As IDisposable
  Protected ReadOnly _dataStore As ILogDataStore

  Protected ReadOnly _loggers As ConcurrentDictionary(Of String, DataStoreLogger) =
            New ConcurrentDictionary(Of String, DataStoreLogger)()

#End Region

#Region "Methods"

  Public Overridable Function CreateLogger(categoryName As String) As ILogger
    Implements ILoggerProvider.CreateLogger

    Return _loggers.GetOrAdd(categoryName,
      Function(name)
        New DataStoreLogger(name, AddressOf GetCurrentConfig, _dataStore)
      End Function)

  End Function

  Protected Function GetCurrentConfig() As DataStoreLoggerConfiguration

    Return _currentConfig

  End Function

  Public Sub Dispose() Implements IDisposable.Dispose

    _loggers.Clear()
    _onChangeToken?.Dispose()

  End Sub

#End Region

End Class

注意:创建 DataStoreLogger 时,会注入 DataStoreLoggerConfigurationLogDataStore

注册 Microsoft Loggers

Microsoft Loggers 作为框架 HostApplicationBuilder 服务通过 ILoggingBuilder 注册。

这是 **.NET Framework** HostApplicationBuilder 类的精简代码。

/// <summary>
/// A builder for hosted applications and services which helps manage configuration,
/// logging, lifetime and more.
/// </summary>
public sealed class HostApplicationBuilder
{
    private readonly ServiceCollection _serviceCollection = new();

    // trimmed

    public HostApplicationBuilder(HostApplicationBuilderSettings? settings)
    {
        // trimmed
        Logging = new LoggingBuilder(Services);
        // trimmed
    }

    // trimmed

    /// <summary>
    /// A collection of services for the application to compose. This is useful for
    /// adding user provided or framework provided services.
    /// </summary>
    public IServiceCollection Services => _serviceCollection;

    /// <summary>
    /// A collection of services for the application to compose. This is useful for
    ///  adding user provided or framework provided services.
    /// </summary>
    public IServiceCollection Services => _serviceCollection;

    /// <summary>
    /// A collection of logging providers for the application to compose. 
    /// This is useful for adding new logging providers. 
    /// </summary>
    public ILoggingBuilder Logging { get; }

    // trimmed

    private sealed class LoggingBuilder : ILoggingBuilder
    {
        public LoggingBuilder(IServiceCollection services)
        {
            Services = services;
        }

        public IServiceCollection Services { get; }
    }

    // trimmed
}

注册 - ServicesExtension 类

LogDataStoreDataStoreLoggerConfigurationDataStoreLoggerProvider 类的注册被抽象到 ServicesExtension 类中的一个扩展方法。

public static class ServicesExtension
{
    public static ILoggingBuilder AddDefaultDataStoreLogger(this ILoggingBuilder builder)
    {
        builder.Services.TryAddEnumerable(
            ServiceDescriptor.Singleton<ILoggerProvider, DataStoreLoggerProvider>());
        return builder;
    }

    public static ILoggingBuilder AddDefaultDataStoreLogger(
        this ILoggingBuilder builder,
        Action<DataStoreLoggerConfiguration> configure)
    {
        builder.AddDefaultDataStoreLogger();
        builder.Services.Configure(configure);
        return builder;
    }
}
Public Module ServicesExtension

  <Extension>
  Public Function AddDefaultDataStoreLogger(builder As ILoggingBuilder) _
         As ILoggingBuilder

    builder.Services.TryAddEnumerable(
      ServiceDescriptor.Singleton(Of ILoggerProvider, DataStoreLoggerProvider))
    Return builder

  End Function

  <Extension>
  Public Function AddDefaultDataStoreLogger( _
    builder As ILoggingBuilder,
    configure As Action(Of DataStoreLoggerConfiguration)) As ILoggingBuilder

    builder.AddDefaultDataStoreLogger()
    builder.Services.Configure(configure)
    Return builder

  End Function

End Module

依赖注入

这是使用默认配置连接依赖注入的示例。

HostApplicationBuilder builder = Host.CreateApplicationBuilder();

builder.AddLogViewer();
builder.Logging.AddDefaultDataStoreLogger();

_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.AddLogViewer()
builder.Logging.AddDefaultDataStoreLogger()

_host = builder.Build()

或者,如果使用自定义配置。

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();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.AddLogViewer()
builder.Logging.AddDefaultDataStoreLogger(
  Sub(options)

    options.Colors(LogLevel.Trace) = New LogEntryColor() With
    {
      .Foreground = Color.White,
      .Background = Color.DarkGray
    }

    options.Colors(LogLevel.Debug) = New LogEntryColor() With
    {
      .Foreground = Color.White,
      .Background = Color.Gray
    }

    options.Colors(LogLevel.Information) = New LogEntryColor() With
    {
      .Foreground = Color.White,
      .Background = Color.DodgerBlue
    }

    options.Colors(LogLevel.Warning) = New LogEntryColor() With
    {
      .Foreground = Color.White,
      .Background = Color.Orchid
    }

  End Sub)


_host = builder.Build()

要创建 Logger,您可以将实例注入到类构造函数中。

public class RandomLoggingService : BackgroundService
{
    #region Constructors

    public RandomLoggingService(ILogger<RandomLoggingService> logger)
        => _logger = logger;

    #endregion

    #region Fields

    private readonly ILogger _logger;

    #endregion
}
Public Class RandomLoggingService : Inherits BackgroundService

#Region "Constructors"

  Public Sub New(logger As ILogger(Of RandomLoggingService))

    _logger = logger

  End Sub

#End Region

#Region "Fields"

  Private _logger As ILogger

#End Region

End Class

或者手动请求实例。

ILogger<class_name> logger
  = _host.Services.GetRequiredService<ILogger<class_name>>();
Dim logger As ILogger(Of class_name)
  = _host.Services.GetRequiredService(Of ILogger(Of class_name))

这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。

手动(不使用依赖注入)

如果不使用依赖注入,仍然可以注册一个或多个 Logger。我们将需要一个单例类来保存注册信息以及用于生成 Logger 实例的 Factory 方法。

这是本文示例应用程序中使用的 LoggingHelper 类。

public static class LoggingHelper
{
     #region Constructors

   static LoggingHelper()
    {
        // retrieve the log level from 'appsettings'
        string value = AppSettings<string>.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
}
Public Module LoggingHelper

#Region "Constructors"

 Sub New()

  ' retrieve the log level from 'appsettings'
  Dim value As String = AppSettings(Of String).Current("Logging:LogLevel", "Default")
  If String.IsNullOrWhiteSpace(value) Then
   value = "Information"
  End If

  Dim logLevel As LogLevel
  If Not [Enum].TryParse(value, logLevel) Then
   logLevel = LogLevel.Information
  End If

  ' wire up the loggers
  Factory = LoggerFactory.Create(
   Sub(builder)

    ' visual debugging tools
    builder.AddDataStoreLogger()

    ' example of adding other loggers...
    builder.AddSimpleConsole(
      Sub(options)
       options.SingleLine = True
       options.TimestampFormat = "hh:mm:ss "
      End Sub)

    ' set minimum log level from 'appsettings'
    builder.SetMinimumLevel(logLevel)

   End Sub)

 End Sub

#End Region

#Region "Properties"

 Public ReadOnly Property Factory As ILoggerFactory

#End Region

End Module

或者,如果使用自定义配置。

public static class LoggingHelper
{
    #region Constructors

    static LoggingHelper()
    {
        // retrieve the log level from 'appsettings'
        string value = AppSettings<string>.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
}
Public Module LoggingHelper

#Region "Constructors"

 Sub New()

  ' retrieve the log level from 'appsettings'
  Dim value As String = AppSettings(Of String).Current("Logging:LogLevel", "Default")
  If String.IsNullOrWhiteSpace(value) Then
   value = "Information"
  End If

  Dim logLevel As LogLevel
  If Not [Enum].TryParse(value, logLevel) Then
   logLevel = LogLevel.Information
  End If

  ' wire up the loggers
  Factory = LoggerFactory.Create(
   Sub(builder)

    ' visual debugging tools
    builder.AddDataStoreLogger(
      Sub(options)

        options.Colors(LogLevel.Trace) = New LogEntryColor() With
        {
          .Foreground = Color.White,
          .Background = Color.DarkGray
        }

        options.Colors(LogLevel.Debug) = New LogEntryColor() With
        {
          .Foreground = Color.White,
          .Background = Color.Gray
        }

        options.Colors(LogLevel.Information) = New LogEntryColor() With
        {
          .Foreground = Color.White,
          .Background = Color.DodgerBlue
        }

        options.Colors(LogLevel.Warning) = New LogEntryColor() With
        {
          .Foreground = Color.White,
          .Background = Color.Orchid
        }

      End Sub)

    ' example of adding other loggers...
    builder.AddSimpleConsole(
      Sub(options)
       options.SingleLine = True
       options.TimestampFormat = "hh:mm:ss "
      End Sub)

    ' set minimum log level from 'appsettings'
    builder.SetMinimumLevel(logLevel)

   End Sub)

 End Sub

#End Region

#Region "Properties"

 Public ReadOnly Property Factory As ILoggerFactory

#End Region

End Module

要创建 Logger,请使用上面 LoggingHelper 类的 Factory 方法。

Logger<class_name> logger
  = new Logger<class_name>(LoggingHelper.Factory);
Dim logger As Logger(Of class_name)
  = New Logger(Of class_name)(LoggingHelper.Factory)

注意

创建 Logger 时,类需要被实例化/创建。如果类未实例化,则会抛出错误。

将 Logger 作为构造函数参数创建是可以接受的。例如,以下是可接受的。

RandomLoggingService service
  = new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
Dim service As RandomLoggingService
  = New RandomLoggingService(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))

这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。

自定义 Serilog Logger 实现

Serilog Sinks (Loggers) 的实现与 Microsoft Logger 实现不同。但是,为了与 Microsoft Logging Framework 一起工作,Serilog 实现 Logger Provider,以便 Microsoft Logging Framework 可以将数据传递给 Serilog Sinks(Logger 实现)。

Logger - DataStoreLoggerSink 类

public class DataStoreLoggerSink : ILogEventSink
{
    protected readonly Func<ILogDataStore> _dataStoreProvider;
    
    private readonly IFormatProvider? _formatProvider;
    private readonly Func<DataStoreLoggerConfiguration>? _getCurrentConfig;

    public DataStoreLoggerSink(Func<ILogDataStore> dataStoreProvider,
                               Func<DataStoreLoggerConfiguration>? 
                               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;
    }
}
Public Class DataStoreLoggerSink : Implements ILogEventSink

  Protected ReadOnly _dataStoreProvider As Func(Of ILogDataStore)

  Private ReadOnly _formatProvider As IFormatProvider
  Private ReadOnly _getCurrentConfig As Func(Of DataStoreLoggerConfiguration)

  Public Sub New(dataStoreProvider As Func(Of ILogDataStore),
         Optional getCurrentConfig As Func(Of DataStoreLoggerConfiguration) = Nothing,
         Optional formatProvider As IFormatProvider = Nothing)
    _dataStoreProvider = dataStoreProvider
    _formatProvider = formatProvider
    _getCurrentConfig = getCurrentConfig
  End Sub


  Public Sub Emit(logEvent As LogEvent) Implements ILogEventSink.Emit

    Dim logLevel As LogLevel

    Select Case logEvent.Level
      Case LogEventLevel.Verbose : logLevel = LogLevel.Trace
      Case LogEventLevel.Debug : logLevel = LogLevel.Debug
      Case LogEventLevel.Warning : logLevel = LogLevel.Warning
      Case LogEventLevel.Error : logLevel = LogLevel.Error
      Case LogEventLevel.Fatal : logLevel = LogLevel.Critical
      Case Else : logLevel = LogLevel.Information
    End Select

    Dim config As DataStoreLoggerConfiguration = If(_getCurrentConfig Is Nothing,
                            New DataStoreLoggerConfiguration(),
                            _getCurrentConfig.Invoke())

    Dim eventId As EventId = EventIdFactory(logEvent)
    If eventId.Id = 0 AndAlso config.EventId <> 0 Then
      eventId = config.EventId
    End If

    Dim message As String = logEvent.RenderMessage(_formatProvider)

    Dim exception As String = If(logEvent.Exception Is Nothing,
                  If(logEvent.Level >= LogEventLevel.Error, message, String.Empty),
                  logEvent.Exception.Message)

    Dim color As LogEntryColor = config.Colors(logLevel)

    AddLogEntry(logLevel, eventId, message, exception, color)

  End Sub

  Protected Overridable Sub AddLogEntry(logLevel As LogLevel, eventId As EventId,
                                        message As String, exception As String,
                                        color As LogEntryColor)

    Dim dataStore As ILogDataStore = _dataStoreProvider.Invoke()

    If dataStore Is Nothing Then
      Return
    End If

    dataStore.AddEntry(
      New LogModel() With
      {
        .Timestamp = DateTime.UtcNow,
        .LogLevel = logLevel,
        .EventId = eventId,
        .State = message,
        .Exception = exception,
        .Color = color
      })

  End Sub

  Private Shared Function EventIdFactory(logEvent As LogEvent) As EventId

    Dim eventId As EventId
    Dim src As LogEventPropertyValue

    If Not logEvent.Properties.TryGetValue("EventId", src) Then
      Return New EventId()
    End If

    Dim id As Integer = Nothing
    Dim eventName As String = Nothing

    ' ref: https://stackoverflow.com/a/56722516
    Dim value As StructureValue = DirectCast(src, StructureValue)

    Dim idProperty As LogEventProperty
      = value.Properties.FirstOrDefault(Function(x) x.Name.Equals("Id"))

    If idProperty IsNot Nothing Then
      id = Integer.Parse(idProperty.Value.ToString())
    End If

    Dim nameProperty As LogEventProperty
      = value.Properties.FirstOrDefault(Function(x) x.Name.Equals("Name"))

    If nameProperty IsNot Nothing Then
      eventName = nameProperty.Value.ToString().Trim(""""c)
    End If

    eventId = New EventId(
      If(id = Nothing, 0, id),
      If(String.IsNullOrWhiteSpace(eventName), String.Empty, eventName))

    Return eventId

  End Function

End Class

配置自定义 Sink - DataStoreLoggerSinkExtensions 类

与 Microsoft ILoggerProvider 实现不同,将配置传递给自定义 Sink 的方式不同。没有 Provider,因此我们将过程封装在扩展方法中。

public static class DataStoreLoggerSinkExtensions
{
    public static LoggerConfiguration DataStoreLoggerSink
    (
        this LoggerSinkConfiguration loggerConfiguration,
        Func<ILogDataStore> dataStoreProvider, 
        Action<DataStoreLoggerConfiguration>? configuration = null,
        IFormatProvider formatProvider = null!
    )
        => loggerConfiguration.Sink(
            new DataStoreLoggerSink(
                dataStoreProvider,
                GetConfig(configuration),
                formatProvider));

    private static Func<DataStoreLoggerConfiguration> GetConfig(
        Action<DataStoreLoggerConfiguration>? configuration)
    {
        // convert from Action to Func delegate to pass data
        DataStoreLoggerConfiguration data = new();
        configuration?.Invoke(data);
        return () => data;
    }
}
Public Module DataStoreLoggerSinkExtensions

  <Extension>
  Public Function DataStoreLoggerSink(loggerConfiguration As LoggerSinkConfiguration,
                    dataStoreProvider As Func(Of ILogDataStore),
                    Optional configuration As Action_
                    (Of DataStoreLoggerConfiguration) = Nothing,
                    Optional formatProvider As IFormatProvider = Nothing) _
                    As LoggerConfiguration

    Return loggerConfiguration.Sink(
      New DataStoreLoggerSink(dataStoreProvider,
                              GetConfig(configuration),
                              formatProvider))

  End Function

  Private Function GetConfig(configuration As Action(Of DataStoreLoggerConfiguration))
    As Func(Of DataStoreLoggerConfiguration)

    Dim data As DataStoreLoggerConfiguration = New DataStoreLoggerConfiguration()

    If configuration IsNot Nothing Then
      configuration.Invoke(data)
    End If

    Return Function() data

  End Function

End Module

注册 Sinks (Loggers)

Serilog 有两种注册 Sinks 的方法:

  1. 手动在代码中
  2. 通过 appsetting* 配置文件

由于我们需要注入 Sink 配置,因此我们将使用第一种方法来处理自定义 Sink,但 Serilog 配置和其他 Sink 将通过 appsetting* 配置文件完成。下面是本文使用的配置。

  "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" ]
  }
}

依赖注入

将 Serilog 与 .NET Logging Framework 结合使用的方式与 Microsoft 实现不同。我们需要在主机服务之后手动注入 LogDataStore 引用,但要在服务构建之前创建 Serilog Logger,并通过依赖注入传递配置。我们使用 Lambda 表达式(内联委托方法)来实现这一点,该方法将在每次创建 Logger 实例时调用。

这是使用默认配置连接依赖注入的示例。

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<ILogDataStore>()!)
        .CreateLogger();

    cfg.ClearProviders()
        .AddSerilog(Log.Logger);
});

_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.AddLogViewer()

Dim services As IServiceCollection = builder.Services

services.AddLogging(
 Sub(cfg)

  ' Use Default Colors
  Log.Logger = New LoggerConfiguration() _
    .ReadFrom.Configuration(builder.Configuration) _
    .WriteTo.DataStoreLoggerSink(
    Function() _host.Services.TryGetService(Of ILogDataStore)) _
  .CreateLogger()

  cfg.ClearProviders() _
   .AddSerilog(Log.Logger)

 End Sub)

_host = builder.Build()

或者,如果使用自定义配置。

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<ILogDataStore>()!,
        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();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.AddLogViewer()

Dim services As IServiceCollection = builder.Services

services.AddLogging(
 Sub(cfg)

  ' Use Custom Colors
  Log.Logger = New LoggerConfiguration() _
    .ReadFrom.Configuration(builder.Configuration) _
    .WriteTo.DataStoreLoggerSink(
     Function() _host.Services.TryGetService(Of ILogDataStore),
     Sub(options)
      options.Colors(LogLevel.Trace) = New LogEntryColor() With
      {
       .Foreground = Color.White,
       .Background = Color.DarkGray
      }

      options.Colors(LogLevel.Debug) = New LogEntryColor() With
      {
       .Foreground = Color.White,
       .Background = Color.Gray
      }

      options.Colors(LogLevel.Information) = New LogEntryColor() With
      {
       .Foreground = Color.White,
       .Background = Color.DodgerBlue
      }

      options.Colors(LogLevel.Warning) = New LogEntryColor() With
      {
       .Foreground = Color.White,
       .Background = Color.Orchid
      }

     End Sub) _
    .CreateLogger()

  cfg.ClearProviders() _
   .AddSerilog(Log.Logger)

 End Sub)

_host = builder.Build()

注意

  • 我们存储 Logger 工厂实例的引用,以便在应用程序关闭时,可以刷新所有 Sink 的缓冲区,例如文件或远程存储。

要创建 logger,您可以将实例注入到类构造函数中。

public class RandomLoggingService : BackgroundService
{
    #region Constructors

    public RandomLoggingService(ILogger<RandomLoggingService> logger)
        => _logger = logger;

    #endregion

    #region Fields

    private readonly ILogger _logger;

    #endregion
}
Public Class RandomLoggingService : Inherits BackgroundService

#Region "Constructors"

  Public Sub New(logger As ILogger(Of RandomLoggingService))

    _logger = logger

  End Sub

#End Region

#Region "Fields"

  Private _logger As ILogger

#End Region

End Class

或者手动请求实例。

ILogger<class> logger = _host.Services.GetRequiredService<ILogger<class>>();
Dim logger As ILogger(Of class_name) =
    _host.Services.GetRequiredService(Of ILogger(Of class_name))

这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。

手动(不使用依赖注入)

如果不使用依赖注入,仍然可以注册一个或多个 Logger。我们将需要一个单例类来保存注册信息以及用于生成 Logger 实例的 Factory 方法。

这是本文示例应用程序中使用的 LoggingHelper 类。

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
}
Public Module LoggingHelper

#Region "Constructors"

 Sub New()

  Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
   .Initialize() _
   .Build()

  ' Use Default Colors
  Log.Logger = New LoggerConfiguration() _
  .ReadFrom.Configuration(configuration) _
  .WriteTo.DataStoreLoggerSink(Function() MainControlsDataStore.DataStore) _
  .CreateLogger()

  ' wire up the loggers
  Factory = LoggerFactory.Create(
    Sub(LoggingBuilder)
      LoggingBuilder.AddSerilog(Log.Logger)
    End Sub)

 End Sub

#End Region

#Region "Properties"

 Public ReadOnly Property Factory As ILoggerFactory

#End Region

#Region "Methods"

 Friend Sub CloseAndFlush()

  Log.CloseAndFlush()

 End Sub

#End Region

End Module

或者,如果使用自定义配置。

public static class LoggingHelper
{
    #region Constructors

    static LoggingHelper()
    {
        IConfigurationRoot configuration = new ConfigurationBuilder()
            .Initialize()
            .Build();

       // Use Custom Colors
        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
}
Public Module LoggingHelper

#Region "Constructors"

 Sub New()

  Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
   .Initialize() _
   .Build()

  ' Use Custom Colors
  Log.Logger = New LoggerConfiguration() _
   .ReadFrom.Configuration(configuration) _
   .WriteTo.DataStoreLoggerSink(
    Function() MainControlsDataStore.DataStore,
    Sub(options)

     options.Colors(LogLevel.Trace) = New LogEntryColor() With
     {
      .Foreground = Color.White,
      .Background = Color.DarkGray
     }

     options.Colors(LogLevel.Debug) = New LogEntryColor() With
     {
      .Foreground = Color.White,
      .Background = Color.Gray
     }

     options.Colors(LogLevel.Information) = New LogEntryColor() With
     {
      .Foreground = Color.White,
      .Background = Color.DodgerBlue
     }

     options.Colors(LogLevel.Warning) = New LogEntryColor() With
     {
      .Foreground = Color.White,
      .Background = Color.Orchid
     }

    End Sub) _
   .CreateLogger()

  ' wire up the loggers
  Factory = LoggerFactory.Create(Sub(LoggingBuilder) 
            LoggingBuilder.AddSerilog(Log.Logger))

 End Sub

#End Region

#Region "Properties"

 Public ReadOnly Property Factory As ILoggerFactory

#End Region

#Region "Methods"

 Friend Sub CloseAndFlush()

  Log.CloseAndFlush()

 End Sub

#End Region

End Module

要创建 Logger,请使用上面 LoggingHelper 类的 Factory 方法。

Logger<class> logger = new Logger<class>(LoggingHelper.Factory);
Dim logger As Logger(Of class_name) = New Logger(Of class_name)(LoggingHelper.Factory)

注意

创建 Logger 时,类需要被实例化/创建。如果类未实例化,则会抛出错误。

将 Logger 作为构造函数参数创建是可以接受的。例如,以下是可接受的。

RandomLoggingService service = 
    new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
Dim service As RandomLoggingService =
    New RandomLoggingService(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))

这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。

自定义 NLog Target Logger 实现 (最新)

NLog Targets (Loggers) 的实现与 Microsoft Logger 实现不同。但是,为了与 Microsoft Logging Framework 一起工作,NLog 内部实现了 Logger Provider,以便 Microsoft Logging Framework 可以将数据传递给 **NLog** Targets(Logger 实现)。

在实现自定义 NLog Target 时,必须注册该 Target,然后在配置文件中启用它。我们将实现 NLog 配置在 *appsetting*.json 文件中。

Logger - DataStoreLoggerTarget 类

[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<IServiceProvider>();

        // reference the shared instance
        _dataStore = serviceProvider.GetRequiredService<ILogDataStore>();

        // load the config options
        IOptionsMonitor<DataStoreLoggerConfiguration>? options
        = serviceProvider.GetService<IOptionsMonitor<DataStoreLoggerConfiguration>>();

        _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
}
<target("datastorelogger")>
Public Class DataStoreLoggerTarget : Inherits TargetWithLayout

#Region "Fields"

    Private _dataStore As ILogDataStore
    Private _config As DataStoreLoggerConfiguration

#End Region

#Region "methods"

    Protected Overrides Sub InitializeTarget()

        ' we need to inject dependencies
        Dim serviceProvider As IServiceProvider = ResolveService(Of IServiceProvider)()

        ' reference the shared instance
        _dataStore = serviceProvider.GetRequiredService(Of ILogDataStore)

        ' load the config options
        Dim options As IOptionsMonitor(Of DataStoreLoggerConfiguration) _
            = serviceProvider.GetService(Of IOptionsMonitor(Of DataStoreLoggerConfiguration))

        _config =
          If(options Is Nothing, _
             New DataStoreLoggerConfiguration(), _
             options.CurrentValue)

        MyBase.InitializeTarget()

    End Sub

    Protected Overrides Sub Write(logEvent As LogEventInfo)

        ' cast NLog Loglevel to Microsoft LogLevel type
        Dim logLevel As MsLogLevel
          = [Enum].ToObject(GetType(MsLogLevel), logEvent.Level.Ordinal)

        ' format the message
        Dim message As String = RenderLogEvent(Layout, logEvent)

        ' retrieve the EventId
        Dim eventId As EventId = logEvent.Properties("EventId")

        If eventId.Id = 0 AndAlso _config.EventId.Id <> 0 Then
            eventId = _config.EventId
        End If

        Dim exMessage As String = String.Empty

        If logEvent.Exception IsNot Nothing Then
            If String.IsNullOrEmpty(logEvent.Exception.Message) Then
                If logLevel = MsLogLevel.Error AndAlso message IsNot Nothing Then
                    exMessage = message.ToString()
                End If
            Else
                exMessage = logEvent.Exception.Message
            End If
        End If

        ' add log entry
        _dataStore.AddEntry(New LogModel() With
        {
            .Timestamp = Date.UtcNow,
           .LogLevel = logLevel,
           .EventId = eventId,
           .State = message,
           .Exception = exMessage,
           .Color = _config.Colors(logLevel)
        })

        Debug.WriteLine(
          $"--- [{logLevel.ToString()(0.3)}] {message} - " + 
          $"{If(String.IsNullOrWhiteSpace(exMessage), "no error", exMessage)}")

        MyBase.Write(logEvent)

    End Sub

#End Region

End Class

配置自定义 Target - ServicesExtension 类

public static class ServicesExtension
{
    public static ILoggingBuilder AddNLogTargets(
        this ILoggingBuilder builder, IConfiguration config)
    {
        LogManager
            .Setup()
            // Register custom Target
            .SetupExtensions(extensionBuilder =>
            extensionBuilder.RegisterTarget<DataStoreLoggerTarget>("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<DataStoreLoggerConfiguration> configure)
    {
        builder.AddNLogTargets(config);
        builder.Services.Configure(configure);
        return builder;
    }
}
Public Module ServicesExtension

    <extension>
    Public Function AddNLogTargets( _
        builder As ILoggingBuilder, config As IConfiguration) As ILoggingBuilder

        LogManager _
            .Setup() _
            .SetupExtensions(
                Sub(extensionBuilder) _
                    extensionBuilder.RegisterTarget(Of DataStoreLoggerTarget)
                    ("DataStoreLogger"))

        builder _
            .ClearProviders() _
            .SetMinimumLevel(MsLogLevel.Trace) _
            .AddNLog(config,
                New NLogProviderOptions With
                {
                    .IgnoreEmptyEventId = False,
                    .CaptureEventId = EventIdCaptureType.Legacy
                })

        Return builder

    End Function

    <extension>
    Public Function AddNLogTargets( _
        builder As ILoggingBuilder, config As IConfiguration, _
        configure As Action(Of DataStoreLoggerConfiguration)) As ILoggingBuilder

        builder.AddNLogTargets(config)
        builder.Services.Configure(configure)
        Return builder

    End Function

End Module

注册 Targets (Loggers)

NLog 有两种注册 Sinks 的方法:

  1. 手动在代码中
  2. 通过 appsetting* 配置文件

由于我们需要注入 Target 配置,因此我们将使用上述第二种方法(在代码中注册自定义 Target),并且如上所示。下面是本文使用的配置。

{
  "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"
      }
    ]
  }
}

依赖注入

ServicesExtension 类和 appsetting* 配置文件连接了 Target 的注册,包括我们的自定义 Target,并配置 NLog 与 .NET Logging Framework 一起工作。现在我们需要告诉 Host 使用 NLog Logging。

这是使用默认配置连接依赖注入的示例。

HostApplicationBuilder builder = Host.CreateApplicationBuilder();

builder.AddLogViewer();

builder.Logging.AddNLogTargets(builder.Configuration);

_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.AddLogViewer()

builder.Logging.AddNLogTargets(builder.Configuration);

_host = builder.Build()

或者,如果使用自定义配置。

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();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.AddLogViewer()

builder.Logging.AddNLogTargets(
    builder.Configuration,
    Sub(options)

        options.Colors(LogLevel.Trace) = New LogEntryColor() With
        {
            .Foreground = Color.White,
            .Background = Color.DarkGray
        }

        options.Colors(LogLevel.Debug) = New LogEntryColor() With
        {
            .Foreground = Color.White,
            .Background = Color.Gray
        }

        options.Colors(LogLevel.Information) = New LogEntryColor() With
        {
            .Foreground = Color.White,
            .Background = Color.DodgerBlue
        }

        options.Colors(LogLevel.Warning) = New LogEntryColor() With
        {
            .Foreground = Color.White,
            .Background = Color.Orchid
        }

    End Sub)

_host = builder.Build()

要创建 Logger,您可以将实例注入到类构造函数中。

public class RandomLoggingService : BackgroundService
{
#region Constructors

    public RandomLoggingService(ILogger<RandomLoggingService> logger)
        => _logger = logger;

    #endregion

    #region Fields

    private readonly ILogger _logger;

    #endregion
}
Public Class RandomLoggingService : Inherits BackgroundService

#Region "Constructors"

  Public Sub New(logger As ILogger(Of RandomLoggingService))

    _logger = logger

  End Sub

#End Region

#Region "Fields"

  Private _logger As ILogger

#End Region

End Class

或者手动请求实例。

ILogger<class> logger
    = _host.Services.GetRequiredService<ILogger<class>>();
Dim logger As ILogger(Of class_name)
  = _host.Services.GetRequiredService(Of ILogger(Of class_name))

这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。

手动(不使用依赖注入)

如果不使用依赖注入,仍然可以注册一个或多个 Logger。

我们需要包装用于依赖注入的 ServicesExtension 来使用非 DI 版本的 LogDataStore 类。

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<DataStoreLoggerConfiguration> configure)
    {
        builder.AddNLogTargetsNoDI(config);
        builder.Services.Configure(configure);
        return builder;
    }
}
Public Module ServicesExtension

    <extension>
    Public Function AddNLogTargetsNoDI( _
        builder As ILoggingBuilder, _
        config As IConfiguration) As ILoggingBuilder

        ' 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

    End Function

    <extension>
    Public Function AddNLogTargetsNoDI( _
        builder As ILoggingBuilder, config As IConfiguration, _
        configure As Action(Of DataStoreLoggerConfiguration)) As ILoggingBuilder

        builder.AddNLogTargetsNoDI(config)
        builder.Services.Configure(configure)
        Return builder

    End Function

End Module

我们还需要一个单例类来保存注册信息以及用于生成 Logger 实例的 Factory 方法。这是本文示例应用程序中使用的 LoggingHelper 类。

public static class LoggingHelper
{
    #region Constructors

    static LoggingHelper()
    {
        // retrieve the log level from 'appsettings'
        string value = AppSettings<string>
            .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
}
Public Module LoggingHelper

#Region "Constructors"

	Sub New()

		' retrieve the log level from 'appsettings'
		Dim value As String = AppSettings(Of String) _
            .Current("Logging:LogLevel", "Default")

		If String.IsNullOrWhiteSpace(value) Then
			value = "Information"
		End If

		Dim logLevel As LogLevel
		If Not [Enum].TryParse(value, logLevel) Then
			logLevel = LogLevel.Information
		End If

		Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
			.Initialize() _
			.Build()

		' wire up the loggers
		Factory = LoggerFactory.Create(
			Sub(builder)

				builder.AddNLogTargetsNoDI(configuration)

				' set minimum log level from 'appsettings'
				builder.SetMinimumLevel(logLevel)

			End Sub)

	End Sub

#End Region

#Region "Properties"

	Public ReadOnly Property Factory As ILoggerFactory

#End Region

End Module

或者,如果使用自定义配置。

public static class LoggingHelper
{
    #region Constructors

    static LoggingHelper()
    {
        // retrieve the log level from 'appsettings'
        string value = AppSettings<string>
            .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, 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
}
Public Module LoggingHelper

#Region "Constructors"

	Sub New()

		' retrieve the log level from 'appsettings'
		Dim value As String = AppSettings(Of String) _
            .Current("Logging:LogLevel", "Default")

		If String.IsNullOrWhiteSpace(value) Then
			value = "Information"
		End If

		Dim logLevel As LogLevel
		If Not [Enum].TryParse(value, logLevel) Then
			logLevel = LogLevel.Information
		End If

		Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
			.Initialize() _
			.Build()

		' wire up the loggers
		Factory = LoggerFactory.Create(
			Sub(builder)

				builder.AddNLogTargetsNoDI(
					configuration,
					Sub(options)

						options.Colors(LogLevel.Trace) = New LogEntryColor() With
						{
							.Foreground = Color.White,
							.Background = Color.DarkGray
						}

						options.Colors(LogLevel.Debug) = New LogEntryColor() With
						{
							.Foreground = Color.White,
							.Background = Color.Gray
						}

						options.Colors(LogLevel.Information) = New LogEntryColor() With
						{
							.Foreground = Color.White,
							.Background = Color.DodgerBlue
						}

						options.Colors(LogLevel.Warning) = New LogEntryColor() With
						{
							.Foreground = Color.White,
							.Background = Color.Orchid
						}

					End Sub)

				' set minimum log level from 'appsettings'
				builder.SetMinimumLevel(logLevel)

			End Sub)

	End Sub

#End Region

#Region "Properties"

	Public ReadOnly Property Factory As ILoggerFactory

#End Region

End Module

要创建 Logger,请使用上面 LoggingHelper 类的 Factory 方法。

Logger<class> logger
    = new Logger<class>(LoggingHelper.Factory);
Dim logger As Logger(Of class_name)
  = New Logger(Of class_name)(LoggingHelper.Factory)

注意

  • 创建 Logger 时,类需要被实例化/创建。如果类未实例化,则会抛出错误。

将 Logger 作为构造函数参数创建是可以接受的。例如,以下是可接受的。

RandomLoggingService service
    = new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
Dim service As RandomLoggingService
  = New RandomLoggingService(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))

这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。

自定义 Apache Log4Net Appender Logger 实现

虽然 Log4Net 支持 .NET Framework(.NET Core 1.0 提供 .NET Standard 1.3),但 Log4Net 在实现时涉及最多,因为有:

  • 对日志记录系统和自定义 Appenders 没有原生的依赖注入支持。
  • 不支持使用 EventID 或其他自定义属性进行日志记录。

通过研究,我找到了一个开源项目 huorswords / Microsoft.Extensions.Logging.Log4Net.AspNetCore,它支持 .NET Framework 的依赖注入,但缺少以下要求:

  • 对自定义 Appenders 没有依赖注入。
  • 不支持使用 EventID 或其他自定义属性进行日志记录。

您可以在此处阅读有关此项目的更多信息:How to use Log4Net with ASP.NET Core for logging | DotNetThoughts Blog。请注意,项目名称有点误导。它不只是针对 AspNetCore。它适用于其他应用程序项目类型。

向 Microsoft.Extensions.Logging.Log4Net.AspNetCore 添加缺失的部分

虽然缺少两个关键要求,但这是一个开源项目,因此我们可以用缺失的部分更新它。以下部分将介绍我们如何通过添加向后兼容当前实现的方式来实现这一点,以避免任何中断性更改。

我将为缺失的部分创建一个 pull request。但是,目前,我已将更新后的项目包含在本文章的下载内容中。

添加 EventID 支持

没有关于如何向内部 Log4Net Logger 添加功能的官方文档。幸运的是,我在官方 Log4Net 存储库上找到了一个如何做的示例:http://svn.apache.org/logging/log4net

添加 EventId 支持有三个部分:

  1. 包装基础 Log4Net Logger 实现(Interface + Class)。
  2. 更新 Microsoft.Extensions.Logging.Log4Net.AspNetCore 中的 Log4NetLogger 类以使用新的 Logger 类。

以下是使用的实现:

  1. Logger 包装器

    a. IEventIDLog Interface

    	public interface IEventIDLog : ILog
    	{
    		void Log(EventId eventId, LoggingEvent loggingEvent);
    	}
    	

    b. EventIDLogImpl class

    	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. 更新 Log4NetLogger

    我将仅显示所做的更改——我们更改了实现,现在可以注入缺失的 EventId 引用。

        public class Log4NetLogger : ILogger
        {
            private readonly IEventIDLog eventIdLogger;
        
            public void Log<TState>(
                LogLevel logLevel,
                EventId eventId,
                TState state,
                Exception exception,
                Func<TState, Exception, string> formatter)
            {
                if (!this.IsEnabled(logLevel))
                {
                    return;
                }
        
                EnsureValidFormatter(formatter);
        
                var candidate = new MessageCandidate<TState>(
                    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);
            }
        }
        
为 Appender 支持添加依赖注入支持

这有 2 个部分:

  1. 包装基础 AppenderSkeleton 类并支持 DI。
  2. 更新 Log4NetProvider 类以准备支持 DI 的 AppenderSkeleton 类。

以下是使用的实现:

  1. 支持 DI 的 ServiceAppenderSkeleton 包装器类。

    我们定义了一个 internal 显式方法来设置 DI 服务提供程序引用,以及一个 protected 方法,该方法可用于从我们的自定义 Appender 中解析任何必需的依赖项。

            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<T>() where T : class
                {
                    if (_serviceProvider == null)
                        return default;
            
                    return _serviceProvider.GetService<T>();
                }
            
                public void Dispose() => _serviceProvider = null;
            }
            
  2. 更新 Log4NetProvider

    我将仅显示所做的更改,以向实现 IAppenderServiceProvider 接口的 Appender 添加 DI 服务提供程序引用。

            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<IAppenderServiceProvider> adapters =
                        this.Repository
                            .GetAppenders()
                            .OfType<IAppenderServiceProvider>();
            
                    foreach (IAppenderServiceProvider adapter in adapters)
                        adapter.ServiceProvider = serviceProvider;
                }
            }
            

Logger - DataStoreLoggerAppender 类

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<IServiceProvider>();
        _dataStore = _serviceProvider.GetRequiredService<ILogDataStore>();
        _options = _serviceProvider.GetService<DataStoreLoggerConfiguration>();
    }

    #endregion
}
Public Class DataStoreLoggerAppender : Inherits ServiceAppenderSkeleton

#Region "Fields"

    Private _dataStore As ILogDataStore
    Private _options As DataStoreLoggerConfiguration

    Private _serviceProvider As IServiceProvider

#End Region

#Region "Methods"

    Protected Overrides Sub Append(loggingEvent As LoggingEvent)

        If _serviceProvider Is Nothing Then
            Initialize()
        End If

        ' cast matching Log4Net Loglevel to Microsoft LogLevel type
        Dim logLevel As LogLevel

        Select Case loggingEvent.Level.Value
            Case Integer.MaxValue : logLevel = LogLevel.None
            Case 120000 : logLevel = LogLevel.Debug
            Case 90000 : logLevel = LogLevel.Critical
            Case 70000 : logLevel = LogLevel.Error
            Case 60000 : logLevel = LogLevel.Warning
            Case 20000 : logLevel = LogLevel.Trace
            Case Else : logLevel = LogLevel.Information
        End Select

        Dim config As DataStoreLoggerConfiguration =
            If(_options Is Nothing, New DataStoreLoggerConfiguration, _options)

        Dim eventId As EventId = loggingEvent.LookupProperty(NameOf(eventId))
        eventId = If(eventId = Nothing AndAlso config.EventId.Id <> 0, _
                                               config.EventId, eventId)

        Dim message As String = loggingEvent.RenderedMessage

        Dim exceptionMessage = loggingEvent.GetExceptionString()

        _dataStore.AddEntry(
            New LogModel() With
            {
                .Timestamp = Date.UtcNow,
                .LogLevel = logLevel,
                .State = message,
                .Exception = exceptionMessage,
                .Color = config.Colors(logLevel)
            })

        Debug.WriteLine($"--- [{logLevel.ToString()(0.3)}] {message}" +
            " - {If(String.IsNullOrWhiteSpace(exceptionMessage), _
                 "no error", exceptionMessage)}")

    End Sub

    Private Sub Initialize()

        _serviceProvider = ResolveService(Of IServiceProvider)()
        _dataStore = _serviceProvider.GetRequiredService(Of ILogDataStore)
        _options = _serviceProvider.GetService(Of DataStoreLoggerConfiguration)

    End Sub

#End Region

End Class

配置自定义 Appender - ServicesExtension 类

public static class ServicesExtension
{
    public static ILoggingBuilder AddLog4Net_
           (this ILoggingBuilder builder, IConfiguration config)
        => builder
            .ClearProviders()
            .AddLog4Net(config.GetLog4NetConfiguration());

    public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder,
        IConfiguration config, Action<DataStoreLoggerConfiguration> configure)
    {
        builder
            .AddLog4Net(config)
            .Services.Configure(configure);

        return builder;
    }

    public static Log4NetProviderOptions? GetLog4NetConfiguration(
        this IConfiguration configuration)
        => configuration
            .GetSection("Log4NetCore")
            .Get<Log4NetProviderOptions>();
}
Public Module ServicesExtension

    <extension>
    Public Function AddLog4Net(builder As ILoggingBuilder,
        config As IConfiguration) As ILoggingBuilder

        builder _
            .ClearProviders() _
            .AddLog4Net(config.GetLog4NetConfiguration())

        Return builder

    End Function

    <extension>
    Public Function AddLog4Net(builder As ILoggingBuilder,
        config As IConfiguration, _
        configure As Action(Of DataStoreLoggerConfiguration))
        As ILoggingBuilder

        builder.AddLog4Net(config)
        builder.Services.Configure(configure)
        Return builder

    End Function

    <extension>
    Private Function GetLog4NetConfiguration(configuration As IConfiguration)
        As Log4NetProviderOptions

        Return configuration _
            .GetSection("Log4NetCore") _
            .Get(Of Log4NetProviderOptions)

    End Function

End Module

注册 Appenders (Loggers)

Log4Net 仅限于使用 XML 配置文件。默认名称是 log4net.config。可以更改此文件的名称。但是,为了本文的目的,我们将不重点介绍这一点。

<?xml version="1.0" encoding="utf-8" ?>
<log4net>
	<appender name="DebugAppender" type="log4net.Appender.DebugAppender" >
		<layout type="log4net.Layout.PatternLayout">
			<conversionPattern value="%date 
             [%thread] %-5level %logger - %message%newline" />
		</layout>
	</appender>
	<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
		<threshold value="ALL" />
		<layout type="log4net.Layout.PatternLayout">
			<conversionPattern value="%date 
            [%thread] %-5level %logger - %message%newline" />
		</layout>
	</appender>
	<appender name="DataStoreLogger" 
     type="Log4Net.Appender.LogView.Core.DataStoreLoggerAppender">
		<threshold value="ALL" />
	</appender>
	<root>
		<Level value="ALL" />
		<appender-ref ref="DebugAppender" />
		<appender-ref ref="ConsoleAppender" />
		<appender-ref ref="DataStoreLogger" />
	</root>
</log4net>

幸运的是,Microsoft.Extensions.Logging.Log4Net.AspNetCore 项目支持覆盖 *log4net.config* 文件中的值。这使我们能够使用 *appsettings*.json 文件为每个启动配置文件支持不同的配置。

这是我们的 *appsettings.Production.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"
        }
      }
    ]
  }
}

注释

  • *log4net.config* 中的默认日志级别适用于所有级别,但是,对于 Production/Release,*appsettings.Production.json* 文件将覆盖为 Warn,适用于 WarningErrorCritical 级别。

依赖注入

ServicesExtension 类和 log4net.config 配置文件连接了 Appenders 的注册,包括我们的自定义 appender,并配置 **Log4Het** 以与 **.NET Logging Framework** 一起工作。现在我们需要告诉 Host 使用 Log4Net Logging。

这是使用默认配置连接依赖注入的示例。

HostApplicationBuilder builder = Host.CreateApplicationBuilder();

builder.AddLogViewer();

builder.Logging.AddLog4Net(builder.Configuration);

_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.AddLogViewer()

builder.Logging.AddLog4Net(builder.Configuration)

_host = builder.Build()

或者,如果使用自定义配置。

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();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.AddLogViewer()

builder.Logging.AddLog4Net(
    builder.Configuration,
    Sub(options)

        options.Colors(LogLevel.Trace) = New LogEntryColor() With
        {
            .Foreground = Color.White,
            .Background = Color.DarkGray
        }

        options.Colors(LogLevel.Debug) = New LogEntryColor() With
        {
            .Foreground = Color.White,
            .Background = Color.Gray
        }

        options.Colors(LogLevel.Information) = New LogEntryColor() With
        {
            .Foreground = Color.White,
            .Background = Color.DodgerBlue
        }

        options.Colors(LogLevel.Warning) = New LogEntryColor() With
        {
            .Foreground = Color.White,
            .Background = Color.Orchid
        }

    End Sub)

_host = builder.Build()

要创建 Logger,您可以将实例注入到类构造函数中。

public class RandomLoggingService : BackgroundService
{
#region Constructors

    public RandomLoggingService(ILogger<RandomLoggingService> logger)
        => _logger = logger;

    #endregion

    #region Fields

    private readonly ILogger _logger;

    #endregion
}
Public Class RandomLoggingService : Inherits BackgroundService

#Region "Constructors"

  Public Sub New(logger As ILogger(Of RandomLoggingService))

    _logger = logger

  End Sub

#End Region

#Region "Fields"

  Private _logger As ILogger

#End Region

End Class

或者手动请求实例。

ILogger<class> logger
    = _host.Services.GetRequiredService<ILogger<class>>();
Dim logger As ILogger(Of class_name)
  = _host.Services.GetRequiredService(Of ILogger(Of class_name))

这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。

手动(不使用依赖注入)

如果不使用依赖注入,仍然可以注册一个或多个 Logger。

我们需要包装用于依赖注入的 ServicesExtension 来使用非 DI 版本的 LogDataStore 类。

public static class ServicesExtension
{
    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<DataStoreLoggerConfiguration> configure)
    {
        builder.AddLog4NetNoDI(config);
        builder.Services.Configure(configure);
        return builder;
    }
}
Public Module ServicesExtension

    <extension>
    Public Function AddLog4NetNoDI(builder As ILoggingBuilder,
        config As IConfiguration) As ILoggingBuilder

        ' 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

    End Function

    <extension>
    Public Function AddLog4NetNoDI(builder As ILoggingBuilder,
        config As IConfiguration, configure As Action(Of DataStoreLoggerConfiguration))
        As ILoggingBuilder

        builder.AddLog4NetNoDI(config)
        builder.Services.Configure(configure)
        Return builder

    End Function

End Module

我们还需要一个单例类来保存注册信息以及用于生成 Logger 实例的 Factory 方法。这是本文示例应用程序中使用的 LoggingHelper 类。

public static class LoggingHelper
{
    #region Constructors

    static LoggingHelper()
    {
        // retrieve the log level from 'appsettings'
        string value = AppSettings<string>
            .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
}
Public Module LoggingHelper

#Region "Constructors"

	Sub New()

		' retrieve the log level from 'appsettings'
		Dim value As String = AppSettings(Of String) _
            .Current("Logging:LogLevel", "Default")

		If String.IsNullOrWhiteSpace(value) Then
			value = "Information"
		End If

		Dim logLevel As LogLevel
		If Not [Enum].TryParse(value, logLevel) Then
			logLevel = LogLevel.Information
		End If

		Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
			.Initialize() _
			.Build()

		' wire up the loggers
		Factory = LoggerFactory.Create(
			Sub(builder)

				builder.AddLog4NetNoDI(configuration)

				' set minimum log level from 'appsettings'
				builder.SetMinimumLevel(logLevel)

			End Sub)

	End Sub

#End Region

#Region "Properties"

	Public ReadOnly Property Factory As ILoggerFactory

#End Region

End Module

或者,如果使用自定义配置。

public static class LoggingHelper
{
    #region Constructors

    static LoggingHelper()
    {
        // retrieve the log level from 'appsettings'
        string value = AppSettings<string>
            .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
}
Public Module LoggingHelper

#Region "Constructors"

	Sub New()

		' retrieve the log level from 'appsettings'
		Dim value As String = AppSettings(Of String) _
            .Current("Logging:LogLevel", "Default")

		If String.IsNullOrWhiteSpace(value) Then
			value = "Information"
		End If

		Dim logLevel As LogLevel
		If Not [Enum].TryParse(value, logLevel) Then
			logLevel = LogLevel.Information
		End If

		Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
			.Initialize() _
			.Build()

		' wire up the loggers
		Factory = LoggerFactory.Create(
			Sub(builder)

				builder.AddLog4NetNoDI(
					configuration,
					Sub(options)

						options.Colors(LogLevel.Trace) = New LogEntryColor() With
						{
							.Foreground = Color.White,
							.Background = Color.DarkGray
						}

						options.Colors(LogLevel.Debug) = New LogEntryColor() With
						{
							.Foreground = Color.White,
							.Background = Color.Gray
						}

						options.Colors(LogLevel.Information) = New LogEntryColor() With
						{
							.Foreground = Color.White,
							.Background = Color.DodgerBlue
						}

						options.Colors(LogLevel.Warning) = New LogEntryColor() With
						{
							.Foreground = Color.White,
							.Background = Color.Orchid
						}

					End Sub)

				' set minimum log level from 'appsettings'
				builder.SetMinimumLevel(logLevel)

			End Sub)

	End Sub

#End Region

#Region "Properties"

	Public ReadOnly Property Factory As ILoggerFactory

#End Region

End Module

要创建 Logger,请使用上面 LoggingHelper 类的 Factory 方法。

Logger<class> logger
    = new Logger<class>(LoggingHelper.Factory);
Dim logger As Logger(Of class_name)
  = New Logger(Of class_name)(LoggingHelper.Factory)

注意

  • 创建 Logger 时,类需要被实例化/创建。如果类未实例化,则会抛出错误。

将 Logger 作为构造函数参数创建是可以接受的。例如,以下是可接受的。

RandomLoggingService service
    = new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
Dim service As RandomLoggingService
  = New RandomLoggingService(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))

这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。

处理日志条目

我们有 LogDataStore 类,它存储来自所有库和应用程序的所有日志条目,基于从 *appsettings*.json 配置文件中检索到的最小 LogLevel

依赖注入

LogDataStore 类被注册为单例。它可以被注入到类中。

public class MyConsumer
{
    #region Constructors 

    public MyConsumer(ILogDataStore dataStore)
        => _dataStore = dataStore;

    #endregion

    #region Properties

    private ILogDataStore? _dataStore;

    #endregion
}
Public Class MyConsumer

#Region "Constructors"

 Public Sub New(dataStore As ILogDataStore)

  _dataStore = dataStore

 End Sub

#End Region

#Region "Fields"

 Private _dataStore As ILogDataStore

#End Region

End Class

或者手动请求实例。

public class MyConsumer
{
    #region Constructors

    public MyConsumer(IServiceProvider serviceProvider)
        => _dataStore = serviceProvider.GetRequiredService<LogDataStore>();

   #endregion

    #region Properties

    private ILogDataStore? _dataStore;

    #endregion
}
Public Class MyConsumer

#Region "Constructors"

 Public Sub New(serviceProvider As IServiceProvider)

  _dataStore = serviceProvider.GetRequiredService(Of LogDataStore)

 End Sub

#End Region

#Region "Fields"

 Private _dataStore As ILogDataStore

#End Region

End Class

我们需要为依赖注入注册 MyConsumer 类来连接所有内容。

HostApplicationBuilder builder = Host.CreateApplicationBuilder();

builder.Services.AddSingleton<LogDataStore>(); // from `ServicesExtension` class above
builder.Services.AddTransient<MyConsumer>();

_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.Services.AddSingleton(Of LogDataStore) ' from `ServicesExtension` class above
builder.Services.AddTransient(Of MyConsumer)

_host = builder.Build()

手动(不使用依赖注入)

Data Store 需要保存在一个单例类中,以便它可以被 Logger(生产者)和 Consumer 类共享。

这是将保存共享 Data Store 的 MainControlsDataStore 类。

public static class MainControlsDataStore
{
    public static ILogDataStore DataStore { get; } = new();
}
Public Module MainControlsDataStore

 Public Property DataStore As ILogDataStore = New LogDataStore()

End Module

我们可以将 Data Store 的实例传递给 Consumer 类以进行 IOC(控制反转),从而允许应用程序/库将来升级到依赖注入或不同的实现。

public class MyConsumer
{
    public MyConsumer(ILogDataStore dataStore)
        => _dataStore = dataStore;

    private LogDataStore? _dataStore;
}
Public Class MyConsumer

#Region "Constructors"

 Public Sub New(dataStore As ILogDataStore)

  _dataStore = dataStore

 End Sub

#End Region

#Region "Fields"

 Private _dataStore As ILogDataStore

#End Region

End Class

要使用 MyConsumer 类,我们会注入 DataStore

MyConsumer instance = new MyConsumer(MainControlsDataStore.DataStore);
Dim instance As MyConsumer = new MyConsumer(MainControlsDataStore.DataStore)

监听新条目

当我们实例化 MyConsumer 类并引用 LogDataStore 类时,我们需要手动监听 Entries 属性的 CollectionChanged 事件,或者让数据绑定完成所有工作。

手动处理 CollectionChanged 事件

public class MyConsumer
{
    public MyConsumer(LogDataStore dataStore)
    {
        _dataStore = dataStore;
        _dataStore.Entries.CollectionChanged += OnCollectionChanged;
    }

    private ILogDataStore? _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
        }
    }
}
Public Class MyConsumer

#Region "Constructors"

 Public Sub New(ByVal dataStore As LogDataStore)
    _dataStore = dataStore
    _dataStore.Entries.CollectionChanged += AddressOf OnCollectionChanged
  End Sub

#End Region

#Region "Fields"

  Private _dataStore As ILogDataStore

#End Region

#Region "Methods"

  Private Sub OnCollectionChanged(sender As Object,
                                  e As NotifyCollectionChangedEventArgs)

    If e.NewItems?.Count > 0 Then
           ' process new items
    End If

    If e.OldItems?.Count > 0 Then
           ' remove items
    End If

  End Sub

#End Region

End Class

LogViewerControl 实现

Logger 代码分为两部分:

  1. 公共代码 - LogViewer.Core 项目 = 共享代码
  2. 特定于应用程序类型的控件实现
    • WinForms 特有 - LogViewer.WinForms 项目 = **WinForm** 包装代码(针对 Common Code)
    • Wpf 特有 - LogViewer.Wpf 项目 = **Wpf** 包装代码(针对 Common Code)
    • Avalonia 特有 - LogViewer.Avalonia 项目 = **Avalonia** 包装代码(针对 Common Code)

这样做的原因是我们需要将数据传回 UI 线程。所有应用程序类型的执行此操作的方法略有不同。包含了一个 DispatcherHelper 类,用于 **Wpf** 和 **WinForms**。**Avalonia** 不需要相同的,它们有一个易于使用的实现。下面,您可以看到实现中的差异。

DispatcherHelper 类

Logger 框架利用独立于 UI 线程的线程来保持性能。消费日志条目并在 UI 上显示它们需要将数据传送到 UI 线程。这方面的抽象将由 DispatcherHelper 类处理。DispatcherHelper 类的 Execute 方法接受一个委托,并识别它是否在 UI 线程上,如果需要,它会在调用委托之前切换。

对 **Wpf** 和 **WinForms** 的使用非常简单。

DispatcherHelper.Execute(() => delegate_method());
'DispatcherHelper
Execute(Sub() delegate_method())

或者您可以内联 delegate_method()

DispatcherHelper.Execute(() =>
{
    // do work here
});
'DispatcherHelper
Execute(Sub()
   ' do work here
  End Sub)

在 **Avalonia** 中的使用非常相似。

await Dispatcher.UIThread.InvokeAsync(() => delegate_method());
Await Dispatcher.UIThread.InvokeAsync(Sub() delegate_method())

或者您可以内联 delegate_method()

await Dispatcher.UIThread.InvokeAsync(() =>
{
    // do work here
});
Await Dispatcher.UIThread.InvokeAsync(
  Sub()
    ' do work here
  End Sub)

WinForms 实现

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
        }
    }
}
Public Module DispatcherHelper

  Public Sub Execute(action As Action)

    ' no cross-tread concerns
    If Application.OpenForms.Count = 0 Then
      action.Invoke()
      Return
    End If

    Try

      If Application.OpenForms(0).InvokeRequired Then
        ' Marshall to Main Thread
        Application.OpenForms(0).Invoke(action)
      Else
        ' no cross-tread concerns
        action.Invoke()
      End If

    Catch ex As Exception

      ' ignore as might be thrown on shutting down

    End Try

  End Sub

End Module

WPF 实现

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);
    }
}
Public Module DispatcherHelper

  Public Sub Execute(action As Action)

    If Application.Current Is Nothing OrElse _
       Application.Current.Dispatcher Is Nothing Then
      Return
    End If

    ' Marshall to Main Thread
    Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, action)

  End Sub

End Module

公共代码 - LogViewer.Core 项目

以上各节已涵盖 DataStoreLoggerDataStoreLoggerProviderDataStoreLoggerConfigurationLogDataStoreLogModelLogEntryColor 类。对于 **WPF**,我们将在“WPF LogViwerControl 实现”部分介绍 LogViewerControlViewModel 类和 ILogDataStoreImpl 接口。

LoggerExtensions 类

包含两个方法:

  • Emit 方法 - Log 方法的高性能包装器。
  • TestPattern 方法 - 一个辅助方法,用于查看所有 LogLevel 类型的输出格式(仅用于调试目的)。
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);
    }
}
Public Module LoggerExtensions

  ' NOTE Optional And ParamArray are not allowed in same method call,
  ' so used overload instead

  <Extension>
  Sub Emit(logger As ILogger, eventId As EventId,
      logLevel As LogLevel, message As String, ParamArray args As Object())
    logger.Emit(eventId, logLevel, message, Nothing, args)
  End Sub

  <Extension>
  Sub Emit(logger As ILogger, eventId As EventId,
    logLevel As LogLevel, message As String, [exception] As Exception,
    ParamArray args As Object())

    If logger Is Nothing Then
      Return
    End If

    If Not logger.IsEnabled(logLevel) Then
      Return
    End If

    Select Case logLevel
      Case LogLevel.Trace
        logger.LogTrace(eventId, message, args)

      Case LogLevel.Debug
        logger.LogDebug(eventId, message, args)

      Case LogLevel.Information
        logger.LogInformation(eventId, message, args)

      Case LogLevel.Warning
        logger.LogWarning(eventId, message, args)

      Case LogLevel.[Error]
        logger.LogError(eventId, [exception], message, args)

      Case LogLevel.Critical
        logger.LogCritical(eventId, message, args)

    End Select

  End Sub

  <Extension>
  Sub TestPattern(logger As ILogger, Optional eventId As EventId = Nothing)

    Dim exception As 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)

  End Sub

End Module

ViewModel: LogViewerControlViewModel 类

对于 **WinForms**、**WPF** 和 **Avalonia** 的依赖注入实现,一个通用的 LogViewerControlViewModel 类用于引用单例 LogDataStore 实例,以便手动(**WinForms**)或通过数据绑定(**WPF**)在 LogViewControl 控件中进行监视。

public class LogViewerControlViewModel : ViewModel, ILogDataStoreImpl
{
    #region Constructor

    public LogViewerControlViewModel(ILogDataStore dataStore)
    {
        DataStore = dataStore;
    }

    #endregion

    #region Properties

    public ILogDataStore DataStore { get; set; }

    #endregion
}
Public Class LogViewerControlViewModel
   Inherits ViewModel
   Implements ILogDataStoreImpl

#Region "Constructors"

  Public Sub New(store As ILogDataStore)
    DataStore = store
  End Sub

#End Region

#Region "Properties"

  Public ReadOnly Property DataStore As ILogDataStore _
       Implements ILogDataStoreImpl.DataStore

#End Region

End Class

WinForms - LogViewerControl

现在我们可以创建控件本身了。对于 **WinForms**,将查看代码后置。如果您想查看 UserControl 的设计,请下载并检查设计器代码。

代码后置

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<LogModel>());

            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<LogModel> 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
}
Public Class LogViewerControl

#Region "Constructors"

  ' supports DI and non-DI usage

  Public Sub New()

    ' This call is required by the designer.
    InitializeComponent()

    ' Stop the flickering!
    ListView.SetDoubleBuffered()

    AddHandler Disposed, AddressOf OnDispose

  End Sub

  Public Sub New(viewModel As LogViewerControlViewModel)

    Me.New()

    RegisterLogDataStore(viewModel.DataStore)

  End Sub

#End Region

#Region "Fields"

  Private _dataStore As ILogDataStore

  Private Shared ReadOnly _semaphore As SemaphoreSlim =
    New SemaphoreSlim(initialCount:=1)

#End Region

#Region "Methods"

  Public Sub RegisterLogDataStore(datastore As ILogDataStore)

    _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
    AddHandler _dataStore.Entries.CollectionChanged, AddressOf OnCollectionChanged

  End Sub

  Private Sub OnCollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)

    ' any new items?
    If e.NewItems IsNot Nothing AndAlso e.NewItems.Count > 0 Then
      AddListViewItems(e.NewItems.Cast(Of LogModel))

      ExclusiveDispatcher(
        Sub()

          ' auto-scroll if required
          If CanAutoScroll.Checked Then
            ListView.Items(ListView.Items.Count - 1).EnsureVisible()
          End If

        End Sub)
    End If

    ' any to remove? ... not required for this purpose.
    If e.OldItems IsNot Nothing AndAlso e.OldItems.Count > 0 Then
      ' remove from ListView.Items
    End If

  End Sub

  Private Sub AddListViewItems(logEntries As IEnumerable(Of LogModel))


    ExclusiveDispatcher(
      Sub()

        For Each item As LogModel In logEntries
          Dim lvi As ListViewItem = New ListViewItem With
            {
              .Font = New Font(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(If(item.State Is Nothing, String.Empty, item.State.ToString()))
          lvi.SubItems.Add(If(item.Exception Is Nothing, String.Empty,
                           item.Exception.ToString()))

          ListView.Items.Add(lvi)
        Next

      End Sub)

  End Sub

  Private Sub ExclusiveDispatcher(action As Action)

    ' ensure only one operation at time from multiple threads
    _semaphore.Wait()

    ' delegate to UI thread
    'DispatcherHelper.
    Execute(Sub() action.Invoke())

    _semaphore.Release()

  End Sub

  ' cleanup time ...
  Private Sub OnDispose(sender As Object, e As EventArgs)

    RemoveHandler Disposed, AddressOf OnDispose

    If _dataStore Is Nothing Then
      Return
    End If

    RemoveHandler _dataStore.Entries.CollectionChanged, AddressOf OnCollectionChanged

  End Sub

#End Region

End Class

LogViewerControl 包含两个控件:

  • ListView 控件 - 日志条目的主要显示。
  • CheckBox 控件 - 切换 ListView 控件的自动滚动。

代码仅引用 LogDataStore 实例,并监听 Entries 集合的变化。当添加项时,将创建一个 ListViewItem,对其进行格式化,然后添加到 ListView 控件中。

它还监听 LogViewerControl 被处置时的情况,并取消引用所有事件以避免内存泄漏。

这是一个 **默认** 着色实际运行的 GIF。

WPF - LogViewerControl

我们将使用数据绑定来管理新日志条目添加时的事件处理。

后台代码

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);
    }
}
Public Class LogViewerControl

  Public Sub New()

    ' This call is required by the designer.
    InitializeComponent()

  End Sub

  Private Sub OnLayoutUpdated(sender As Object, e As EventArgs)

    If Not CanAutoScroll.IsChecked Then
      Return
    End If

    ' design time
    If DataContext Is Nothing Then
      Return
    End If

    Dim store As ILogDataStoreImpl = DirectCast(DataContext, ILogDataStoreImpl)

    ' Okay, we can now get the item and scroll into view
    Dim item As LogModel = store.DataStore.Entries.LastOrDefault()

    If item Is Nothing Then
      Return
    End If

    ListView.ScrollIntoView(item)

  End Sub

End Class

我们需要支持:

  • 带 MVVM 的依赖注入。
  • 无依赖注入和 MVVM。
  • 无依赖注入,并在代码后置中使用手动数据绑定。

对于 MVVM,LogDataStore 将位于 ModelViewModel 中。最后一个选项可能将 LogDataStore 作为 WindowUserControl 的属性暴露。控件需要访问 LogDataStore 以用于这两种情况。LogViewControl 需要一个指向该属性的通用接口。

public interface ILogDataStoreImpl
{
    public LogDataStore DataStore { get; }
}
Public Interface ILogDataStoreImpl

  ReadOnly Property DataStore As ILogDataStore

End Interface

用户界面

XAML 侧重于 ListView 控件中的数据绑定。

<ListView x:Name="ListView"
          ItemsSource="{Binding DataStore.Entries}"
          LayoutUpdated="OnLayoutUpdated">
    <ListView.Resources>
        <!-- trimmed -->
        <Style TargetType="{x:Type ListViewItem}">
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            <Setter Property="BorderBrush" Value="Silver"/>
            <Setter Property="BorderThickness" Value="0,0,0,1"/>
            <Setter Property="Foreground" Value="{Binding Color.Foreground,
                Converter={StaticResource ColorConverter}}" />
            <Setter Property="Background" Value="{Binding Color.Background,
                Converter={StaticResource ColorConverter}}" />
            <!-- trimmed -->
         </Style>
    </ListView.Resources>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Time" Width="140">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Timestamp}"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="Level" Width="80">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding LogLevel}"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="Event Id" Width="120">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding EventId}"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="State" Width="300">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding State}"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="Exception" Width="300">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Exception}"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

注意:下载解决方案以查看 UI 的完整实现。

由于 DataStoreLogger 被 **WinForms** 和 **WPF** 项目类型使用,因此 System.Drawing.Color 类在 DataStoreLoggerConfiguration 类中使用。因此,对于 **WPF**,我们需要将 Color 类型类 System.Windows.Media.Color 转换为并返回 SolidColorBrush

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();
}
Public Class ChangeColorTypeConverter : Implements IValueConverter

  Public Function Convert(value As Object, targetType As Type,
                          parameter As Object, culture As CultureInfo)
                          As Object Implements IValueConverter.Convert

    Dim sysDrawColor As SysDrawColor = DirectCast(value, SysDrawColor)
    Return New SolidColorBrush(Color.FromArgb(
      sysDrawColor.A,
      sysDrawColor.R,
      sysDrawColor.G,
      sysDrawColor.B))

  End Function

  Public Function ConvertBack(value As Object, targetType As Type,
                              parameter As Object, culture As CultureInfo)
                              As Object Implements IValueConverter.ConvertBack
    Throw New NotImplementedException
  End Function

End Class

这是一个 **自定义** 着色实际运行的 GIF。

Avalonia - LogViewerControl

我们将使用数据绑定来管理新日志条目添加时的事件处理。

对于 **Avalonia**,实现有所不同,因为控件不相同。这里,我们使用 DataGrid,而 WPF 使用 ListView。对于自动滚动,与 **WPF** 相比存在细微差别。下面,您可以看到随着项目的添加,我们如何处理滚动到视图,这与 **WPF** 不同。

后台代码

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<LogModel>().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;
    }
}
Partial Public Class LogViewerControl : Inherits UserControl

  Private _vm As ILogDataStoreImpl
  Private _model As LogModel

  Private MyDataGrid As DataGrid
  Private CanAutoScroll As CheckBox

  Sub New()
    InitializeComponent()
  End Sub

  ' Auto-wiring does not work for VB, so do it manually
  ' Wires up the controls and optionally loads XAML markup and attaches
  ' dev tools (if Avalonia.Diagnostics package is referenced)
  Private Sub InitializeComponent(Optional loadXaml As Boolean = True)

    If loadXaml Then
      AvaloniaXamlLoader.Load(Me)
    End If

    MyDataGrid = FindNameScope().Find("MyDataGrid")
    CanAutoScroll = FindNameScope().Find("CanAutoScroll")

  End Sub

  Private Shadows Sub OnDataContextChanged(sender As Object, e As EventArgs)

    If DataContext Is Nothing Then
      Return
    End If

    _vm = DirectCast(DataContext, ILogDataStoreImpl)
    AddHandler _vm.DataStore.Entries.CollectionChanged, 
               AddressOf OnCollectionChanged


  End Sub

  Private Sub OnCollectionChanged(sender As Object,
                                  e As NotifyCollectionChangedEventArgs)

    _model = MyDataGrid.Items.Cast(Of LogModel).LastOrDefault()

  End Sub

  Private Sub OnLayoutUpdated(sender As Object, e As EventArgs)

    If CanAutoScroll.IsChecked <> True OrElse _model Is Nothing Then
      Return
    End If

    MyDataGrid.ScrollIntoView(_model, Nothing)
    _model = Nothing

  End Sub

  Private Shadows Sub OnDetachedFromLogicalTree(sender As Object,
                                                e As LogicalTreeAttachmentEventArgs)

    If _vm Is Nothing Then
      Return
    End If

    RemoveHandler _vm.DataStore.Entries.CollectionChanged,
      AddressOf OnCollectionChanged

  End Sub

End Class

我们需要支持:

  • 带 MVVM 的依赖注入。
  • 无依赖注入和 MVVM。
  • 无依赖注入,并在代码后置中使用手动数据绑定。

对于 MVVM,LogDataStore 将位于 ModelViewModel 中。最后一个选项可能将 LogDataStore 作为 Window 或 UserControl 的属性暴露。控件需要访问 LogDataStore 以用于这两种情况。LogViewControl 需要一个指向该属性的通用接口。

public interface ILogDataStoreImpl
{
    public LogDataStore DataStore { get; }
}
Public Interface ILogDataStoreImpl

  ReadOnly Property DataStore As ILogDataStore

End Interface

用户界面

XAML 侧重于 ListView 控件中的数据绑定。

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition Height="Auto" />
  </Grid.RowDefinitions>
  <Grid.Resources>
    <converters:ChangeColorTypeConverter x:Key="ColorConverter" />
    <converters:EventIdConverter x:Key="EventIdConverter"/>
    <SolidColorBrush x:Key="ColorBlack">Black</SolidColorBrush>
    <SolidColorBrush x:Key="ColorTransparent">Transparent</SolidColorBrush>
  </Grid.Resources>
  <Grid.Styles>
    <Style Selector="DataGridRow">
      <Setter Property="Padding" Value="0" />
      <Setter Property="Foreground" Value="{Binding Color.Foreground,
                    Converter={StaticResource ColorConverter},
                    ConverterParameter={StaticResource ColorBlack}}" />
      <Setter Property="Background" Value="{Binding Color.Background,
                    Converter={StaticResource ColorConverter},
                    ConverterParameter={StaticResource ColorTransparent}}" />
    </Style>
    <Style Selector="DataGridCell.size">
      <Setter Property="FontSize" Value="11" />
      <Setter Property="Padding" Value="0" />
    </Style>
  </Grid.Styles>
  <DataGrid x:Name="MyDataGrid"
            Items="{Binding DataStore.Entries}" AutoGenerateColumns="False"
            CanUserSortColumns="False"
            LayoutUpdated="OnLayoutUpdated">
    <DataGrid.Columns>
      <DataGridTextColumn CellStyleClasses="size"
                          Header="Time" Width="150"
                          Binding="{Binding Timestamp}"/>
      <DataGridTextColumn CellStyleClasses="size"
                          Header="Level" Width="90"
                          Binding="{Binding LogLevel}" />
      <DataGridTextColumn CellStyleClasses="size"
                          Header="Event Id" Width="120"
                          Binding="{Binding EventId,
                            Converter={StaticResource EventIdConverter}}" />
      <DataGridTextColumn CellStyleClasses="size"
                          Header="State" Width="300"
                          Binding="{Binding State}" />
      <DataGridTextColumn CellStyleClasses="size"
                          Header="Exception" Width="300"
                          Binding="{Binding Exception}" />
    </DataGrid.Columns>
  </DataGrid>
</Grid>

注意:下载解决方案以查看 UI 的完整实现。

由于 DataStoreLogger 被 **WinForms** 和 **WPF** 项目类型使用,因此 System.Drawing.Color 类在 DataStoreLoggerConfiguration 类中使用。因此,对于 **WPF**,我们需要将 Color 类型类 System.Windows.Media.Color 转换为并返回 SolidColorBrush

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();
}
Public Class ChangeColorTypeConverter : Implements IValueConverter

  Public Function Convert(value As Object, targetType As Type,
                          parameter As Object, culture As CultureInfo)
                          As Object Implements IValueConverter.Convert

    Dim sysDrawColor As SysDrawColor = DirectCast(value, SysDrawColor)
    Return New SolidColorBrush(Color.FromArgb(
      sysDrawColor.A,
      sysDrawColor.R,
      sysDrawColor.G,
      sysDrawColor.B))

  End Function

  Public Function ConvertBack(value As Object, targetType As Type,
                              parameter As Object, culture As CultureInfo)
                              As Object Implements IValueConverter.ConvertBack
    Throw New NotImplementedException
  End Function

End Class

与 **WPF** 不同,我们需要从 EventId 类中提取字符串值,因为 **Avalonia** 数据绑定到 DataGrid 控件不使用该类的 ToString() 方法。

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);
}
Public Class EventIdConverter : Implements IValueConverter

  Public Function Convert(value As Object, targetType As Type,
                          parameter As Object, culture As CultureInfo)
                          As Object Implements IValueConverter.Convert

    If value Is Nothing Then
      Return "0"
    End If

    Dim eventId As EventId = DirectCast(value, EventId)

    Return eventId.ToString()

  End Function

  Public Function ConvertBack(value As Object, targetType As Type,
                  parameter As Object, culture As CultureInfo)
                  As Object Implements IValueConverter.ConvertBack
    Return New EventId(0, If(value Is Nothing, String.Empty, value.ToString()))
  End Function

End Class

这是一个 **自定义** 着色实际运行的 GIF。

使用 LogViewControl

我们已经创建了自定义 Logger,我们有了一个通用的 Data Store 来共享所有日志条目,并创建了一个LogViewerControl,现在我们可以使用了。

WinForms - 依赖注入

注册 - ServicesExtension 类

LogViewerControlLogViewerControlViewModel 的注册被抽象到 ServicesExtension 类中的一个扩展方法。

public static class ServicesExtension
{
    public static HostApplicationBuilder AddLogViewer(this HostApplicationBuilder builder)
    {
        builder.Services.AddSingleton<ILogDataStore, Logging.LogDataStore>();
        builder.Services.AddSingleton<LogViewerControlViewModel>();
        builder.Services.AddTransient<LogViewerControl>();

        return builder;
    }
}
Public Module ServicesExtension

  <Extension>
  Public Function AddLogViewer(builder As HostApplicationBuilder) _
         As HostApplicationBuilder

    builder.Services.AddSingleton(Of ILogDataStore, Logging.LogDataStore)
    builder.Services.AddSingleton(Of LogViewerControlViewModel)
    builder.Services.AddTransient(Of LogViewerControl)

    Return builder

  End Function

End Module

注释

  • LogViewerControlViewModel 类被注册为单例,用于 DataStoreLogger 所需的共享 LogDataStore 实例,以便与 LogViewerControl 共享日志条目。
  • 每次实例化 LogViewerControl 时,将在宿主 LogViewerControl 控件中手动连接共享的 LogViewerControlViewModel 实例。

MainForm 代码后置

MainForm Designer 有一个名为 HostPanelPanel 控件,用于托管 LogViewerControl。下面,我们可以看到 LogViewerControl 被注入到 MainForm 中,并被添加到 HostPanel 中。

public partial class MainForm : Form
{
    #region Constructors

    public MainForm(MainControlsDataStore controlsDataStore)
    {
        InitializeComponent();

        // wire up the control
        HostPanel.AddControl(controlsDataStore.LogViewer);
    }

    #endregion
}
Public Class MainForm

#Region "Constructors"

  Sub New(controlsDataStore As MainControlsDataStore)

    ' This call is required by the designer.
    InitializeComponent()

    ' wire up the control
    HostPanel.AddControl(controlsDataStore.LogViewer)

  End Sub

#End Region

End Class

AddControl 是一个封装了执行此任务代码的扩展方法。

public static class ControlsExtension
{
    public static void AddControl(this Panel panel, Control control)
    {
        panel.Controls.Add(control);
        control.Dock = DockStyle.Fill;
        control.BringToFront();
    }
}
Public Module ControlsExtension

  <Extension>
  Public Sub AddControl(panel As Panel, control As Control)

    panel.Controls.Add(control)
    control.Dock = DockStyle.Fill
    control.BringToFront()

  End Sub

End Module

注册 - Bootstrapper 类 (C#)

我们不能在 static 类中使用依赖注入,在这种情况下,**WinForms** 应用程序中的 Program 类。所以我们添加一个 Bootstrapper 类并指向一个实例。

internal static class Program
{
    #region Bootstrap

    [STAThread]
    static void Main() => _ = new Bootstrapper();

    #endregion
}

然后,在 Bootstrapper 类中,我们可以连接依赖项。

HostApplicationBuilder builder = Host.CreateApplicationBuilder();

builder.Logging.AddDefaultDataStoreLogger();

builder.Services
        .AddSingleton<MainControlsDataStore>()
        .AddSingleton<MainForm>();

_host = builder.Build();

用法

注册后,我们可以从 Bootstrapper 类中显示 MainForm

// set and show
Application.Run(_host.Services.GetRequiredService<MainForm>());

注册 - ApplicationEvents 类

**VB.NET** 以不同于 **C#** 的方式连接 WinForms 应用程序。

Partial Friend Class MyApplication

 Protected Overrides Function OnStartup( _
   eventArgs As ApplicationServices.StartupEventArgs) As Boolean

  InitializeDI()

  Return MyBase.OnStartup(eventArgs)

 End Function

 Private Sub InitializeDI()

  Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

  builder.Logging.AddDefaultDataStoreLogger()

  Dim services As IServiceCollection = builder.Services

  services _
   .AddSingleton(Of MainControlsDataStore) _
   .AddSingleton(Of MainForm)

  _host = builder.Build()

 End Sub

End Class

WinForms - 手动(不使用依赖注入)

对于手动(无依赖注入),我们将 LogViewControl 控件直接添加到 Form 中,然后手动将 LogDataStore 实例注册到 LogViewControl 控件。

MainForm 代码后置

public partial class MainForm : Form
{
    public Form1()
    {
        InitializeComponent();

        // Initialize service and pass in the Logger
        RandomLoggingService service =
            new(new Logger<RandomLoggingService>(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);
    }
}
Public Class MyMainForm

  Sub New()

    ' This call is required by the designer.
    InitializeComponent()

    ' Initialize service and pass in the Logger
    Dim service As RandomLoggingService =
      New RandomLoggingService
      (New Logger(Of RandomLoggingService)(LoggingHelper.Factory))

    Dim task As Task = service.StartAsync(CancellationToken.None)

    ' manually wire up the logging to the view ...
    '   the control will show backlog entries...
    LogViewerControl.RegisterLogDataStore(MainControlsDataStore.DataStore)

  End Sub

End Class

WPF - 依赖注入

与 **WinForms** 实现有很大的重叠。

注册 - ServicesExtension 类

LogViewerControlLogViewerControlViewModel 的注册被抽象到 ServicesExtension 类中的一个扩展方法。DataContext 的设置也在实例化时通过依赖注入完成。

public static class ServicesExtension
{
  public static HostApplicationBuilder AddLogViewer(this HostApplicationBuilder builder)
  {
    builder.Services.AddSingleton<ILogDataStore, Logging.LogDataStore>();
    builder.Services.AddSingleton<LogViewerControlViewModel>();
    builder.Services.AddTransient(service => new LogViewerControl
    {
      DataContext = service.GetRequiredService<LogViewerControlViewModel>()
    });

    return builder;
  }
}
Public Module ServicesExtension

  <Extension>
  Public Function AddLogViewer(builder As HostApplicationBuilder, _
  Optional config As Action(Of DataStoreLoggerConfiguration) = Nothing) _
  As HostApplicationBuilder

    builder.Services.AddSingleton(Of ILogDataStore, Logging.LogDataStore)
    builder.Services.AddSingleton(Of LogViewerControlViewModel)
    builder.Services.AddTransient(
      Function(service) New LogViewerControl() With
      {
        .DataContext = service.GetRequiredService(Of LogViewerControlViewModel)
      })

    Return builder

  End Function

End Module

注释

  • LogViewerControlViewModel 类被注册为单例,用于 DataStoreLogger 所需的共享 LogDataStore 实例,以便与 LogViewerControl 共享日志条目。
  • 每次实例化 LogViewerControl 时,DataContext 将自动设置为共享的 LogViewerControlViewModel 实例。

MainWindow - LogViewerControl Host

托管 UserControl 有很多不同的方法。我使用的方法是 ContentControl

<Window x:Class="WpfLoggingDI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        mc:Ignorable="d"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:control="clr-namespace:LogViewer.Wpf;assembly=LogViewer.Wpf"
        xmlns:viewModels="clr-namespace:LogViewer.Core.ViewModels;assembly=LogViewer.Core"

        Title="C# WPF MVVM | LogViewer Control Example - Dot Net 7.0"
        WindowStartupLocation="CenterScreen" Height="634" Width="600">

    <Window.Resources>
        <DataTemplate DataType="{x:Type viewModels:LogViewerControlViewModel}">
            <control:LogViewerControl />
        </DataTemplate>
    </Window.Resources>

    <ContentControl Grid.Row="1" Content="{Binding LogViewer}" />

</Window>

我们注册 MainWindow 以便依赖注入通过 MainViewModel 类注入 LogViewerControlMainViewModel 将暴露 LogViewerControlViewModel,使用模板的数据绑定将初始化 LogViewControl

MainViewModel 类

public class MainViewModel : ViewModel
{
    #region Constructor

    public MainViewModel(LogViewerControlViewModel logViewer)
    {
        LogViewer = logViewer;
    }

    #endregion

    #region Properties

    public LogViewerControlViewModel LogViewer { get; }

    #endregion
}
Public Class MainViewModel : Inherits Viewmodel

#Region "Constructor"

  Public Sub New(logViewer As LogViewerControlViewModel)
    Me.LogViewer = logViewer
  End Sub

#End Region

#Region "Properties"

  Public Property LogViewer As LogViewerControlViewModel

#End Region

End Class

注册 - App (C#) / Application (VB) 类

HostApplicationBuilder builder = Host.CreateApplicationBuilder();

builder.Logging.AddDataStoreLogger();

builder.Services.
    .AddSingleton<MainViewModel>()
    .AddSingleton<MainWindow>(service => new MainWindow
    {
        DataContext = service.GetRequiredService<MainViewModel>()
    });

_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.Logging.AddDefaultDataStoreLogger()

Dim services As IServiceCollection = builder.Services

services _
  .AddSingleton(Of MainViewModel) _
  .AddSingleton(Of MainWindow)(
    Function(service) New MainWindow() With
    {
      .DataContext = service.GetRequiredService(Of MainViewModel)
    })

_host = builder.Build()

用法

注册后,我们可以从 App 类中显示 MainWindow

MainWindow = _host.Services.GetRequiredService<MainWindow>();
MainWindow.Show();
MainWindow = _host.Services.GetRequiredService(Of MainWindow)()
MainWindow.Show()

WPF - 手动(不使用依赖注入)

对于手动(无依赖注入),我们将 LogViewControl 控件直接添加到 Window 中,将 LogDataStore 实例的引用手动存储为 Window 上的 Property,然后将 LogViewControl 控件的 DataContext 设置为 Window,并让数据绑定连接 LogViewControl 控件。

MainWindow XAML - LogViewerControl Host

<Window x:Class="WpfLoggingNoDI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        mc:Ignorable="d"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:control="clr-namespace:LogViewer.Wpf;assembly=LogViewer.Wpf"

        Title="C# WINFORMS MINIMAL | LogViewer Control Example - Dot Net 7.0"
        WindowStartupLocation="CenterScreen" Height="634" Width="600">

    <control:LogViewerControl x:Name="LogViewerControl" />

</Window>

MainWindow 代码后置

public partial class MainWindow : ILogDataStoreImpl
{
    public MainWindow()
    {
        InitializeComponent();

        // Initialize service and pass in the Logger
        RandomLoggingService service =
          new(new Logger<RandomLoggingService>(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 ILogDataStore DataStore { get; init;  }
}
Class MainWindow : Implements ILogDataStoreImpl

  Sub New()

    ' This call is required by the designer.
    InitializeComponent()

    ' Initialize service and pass in the Logger
    Dim service As RandomLoggingService =
      New RandomLoggingService(New Logger_
          (Of RandomLoggingService)(LoggingHelper.Factory))

    ' Start generating log entries
    Dim task As Task = 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 = Me

  End Sub

  Public Property DataStore As ILogDataStore Implements ILogDataStoreImpl.DataStore

End Class

Avalonia - 依赖注入

与 **WPF** 实现有很大的重叠,因此如果您熟悉 **WPF**,那么这应该会让您感觉很熟悉。

注册 - ServicesExtension 类

LogViewerControlViewModel 的注册被抽象到 ServicesExtension 类中的一个扩展方法。DataContext 的设置也在实例化时通过依赖注入完成。

public static class ServicesExtension
{
    public static HostApplicationBuilder AddLogViewer
                  (this HostApplicationBuilder builder)
    {
        builder.Services.AddSingleton<ILogDataStore, LogDataStore>();
        builder.Services.AddSingleton<LogViewerControlViewModel>();

        return builder;
    }
}
Public Module ServicesExtension

  <Extension>
  Public Function AddLogViewer(builder As HostApplicationBuilder,
                               Optional config As Action(Of DataStoreLoggerConfiguration)
                               = Nothing) As HostApplicationBuilder

    builder.Services.AddSingleton(Of ILogDataStore, Logging.LogDataStore)
    builder.Services.AddSingleton(Of LogViewerControlViewModel)

    Return builder

  End Function

End Module

注释

  • LogViewerControlViewModel 类被注册为单例,用于 DataStoreLogger 所需的共享 LogDataStore 实例,以便与 LogViewerControl 共享日志条目。
  • 每次实例化 LogViewerControl 时,DataContext 将手动设置为共享的 LogViewerControlViewModel 实例。

MainWindow - LogViewerControl Host

托管 UserControl 有很多不同的方法。我使用的方法是 ContentControl

<Window x:Class="AvaloniaLoggingDI.Views.MainWindow"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        x:Name="Window"

        xmlns:control="clr-namespace:LogViewer.Avalonia;assembly=LogViewer.Avalonia"

        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"

        Title="C# AVALONIA | LogViewer Control Example - Dot Net 7.0"
        Icon="/Assets/avalonia-logo.ico"
        WindowStartupLocation="CenterScreen" Height="634" Width="600">

  <control:LogViewerControl DataContext="{Binding LogViewer}" />

</Window>

MainViewModel 类

public class MainViewModel : ViewModelBase
{
    #region Constructor

    public MainViewModel(LogViewerControlViewModel logViewer)
    {
        LogViewer = logViewer;
    }

    #endregion

    #region Properties

    public LogViewerControlViewModel LogViewer { get; }

    #endregion
}
Public Class MainViewModel : Inherits ViewModelBase

#Region "Constructor"

  Public Sub New(logViewer As LogViewerControlViewModel)
    Me.LogViewer = logViewer
  End Sub

#End Region

#Region "Properties"

  Public Property LogViewer As LogViewerControlViewModel

#End Region

End Class

注册 - App 类

HostApplicationBuilder builder = Host.CreateApplicationBuilder();

builder.Logging.AddDefaultDataStoreLogger();

builder.Services.
    .AddSingleton<MainViewModel>()
 .AddSingleton<MainViewModel>()
    .AddSingleton<MainWindow>(service => new MainWindow
    {
        DataContext = service.GetRequiredService<MainViewModel>()
    });

_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

builder.Logging.AddDefaultDataStoreLogger()

Dim services As IServiceCollection = builder.Services

services _
.AddSingleton(Of MainViewModel) _
.AddSingleton(Of MainWindow)(
  Function(service) New MainWindow() With
  {
    .DataContext = service.GetService(Of MainViewModel)
  })

_host = builder.Build()

用法

注册后,我们可以从 App 类中显示 MainWindow

desktop.MainWindow = _host.Services.GetRequiredService<MainWindow>();
desktop.MainWindow = _host.Services.GetRequiredService(Of MainWindow)

Avalonia - 手动(不使用依赖注入)

对于手动(无依赖注入),我们将 LogViewControl 控件直接添加到 Window 中,将 LogDataStore 实例的引用手动存储为 Window 上的 Property,然后将 LogViewControl 控件的 DataContext 设置为 Window,并让数据绑定连接 LogViewControl 控件。

MainWindow XAML - LogViewerControl Host

<Window x:Class="AvaloniaLoggingNoDI.Views.MainWindow"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        x:Name="Window"

        xmlns:control="clr-namespace:LogViewer.Avalonia;assembly=LogViewer.Avalonia"

        mc:Ignorable="d"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        
        Title="C# AVALONIA MINIMAL | LogViewer Control Example - Dot Net 7.0"
        Icon="/Assets/avalonia-logo.ico"
        WindowStartupLocation="CenterScreen" Height="634" Width="600">

    <control:LogViewerControl x:Name="LogViewerControl" />

</Window>

MainWindow 代码后置

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<RandomLoggingService>(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;  }
}
Partial Public Class MainWindow : Inherits Window : Implements ILogDataStoreImpl

  Private LogViewerControl As LogViewerControl

  Sub New()

    ' This call is required by the designer.
    InitializeComponent()

    ' Initialize service and pass in the Logger
    Dim service As RandomLoggingService =
      New RandomLoggingService_
      (New Logger(Of RandomLoggingService)(LoggingHelper.Factory))

    ' Start generating log entries
    Dim task As Task = 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 = Me

  End Sub

  ' Auto-wiring does not work for VB, so do it manually
  ' Wires up the controls and optionally loads XAML markup and attaches
  '   dev tools (if Avalonia.Diagnostics package is referenced)
  Private Sub InitializeComponent(Optional loadXaml As Boolean = True)

    If loadXaml Then
      AvaloniaXamlLoader.Load(Me)
    End If

    LogViewerControl = FindNameScope().Find("LogViewerControl")

  End Sub

  Public Property DataStore As ILogDataStore Implements ILogDataStoreImpl.DataStore

End Class

生成示例日志消息

最后要做的是生成 Log 消息以模拟实时应用程序。为此,我将使用一个 BackgroundServiceBackgroundService 服务类用于为 **ASP.NET** 后台任务或 Windows 服务创建长时间运行的任务。我们也可以在桌面应用程序中使用它,但是,与 **ASP.NET** 不同,它需要手动激活和关闭。

我们将利用 **.NET Framework** HostedServicesHostedServices 可以管理我们应用程序中的一个或多个后台任务。

后台服务 - RandomLoggingService 类

public class RandomLoggingService : BackgroundService
{
    #region Constructors

    public RandomLoggingService(ILogger<RandomLoggingService> logger)
        => _logger = logger;

    #endregion

    #region Fields

    #region Injected

    private readonly ILogger _logger;

    #endregion

    // ChatGPT generated lists

    private readonly List<string> _messages = new()
    {
        "Bringing your virtual world to life!",
        // trimmed
    };

    readonly List<string> _eventNames = new()
    {
        "OnButtonClicked",
        // trimmed
    };

    readonly List<string> _errorMessages = new()
    {
        "Error: Could not connect to the server. Please check your internet connection.",
        // trimmed
    };

    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
}
Public Class RandomLoggingService : Inherits BackgroundService

#Region "Constructors"

  Public Sub New(logger As ILogger(Of RandomLoggingService))

    _logger = logger

  End Sub

#End Region

#Region "Fields"

#Region "Injects"

  Private _logger As ILogger

#End Region

  ' ChatGPT generated lists

  Private ReadOnly _messages As List(Of String) = New List(Of String) From {
    "Bringing your virtual world to life!",
    ' trimmed
  }

  Private ReadOnly _eventNames As List(Of String) = New List(Of String)() From {
      "OnButtonClicked",
      ' trimmed
  }

  Private ReadOnly _errorMessages As List(Of String) = New List(Of String)() From {
      "Error: Could not connect to the server. Please check your internet connection.",
      ' trimmed
  }

  Private ReadOnly _random As Random = New Random()
  Private Shared ReadOnly EventId As EventId
    = New EventId(id:=&H1A4, name:="RandomLoggingService")

#End Region

#Region "BackgroundService"

  Protected Overrides Async Function ExecuteAsync(
    stoppingToken As CancellationToken) As Task

    _logger.Emit(EventId, LogLevel.Information, "Started")

    While Not stoppingToken.IsCancellationRequested

      ' wait for a pre-determined interval
      Await Task.Delay(1000, stoppingToken).ConfigureAwait(False)

      ' heartbeat logging
      GenerateLogEntry()

    End While

    _logger.Emit(EventId, LogLevel.Information, "Stopped")

  End Function

  Public Overrides Async Function StartAsync(
    cancellationToken As CancellationToken) As Task

    Await Task.Yield()

    _logger.Emit(EventId, LogLevel.Information, "Starting")
    Await MyBase.StartAsync(cancellationToken).ConfigureAwait(False)

  End Function

  Public Overrides Async Function StopAsync(
    cancellationToken As CancellationToken) As Task

    _logger.Emit(EventId, LogLevel.Information, "Stopping")
    Await MyBase.StopAsync(cancellationToken).ConfigureAwait(False)

  End Function

#End Region

#Region "Methods"

  Private Sub GenerateLogEntry()

    Dim level As LogLevel

    Select Case _random.Next(0, 100)
      Case < 50 : level = LogLevel.Information
      Case < 65 : level = LogLevel.Debug
      Case < 75 : level = LogLevel.Trace
      Case < 85 : level = LogLevel.Warning
      Case < 95 : level = LogLevel.Error
      Case Else : level = LogLevel.Critical
    End Select

    If level < LogLevel.Error Then
      _logger.Emit(GenerateEventId(), level, GetMessage())
      Return
    End If

    _logger.Emit(GenerateEventId(), level, GetMessage(),
          New Exception(_errorMessages(_random.Next(0, _errorMessages.Count))))

  End Sub

  Private Function GenerateEventId() As EventId

    Dim index As Integer = _random.[Next](0, _eventNames.Count)
    Return New EventId(id:=&H1A4 + index, name:=_eventNames(index))

  End Function

  Private Function GetMessage() As String

    Return _messages(_random.[Next](0, _messages.Count))

  End Function

#End Region

End Class

依赖注入

使用后台服务是一个两步过程:

  1. 我们需要设置类的范围并注册服务。
  2. 手动启动管理所有注册的后台服务的托管服务。

注册

public static class ServicesExtension
{
    public static HostApplicationBuilder AddRandomBackgroundService(
        this HostApplicationBuilder builder)
    {
        builder.Services.AddSingleton<RandomLoggingService>();
        builder.Services.AddHostedService(service
            => service.GetRequiredService<RandomLoggingService>());

        return builder;
    }
}
Public Module ServicesExtension

  <Extension>
  Public Function AddRandomBackgroundService(builder As HostApplicationBuilder)
    As HostApplicationBuilder

    builder.Services.AddSingleton(Of RandomLoggingService)
    builder.Services.AddHostedService(
      Function(service) service.GetRequiredService(Of RandomLoggingService))

    Return builder

  End Function

End Module

用法

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);
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()

' Random Logging Service
builder.AddRandomBackgroundService()

_host = builder.Build()

' startup background services
Dim task As Task = _host.StartAsync(_cancellationTokenSource.Token)

手动(不使用依赖注入)

// Initialize service and pass in the Logger
RandomLoggingService service = 
   new(new Logger<RandomLoggingService>(LoggingHelper.Factory));

// Start generating log entries
_ = service.StartAsync(_cancellationTokenSource.Token);
' Initialize service and pass in the Logger
Dim service As RandomLoggingService = New RandomLoggingService_
            (New Logger(Of RandomLoggingService)(LoggingHelper.Factory))

' startup background services
Dim task As Task = _host.StartAsync(_cancellationTokenSource.Token)

LoggerMessageAttribute (仅限 C#)

在 .Net 6.0 中,支持通过 LoggerMessageAttribute 进行编译时源生成的、高性能的日志记录 API。

Microsoft 提供了涵盖使用情况的文档,称为 Compile-time logging source generation。列出的日志记录约束必须遵守:

  • 日志记录方法必须是部分方法并返回 void
  • 日志记录方法名称不得以下划线开头。
  • 日志记录方法的参数名称不得以下划线开头。
  • 日志记录方法可能未在嵌套类型中定义。
  • 日志记录方法不能是泛型。
  • 如果日志记录方法是 static,则需要 ILogger 实例作为参数。

其他未列出的约束包括:

  • 需要 Event Id,它是 static 参数。
  • 可选的 Event Name 是 static 参数。
  • 异常必须包含在消息中,而不是单独的字段。

即将发布的 .NET 8.0(截至本文撰写时)为可传递的构造函数参数增加了更多灵活性,但 static 字段仍然存在。您可以在此处阅读更多内容:Expanding LoggerMessageAttribute Constructor Overloads for Enhanced Functionality

考虑到以上约束,我们现在可以更新代码。

  1. 每个应用程序项目都需要一个专用的 Logging 方法,并带有 LoggerMessageAttribute 装饰器。
  2. 每个 Event Name 都需要自己的专用 Logging 方法。

专用应用程序日志方法

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}");
}

调用时,我们只需使用:

ApplicationLog.Emit(logger, logLevel, message);

如果存在异常,则:

ApplicationLog.Emit(logger, logLevel, message, exception);

专用 RandomServiceLog 方法

由于我们有多个 Event Name,每个 Event Name 都需要自己的专用 Logging 方法。下面,我设置了一个 Lookup 表来简化调用正确的方法并共享 Event Name。

public static partial class RandomServiceLog
{
    public static Dictionary<string, Action<ILogger, LogLevel, string>> 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);
}

注意:上面,每个唯一的 Event Name 都有一个唯一的 Event Id。这不是强制性的,但强烈建议。

调用时,我们只需使用:

RandomServiceLog.Emit("Event_Name", logger, LogLevel.Information, "message goes here");

更新 RandomLoggingService 类

现在我们可以更新 RandomLoggingService 类了。

public class RandomLoggingService : BackgroundService
{
    #region Constructors

    public RandomLoggingService(ILogger<RandomLoggingService> logger)
    {
        _logger = logger;
        _eventNames = RandomServiceLog.Events.Keys.ToList();
    }

    #endregion

    #region Fields

    #region Injected

    private readonly ILogger _logger;

    #endregion

    // ChatGPT generated lists

    private readonly List<string> _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<string> _eventNames;

    private readonly List<string> _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
}

要查看更新后的 RandomLoggingService 的实际效果,请下载代码并在 _MSlogger/Attribute_ 解决方案文件夹中的 WpfLoggingAttrDI 项目中运行。

摘要

我们涵盖了日志记录的工作原理;如何为 **WinForms** **WPF** 和 **Avalonia** 应用程序类型创建、注册和使用自定义 Logger & Provider,同时支持 **C#** 和 **VB**。我们查看了 .NET 内部代码以处理 Logger 和 Provider。我们为 **WinForms** **WPF** 和 **Avalonia** 应用程序类型创建了自定义控件,同时支持 **C#** 和 **VB**,以使用 Microsoft 的默认 Logger 和第三方 SeriLog 结构化 Logger 来消费日志。我们还涵盖了如何在依赖注入和手动连接中使用自定义 Logger 和自定义控件。最后,我们创建了 **.NET Framework** 后台服务来模拟生成日志条目的应用程序。

虽然本文篇幅较长且内容详实,但创建自定义 Logger 和消费生成的内容并不复杂,无论应用程序类型如何,以及应用程序是如何连接的,无论是手动的还是通过依赖注入。

所有源代码,包括 **C#** 和 **VB**,都可以在本文顶部的链接中找到。要在您自己的项目中进行使用,请复制应用程序类型所需的所有库,添加对 LogViewer 控件项目 + Logger 项目类型的引用,然后按照使用指南进行操作。

如果您有任何问题,请在下方留言,我很乐意为您解答。

参考文献

文档、文章等

Nuget 包

历史

  • 2023年3月23日 - v1.00 - 初始发布
  • 2023年3月28日 - v1.10 - 添加了对 NLOG 日志平台的支持 + **WinForms**、**WPF** 和 **Avalonia** 示例 DI 和非 DI 应用程序(x6);修复了 LogViewer.Winforms 项目中可能偶尔出现的“索引超出范围”异常。
  • 2023年3月29日 - v1.20 = 添加了对 Apache Log4Net 日志服务 + **WinForms**、**WPF** 和 **Avalonia** 示例 DI 和非 DI 应用程序(x6)的支持;各种代码清理和优化。
  • 2023年4月20日 - v1.20a - 使用 Microsoft 的文件浏览器“Compress to Zip”重新压缩项目。
  • 2023年9月12日 - v1.30 - 添加了 LoggerMessageAttribute (仅限 C#) 部分。
© . All rights reserved.