C#自定义ILogger如何复用AppSettings公共路径配置
我检索了相关资料未找到目标解决方案,此处仅提供核心代码片段用于说明问题,全量代码依赖整套业务系统无法独立运行。
我使用C#内置日志框架实现向Windows事件查看器、控制台输出日志;因不想引入第三方日志类库,自行编写了简易文件日志组件,可正常将同源日志写入文件。
我的appsettings.json中配置了工作目录等业务参数,同时也单独配置了日志组件的存储路径,我希望复用公共的工作路径配置,无需两处重复配置相同路径。
现有配置与代码
appsettings.json
{ "AppSettings": { "WorkingFolderPath": "C:\\mypath\\", "HeartbeatIntervalMinutes": 0, // 调试模式下该值单位为秒 "FileRetryDelayMinutes": 1, "PingTimeoutMilliseconds": 200 }, "Logging": { "LogLevel": { "Default": "Error", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Warning" }, "Debug": { "LogLevel": { "Default": "Debug" } }, "Console": { "IncludeScopes": true, "LogLevel": { "Default": "Debug" } }, "EventLog": { "LogLevel": { "Default": "Warning" } }, "FileLog": { "Options": { "FolderPath": "C:\\mypath\\", "RetentionDays": 5 }, "LogLevel": { "Default": "Debug" } } } }
如配置文件所示,目标是直接使用AppSettings节点下的WorkingFolderPath作为FileLog日志组件的存储路径,无需在FileLog的Options节点下重复配置FolderPath。
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseWindowsService() .ConfigureLogging((context, logging) => { logging.ClearProviders(); logging.AddConsole(); logging.AddDebug(); logging.AddEventLog(new EventLogSettings() { SourceName = "FreshIQAppMessagingService" }); logging.AddFileLogger(options => { context.Configuration.GetSection("Logging").GetSection("FileLog").GetSection("Options").Bind(options); }); }) .ConfigureAppConfiguration((hostContext, config) => { config .SetBasePath(ApplicationPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true); config.AddEnvironmentVariables(); }) .ConfigureServices((hostContext, services) => { services.Configure<Configuration>(hostContext.Configuration.GetSection("AppSettings")); services.AddHostedService<Service1>(); services.AddHostedService<Service2>(); services.AddHostedService<Service3>(); services.AddHostedService<Service4>(); });
当前代码在配置日志时仅绑定了FileLog节点下的Options配置,未找到合适的修改方案,不知道如何调整相关类的实现,才能将appsettings中AppSettings节点下的WorkingFolderPath传入自定义FileLogger,替换原有Options中的路径配置,实现配置复用。
当前绑定配置的代码片段如下:
logging.AddFileLogger(options => { context.Configuration.GetSection("Logging").GetSection("FileLog").GetSection("Options").Bind(options); });
自定义日志组件实现
FileLoggerExtensions.cs
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; namespace FreshIQAppMessaging.Logging { public static class FileLoggerExtensions { public static ILoggingBuilder AddFileLogger(this ILoggingBuilder loggingBuilder, Action<FileLoggerOptions> configure) { loggingBuilder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>(); loggingBuilder.Services.Configure(configure); return loggingBuilder; } } }
FileLoggerProvider.cs
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.IO; namespace FreshIQAppMessaging.Logging { [ProviderAlias("FileLog")] public class FileLoggerProvider : ILoggerProvider { public readonly FileLoggerOptions Options; public FileLoggerProvider(IOptions<FileLoggerOptions> options) { Options = options.Value; if (!Directory.Exists(Options.FolderPath)) { Directory.CreateDirectory(Options.FolderPath); } } public ILogger CreateLogger(string categoryName) { return new FileLogger(this); } public void Dispose() { } } }
FileLogger.cs
using Microsoft.Extensions.Logging; using System; using System.Globalization; using System.IO; namespace FreshIQAppMessaging.Logging { public class FileLogger : ILogger { protected readonly FileLoggerProvider _fileLoggerProvider; public FileLogger(FileLoggerProvider fileLoggerProvider) { _fileLoggerProvider = fileLoggerProvider; } public IDisposable? BeginScope<TState>(TState state) { return null; } public bool IsEnabled(LogLevel logLevel) { return logLevel != LogLevel.None; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } // 清理过期日志文件 var logFiles = Directory.GetFiles(_fileLoggerProvider.Options.FolderPath, "*-MyApp.log"); foreach (var logFilePath in logFiles) { var logFileName = new FileInfo(logFilePath).Name; if (DateTime.TryParseExact(logFileName.Substring(0, 8), "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var logFileDate)) { if (logFileDate.AddDays(_fileLoggerProvider.Options.RetentionDays) < DateTime.Now) { File.Delete(logFilePath); } } } var fullFilePath = $"{_fileLoggerProvider.Options.FolderPath}\\{DateTime.Now.ToString("yyyyMMdd")}-MyApp.log"; var logRecord = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} [{logLevel.ToString()}] {formatter(state, exception)} {(exception != null ? exception.StackTrace : "")}"; using (var streamWriter = new StreamWriter(fullFilePath, true)) { streamWriter.WriteLine(logRecord); } } } }
FileLoggerOptions.cs
using System; using System.Collections.Generic; using System.Text; namespace FreshIQAppMessaging.Logging { public class FileLoggerOptions { public virtual string? FolderPath { get; set; } public virtual int RetentionDays { get; set; } = 5; } }
只需要修改Program.cs中AddFileLogger的配置绑定逻辑,绑定完FileLog的Options配置后,手动读取AppSettings:WorkingFolderPath赋值给FolderPath即可,显式读取的工作路径优先级高于FileLog节点下配置的路径,修改后的代码如下:
logging.AddFileLogger(options => { // 先绑定FileLog节点下的基础配置 context.Configuration.GetSection("Logging").GetSection("FileLog").GetSection("Options").Bind(options); // 读取公共工作路径配置,覆盖FolderPath var workingPath = context.Configuration.GetValue<string>("AppSettings:WorkingFolderPath"); if (!string.IsNullOrWhiteSpace(workingPath)) { options.FolderPath = workingPath; } });
修改完成后即可删除appsettings.json中Logging:FileLog:Options:FolderPath的重复配置,日志组件会自动读取公共工作路径作为日志存储目录,实现配置复用。
注意:.NET Host的默认执行顺序是先加载配置,再执行日志配置、服务配置,所以
context.Configuration中已经包含了appsettings.json的所有配置内容,直接读取不会出现空值问题。如果后续调整执行顺序出现配置读取为空的情况,将ConfigureAppConfiguration移动到ConfigureLogging之前即可。
内容的提问来源于stack exchange,提问作者Chris Ward

