.NET 7类库扩展方法中如何使用主应用提供的日志抽象?
我有一个基于.NET 7的C#类库,通过扩展方法被WPF/控制台应用初始化。类库不依赖任何外部日志提供程序,需使用主应用提供的日志服务。类库内的服务(如MyLibService1、MyLibService2)通过ILogger<>依赖注入日志正常,但在扩展方法中无法实现日志记录。
类库简化初始化代码如下:
public static class AddMyLibraryExtensions { public static HostApplicationBuilder AddMyLibrary(this HostApplicationBuilder builder) { var libConfiguration = builder.Configuration.GetRequiredSection("MyLibrary"); builder.Services.AddOptions<MyLibrarySettings>() .Bind(libConfiguration); builder.Services.TryAddTransient<IMyLibService1, MyLibService1>(); builder.Services.TryAddTransient<IMyLibService2, MyLibService2>(); var libSettings = libConfiguration.Get<MyLibrarySettings>(); builder.Services.AddDbContext<MyLibDbContext>(opts => { opts.UseSqlite(libSettings.ConnectionString); }); return builder; } }
主应用使用方式如下(使用CreateApplicationBuilder有诸多优势):
static void Main(string[] args) { var builder = Host.CreateApplicationBuilder(); builder.Services.AddTransient<IAppService1, AppService1>(); builder.Services.AddTransient<IAppService2, AppService2>(); builder.AddMyLibrary(); // 类库可能在此处添加... builder.Services.AddLogging(cfg => { var logConf = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .CreateLogger(); Log.Logger = logConf; cfg.AddSerilog(logConf); // 示例使用Serilog }); // builder.AddLibrary(); // 或在此处添加... var host = builder.Build(); var job = host.Services.GetService<IAppService1>(); job.DoWork(); }
类库无法知晓日志何时添加(是否添加)、使用何种提供程序,且扩展方法中无法使用依赖注入,宿主构建完成前也无法使用宿主的服务定位器。
请问有没有办法在AddMyLibrary方法中使用主应用的日志服务?目前我想到的方式是将应用日志作为参数传入扩展方法,如.AddMyLibrary(object logger),但这种方式不够优雅。
方法1:利用HostApplicationBuilder内置日志工厂
HostApplicationBuilder本身内置了临时日志工厂(builder.Logging),可以直接创建ILogger实例,即使主应用后续配置了完整日志提供程序,临时日志也会自动桥接到最终系统,不会丢失日志条目。
修改类库扩展方法:
public static class AddMyLibraryExtensions { public static HostApplicationBuilder AddMyLibrary(this HostApplicationBuilder builder) { // 创建类库专属日志实例 var logger = builder.Logging.CreateLogger<AddMyLibraryExtensions>(); try { var libConfiguration = builder.Configuration.GetRequiredSection("MyLibrary"); logger.LogInformation("正在加载MyLibrary配置"); builder.Services.AddOptions<MyLibrarySettings>() .Bind(libConfiguration); builder.Services.TryAddTransient<IMyLibService1, MyLibService1>(); builder.Services.TryAddTransient<IMyLibService2, MyLibService2>(); var libSettings = libConfiguration.Get<MyLibrarySettings>(); if (libSettings == null) { logger.LogError("MyLibrary配置加载失败"); throw new InvalidOperationException("MyLibrary配置无效"); } builder.Services.AddDbContext<MyLibDbContext>(opts => { opts.UseSqlite(libSettings.ConnectionString); logger.LogInformation("已配置SQLite数据库连接: {ConnectionString}", libSettings.ConnectionString); }); logger.LogInformation("MyLibrary初始化完成"); } catch (Exception ex) { logger.LogError(ex, "MyLibrary初始化出错"); throw; } return builder; } }
方法2:延迟初始化到宿主启动时
如果需要依赖完整配置的日志系统,可将需要日志的初始化逻辑延迟到宿主构建完成后,通过IHostedService执行,确保使用主应用最终配置的日志服务:
- 在类库中添加初始化托管服务:
internal class MyLibraryInitializerHostedService : IHostedService { private readonly ILogger<MyLibraryInitializerHostedService> _logger; private readonly MyLibrarySettings _settings; private readonly MyLibDbContext _dbContext; public MyLibraryInitializerHostedService(ILogger<MyLibraryInitializerHostedService> logger, IOptions<MyLibrarySettings> settings, MyLibDbContext dbContext) { _logger = logger; _settings = settings.Value; _dbContext = dbContext; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("开始执行MyLibrary延迟初始化"); // 这里可执行需要日志的初始化逻辑,比如数据库迁移 await _dbContext.Database.EnsureCreatedAsync(cancellationToken); _logger.LogInformation("MyLibrary数据库初始化完成"); } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } }
- 在扩展方法中注册该服务:
public static HostApplicationBuilder AddMyLibrary(this HostApplicationBuilder builder) { var libConfiguration = builder.Configuration.GetRequiredSection("MyLibrary"); builder.Services.AddOptions<MyLibrarySettings>() .Bind(libConfiguration); builder.Services.TryAddTransient<IMyLibService1, MyLibService1>(); builder.Services.TryAddTransient<IMyLibService2, MyLibService2>(); builder.Services.AddDbContext<MyLibDbContext>(opts => { var libSettings = libConfiguration.Get<MyLibrarySettings>(); opts.UseSqlite(libSettings.ConnectionString); }); // 注册延迟初始化服务 builder.Services.AddHostedService<MyLibraryInitializerHostedService>(); return builder; }
方法3:优雅接收日志工厂参数
如果觉得直接传logger不够优雅,可以改为接收ILoggerFactory参数,类库可自主创建符合命名空间的日志实例,同时保持与主应用日志系统解耦:
修改扩展方法签名:
public static HostApplicationBuilder AddMyLibrary(this HostApplicationBuilder builder, ILoggerFactory loggerFactory = null) { // 优先使用传入的工厂,否则用builder内置的 var logger = (loggerFactory ?? builder.Logging).CreateLogger<AddMyLibraryExtensions>(); // 后续初始化逻辑同方法1... }
主应用调用时可选择传入:
// 若已配置日志,可传入LoggerFactory var loggerFactory = builder.Services.BuildServiceProvider().GetRequiredService<ILoggerFactory>(); builder.AddMyLibrary(loggerFactory);
也可保持无参调用,自动使用内置日志。
内容的提问来源于stack exchange,提问作者abk

