问题1ItemGroupFrameworkReferenceIncludeMicrosoft.AspNetCore.App//ItemGroupItemGroupFrameworkReferenceIncludeMicrosoft.AspNetCore.App//ItemGroup2ItemGroupFrameworkReferenceIncludeMicrosoft.AspNetCore.App//ItemGroup语法2vardomainAssembly.GetEntryAssembly()?.GetName().Name!;这行代码的作用是自动获取当前主程序的程序集名称结合你正在写的 Serilog 日志配置通常用来做日志标识、文件命名。下面逐段拆解语法并补充场景说明。一、逐部分语法拆解vardomainAssembly.GetEntryAssembly()?.GetName().Name!;Assembly.GetEntryAssembly()属于System.Reflection反射命名空间获取当前程序的入口程序集也就是程序启动时的主 exe 对应的程序集对象你的 WPF 主程序生成的 exe。通俗理解拿到当前正在运行的这个软件本身的元数据信息。?.空条件运算符安全调用机制如果前面的GetEntryAssembly()返回 null极少数特殊场景下入口程序集为空后面的.GetName()不会执行直接返回 null避免触发空引用异常崩溃。.GetName()从程序集对象中提取完整的名称信息对象里面包含程序名、版本号、公钥标记等程序元数据。.Name从名称信息中取出程序集的短名称也就是你的项目名称比如项目叫XrayDetection.UI这里拿到的就是这个字符串。!空值消除运算符和你之前学的_serviceProvider!完全一致告诉编译器这里的Name一定不会为 null强制消除“可能为空”的编译警告。正常 WPF 桌面程序启动时入口程序集必然存在Name也一定有值这里使用!是安全的。var domain隐式类型声明变量存储最终拿到的程序集名称字符串。varenvDirEnvironment.GetEnvironmentVariable(XRay_PROFILE_DIR);从 Windows 系统环境变量中读取一个自定义的目录路径是工业软件常用的「外部配置路径」写法配合你正在写的 Serilog 日志配置大概率是用来指定日志文件、配置文件的存储目录。一、语法逐段拆解Environment.NET 内置的系统环境类命名空间System用来读取系统信息、环境变量、当前用户、桌面路径等系统级数据。GetEnvironmentVariable(变量名)读取 Windows 系统中指定名称的环境变量值参数是环境变量的键名这里是自定义的XRay_PROFILE_DIR。环境变量存在返回对应的字符串路径比如D:\XRayDevice\Profile环境变量不存在返回nullvar envDir存储读取到的目录路径字符串后续代码会用这个路径来存放日志、配置文件等数据。第一个 !Assembly.GetEntryAssembly()!Assembly.GetEntryAssembly() 返回类型是 Assembly?可空编译器提示「可能为 null」! 作用告诉编译器开发者能保证这个返回值一定不为 null强制消除空引用警告场景说明你的 WPF 上位机正常双击 exe 启动时必然存在入口程序集这里不会是 null使用!是安全的极端例外单元测试、反射动态加载程序集场景才可能返回 null工控软件正常运行不会触发。二、工业项目核心价值为什么不把路径写死在代码里这是工控软件的标准设计思路核心是路径配置与代码解耦适配现场个性化要求客户现场通常会要求日志、检测数据存放在指定盘符比如 D 盘、数据盘不能放在 C 盘程序目录防止系统崩溃丢失数据。直接在系统环境变量里配置路径程序自动读取无需修改代码、重新编译打包。多设备统一部署同一款软件部署到多台设备每台设备的存储路径可能不同通过环境变量差异化配置安装包完全通用。权限兼容部分工业系统对程序目录有写保护日志、配置文件必须写到指定可写目录通过环境变量灵活适配。四、补充说明环境变量的生效范围该方法默认会依次读取「进程级环境变量 → 当前用户环境变量 → 系统环境变量」工业现场一般配置系统环境变量所有用户运行程序都能读到。空值兜底必备现场忘记配置环境变量时方法会返回 null代码里必须加空判断和默认路径兜底否则后续拼接路径会直接抛空引用异常。命名约定自定义环境变量通常用项目/设备名做前缀和系统变量区分开你这里的XRay_前缀就是典型的工业项目命名习惯。3publicstaticLoggingLevelSwitchDefaultLevelSwitch{get;}new(LogEventLevel.Verbose);LoggingLevelSwitch LogEventLevel.Verbose1.LoggingLevelSwitch是什么Serilog 提供的动态日志级别切换控制器核心作用程序运行时不用重启软件随时修改日志输出级别无需改代码重编译。内置属性MinimumLevel赋值LogEventLevel枚举控制最低输出等级典型工控场景正常生产设Information现场调试故障时界面按钮切换为Verbose打印完整硬件报文排查完切回低等级减少日志文件体积。2.LogEventLevel.Verbose日志等级从低到高完整排序等级含义工控使用场景Verbose最详细跟踪日志最低级别打印Modbus原始寄存器、EtherCAT周期数据、算法中间计算值仅本地调试用Debug调试信息仿真参数、流程步骤、函数入参输出Information正常业务记录扫描启停、用户登录、配方切换、批次完成Warning告警不中断流程单次通讯超时、温度接近阈值Error功能异常PLC断线、数据库写入失败、扫描报错Fatal致命崩溃最高级别高压异常、设备急停、程序闪退初始化new(LogEventLevel.Verbose) 默认开启全部日志所有等级日志都会输出。三、工控项目优势免重启调试产线设备故障时操作员在软件界面点“开启调试日志”立刻打印底层硬件原始报文排查完成一键关闭不用重启上位机打断生产统一全局管控WPF界面、后台WebApi、PLC心跳服务、检测算法共用一套日志级别不会出现有的模块打印、有的模块不打印过滤系统冗余日志搭配MinimumLevel.Override屏蔽微软框架大量冗余Debug日志只保留自己业务代码的详细日志。4rollOnFileSizeLimit5builder.Host.UseSerilog((context,services,config)1. 参数1context——HostBuilderContext核心能力承载程序启动时的全局配置、运行环境读取appsettings.json、环境变量配置context.Configuration[MesServerIp]获取运行环境标识context.Environment.IsDevelopment()判断是调试/生产模式2. 参数2services——IServiceProvider服务容器核心能力DI 容器实例可从容器取出已注册的服务参与日志配置工控典型用法读取数据库配置、硬件服务参数写入日志标识注入自定义加密服务过滤日志敏感字段设备密钥、账号密码示例// 从容器拿到设备基础信息服务附加到每条日志vardeviceInfoservices.GetRequiredServiceIDeviceInfoService();config.Enrich.WithProperty(DeviceCode,deviceInfo.DeviceCode);3. 参数3config——LoggerConfigurationSerilog日志配置构建器核心能力你整条日志配置的操作对象所有日志规则都靠它链式调用管控日志最低输出级别.MinimumLevel.XXX()附加日志字段.Enrich.XXX()配置输出目标文件/控制台/数据库.WriteTo.XXX()日志过滤、分流、模块独立文件.Filter.XXX()你代码里所有链式调用操作的都是这个config对象config.MinimumLevel.ControlledBy(DefaultLevelSwitch)// 操作日志级别.Enrich.FromLogContext()// 附加上下文.WriteTo.File(...)// 输出文件.WriteTo.Console(...)// 输出控制台ConfigureServiceIndependentLogs(config,baseLogPath);// 传给自定义分流方法三者完整协作逻辑context拿环境、配置文件信息做环境差异化日志策略services读取DI容器内业务服务把设备/批次信息附加进日志config最终承载全部日志输出、过滤、存储规则是Serilog的配置核心载体四、执行时序WebApplication.CreateBuilder() 创建 builder初始化 Host、DI、配置builder.Host.UseSerilog()注册日志框架到宿主内部回调执行你的日志配置代码拼接日志路径、创建文件夹、配置文件 / 控制台输出、模块分流builder.Build() 构建WebApplication实例DI 容器正式生成程序运行时所有ILogger注入自动绑定 Serilog 配置。补充说明重载区别UseSerilog有两类常用重载单参数重载UseSerilog(config {})缺少context、services无法读取配置文件和DI服务三参数重载UseSerilog((context, services, config) {})工业项目首选配置灵活性最高也是你当前使用的版本。6config.MinimumLevel.ControlledBy(DefaultLevelSwitch).Enrich.FromLogContext()MinimumLevel.ControlledBy(DefaultLevelSwitch)绑定全局静态日志级别控制器程序运行时无需重启直接修改DefaultLevelSwitch.MinimumLevel 切换日志粒度产线正常生产设 Information只保留业务、告警、报错日志减少磁盘占用硬件故障调试切 Verbose打印完整 Modbus/EtherCAT 底层报文方便定位通讯问题。Enrich.FromLogContext()开启日志上下文传递支持局部附加业务字段// 扫描单个工件时给当前流程所有日志绑定批次ID using (LogContext.PushProperty(BatchNo, scanBatch.BatchCode)) { _logger.LogInformation(开始检测工件); }每条日志自动携带批次、设备号、工位标识多任务并行时可精准过滤单流程日志。7.WriteTo.Console(outputTemplate:LogOutputTemplate);ConfigureServiceIndependentLogs(config,baseLogPath);一、.WriteTo.Console(outputTemplate:LogOutputTemplate);完整解析1. 功能作用把所有满足级别条件的日志同步输出到程序控制台窗口和文件日志共用一套格式化模板。2. 参数说明outputTemplate:LogOutputTemplate复用你定义好的全局日志模板控制台打印格式与磁盘日志文件完全统一模板示例publicconststringLogOutputTemplate{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception};输出效果示例2026-07-04 15:20:30.123 [INF] 启动X光检测上位机Web服务3. 工控项目使用建议Debug开发阶段保留实时看硬件交互、接口请求日志调试更高效Release现场生产打包可注释掉这一行减少IO性能消耗同时隐藏内部调试细节避免无关日志干扰操作员。二、ConfigureServiceIndependentLogs(config, baseLogPath);自定义分流方法解析1. 入参含义configSerilog 日志配置构建器实例和前面文件、控制台输出共用同一个配置对象baseLogPath日志根目录拼接好的D:/xrayprofiles/logs或环境变量指定目录用来存放各模块独立日志文件。2. 核心业务逻辑工控标准设计这个方法内部会通过 Serilog 的过滤器Filter按命名空间/业务模块拆分独立日志文件实现日志分层隔离典型拆分规则硬件通讯模块PLC、运动轴、相机→ 输出{domain}-Hardware.log检测算法、图像处理模块 → 输出{domain}-Detect.logMES对接WebAPI、远程接口模块 → 输出{domain}-WebApi.log主界面、业务流程模块 → 保留写入主日志{domain}-.log4. 现场落地价值产线故障排查时不用在全量混合日志里检索PLC断线 → 直接打开硬件独立日志查看原始通讯报文检测图像报错 → 只看算法模块日志大幅缩短设备故障定位时间。三、两行代码执行顺序说明先执行.WriteTo.Console()完成控制台输出配置再调用自定义方法ConfigureServiceIndependentLogs()在同一个config对象上追加多模块独立文件输出所有输出目标主文件、控制台、各模块独立文件并行生效日志会同时写入所有配置的输出通道。