# 2025-01-21 提取网络配置解析方法 ## 概述 从 `StartNetworkDeviceAsync` 方法中提取网络配置解析逻辑,创建独立的 `ParseNetworkConfigurationAsync` 方法,专门用于解析 LTE 和 NR 小区配置参数。 ## 主要变更 ### 1. 新增网络配置解析方法 - **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs` - **方法**: `ParseNetworkConfigurationAsync` - **功能**: 解析 RAN 配置中的 `cell_list` 和 `nr_cell_list` 参数 - **返回**: 包含 LTE 和 NR 小区配置的解析结果 ### 2. 方法签名 ```csharp private async Task<(bool IsSuccess, string ErrorMessage, LteCellConfiguration? LteCellConfig, NrCellConfigurationCollection? NrCellConfigs)> ParseNetworkConfigurationAsync(string networkStackCode, CancellationToken cancellationToken) ``` ### 3. 解析逻辑 - **获取网络栈配置**: 通过 `networkStackCode` 获取网络栈配置 - **获取 RAN 配置**: 通过 `RanId` 获取 RAN 配置 - **解析配置内容**: 从 `ConfigContent` JSON 中解析: - `cell_list` → `LteCellConfiguration` - `nr_cell_list` → `NrCellConfigurationCollection` ### 4. 错误处理 - **网络栈配置不存在**: 抛出 `InvalidOperationException` - **RAN ID 为空**: 抛出 `InvalidOperationException` - **RAN 配置不存在**: 抛出 `InvalidOperationException` - **配置内容为空**: 抛出 `InvalidOperationException` - **JSON 解析失败**: 捕获 `JsonException` 并转换为 `InvalidOperationException` ### 5. 日志记录 - **成功解析**: 记录网络栈编码、RAN ID、LTE 和 NR 配置状态 - **解析失败**: 记录错误信息 ### 6. 更新 StartNetworkDeviceAsync 方法 - **移除重复代码**: 删除原有的网络栈和 RAN 配置获取逻辑 - **调用新方法**: 使用 `ParseNetworkConfigurationAsync` 解析配置 - **增加日志**: 记录网络配置解析完成状态 ### 7. 添加必要的引用 - **LTE 配置**: `using X1.Domain.Models.LteCellConfiguration;` - **NR 配置**: `using X1.Domain.Models.NrCellConfiguration;` - **JSON 序列化**: `using Newtonsoft.Json;` ## 技术特点 ### 1. 职责分离 - **网络配置解析**: 专门负责解析网络配置参数 - **设备启动**: 专注于设备启动逻辑 - **代码复用**: 解析方法可在其他地方复用 ### 2. 类型安全 - **强类型返回**: 返回具体的配置类型而不是动态对象 - **可空类型**: 支持配置可能不存在的情况 - **异常处理**: 完整的异常处理机制 ### 3. 性能优化 - **异步操作**: 所有数据库操作都是异步的 - **作用域管理**: 使用 `IServiceScopeExecutor` 管理服务作用域 - **错误快速返回**: 遇到错误立即返回,避免不必要的处理 ## 使用示例 ```csharp // 解析网络配置 var parseResult = await ParseNetworkConfigurationAsync(networkStackCode, cancellationToken); if (!parseResult.IsSuccess) { throw new InvalidOperationException($"解析网络配置失败: {parseResult.ErrorMessage}"); } // 使用解析结果 var lteConfig = parseResult.LteCellConfig; var nrConfigs = parseResult.NrCellConfigs; // 记录解析状态 _logger.LogInformation("LTE配置: {HasLteConfig}, NR配置: {HasNrConfig}", lteConfig != null, nrConfigs != null); ``` ## 影响范围 - **StartFlowControllerHandler**: 主要修改文件 - **网络配置解析**: 提取为独立方法,提高代码可维护性 - **日志记录**: 增加网络配置解析相关的日志信息 - **错误处理**: 改进错误处理和异常信息 ## 修改文件列表 1. `X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs` - 添加 `ParseNetworkConfigurationAsync` 方法 - 更新 `StartNetworkDeviceAsync` 方法 - 添加必要的 using 引用 ## 重要修复 ### 避免嵌套 _scopeExecutor.ExecuteAsync - **问题**: 最初的实现中,`ParseNetworkConfigurationAsync` 方法内部使用了 `_scopeExecutor.ExecuteAsync`,而调用它的 `StartNetworkDeviceAsync` 方法也在使用 `_scopeExecutor.ExecuteAsync`,造成不必要的嵌套 - **解决方案**: 修改 `ParseNetworkConfigurationAsync` 方法签名,直接接收所需的仓储接口作为参数,避免嵌套使用 `_scopeExecutor.ExecuteAsync` - **新方法签名**: ```csharp private async Task<(bool IsSuccess, string ErrorMessage, LteCellConfiguration? LteCellConfig, NrCellConfigurationCollection? NrCellConfigs)> ParseNetworkConfigurationAsync( INetworkStackConfigRepository networkStackConfig, IRAN_ConfigurationRepository ranConfiguration, string networkStackCode, CancellationToken cancellationToken) ``` - **调用方式**: 在 `StartNetworkDeviceAsync` 中先获取仓储实例,然后传递给 `ParseNetworkConfigurationAsync` 方法 ### 简化返回值和集成缓存设置 - **问题**: 原来的方法返回复杂的配置对象,但实际上只需要知道解析是否成功 - **解决方案**: 简化方法返回值,只返回 `(bool IsSuccess, string ErrorMessage)`,并将 Redis 缓存设置集成到解析方法中 - **新方法签名**: ```csharp private async Task<(bool IsSuccess, string ErrorMessage)> ParseNetworkConfigurationAsync( INetworkStackConfigRepository networkStackConfig, IRAN_ConfigurationRepository ranConfiguration, IRedisCacheService redisCache, string networkStackCode, string taskExecutionId, string deviceCode, string runtimeCode, CancellationToken cancellationToken) ``` - **功能整合**: 将 Redis 缓存设置逻辑移到 `ParseNetworkConfigurationAsync` 方法中,使方法职责更加完整 - **调用顺序优化**: 先启动设备运行时获取 `runtimeCode`,然后解析配置并设置缓存 ### 性能优化:避免使用 dynamic 类型 - **问题**: 使用 `JsonConvert.DeserializeObject` 会影响性能,因为动态类型在运行时需要进行类型解析和方法调用 - **解决方案**: 使用 `JsonConvert.DeserializeObject>` 替代 `dynamic` 类型 - **性能优势**: - 避免动态类型的运行时开销 - 减少反射操作 - 降低内存分配 - 提高类型安全性 - **代码改进**: ```csharp // 优化前 var configJson = JsonConvert.DeserializeObject(ranConfig.ConfigContent); bool hasLteConfig = configJson?.cell_list != null; // 优化后 var configJson = JsonConvert.DeserializeObject>(ranConfig.ConfigContent); bool hasLteConfig = configJson.ContainsKey("cell_list") && configJson["cell_list"] != null; ``` ### 进一步性能优化:使用 System.Text.Json - **问题**: 使用 `Newtonsoft.Json` 的 `Dictionary` 仍然有性能开销 - **解决方案**: 使用 `System.Text.Json` 的 `JsonDocument`,这是 .NET 原生的高性能 JSON 解析器 - **性能优势**: - 零分配解析(Zero-allocation parsing) - 更快的序列化/反序列化速度 - 更低的内存使用 - 更好的缓存局部性 - **代码改进**: ```csharp // 优化前(Newtonsoft.Json) var configJson = JsonConvert.DeserializeObject>(ranConfig.ConfigContent); bool hasLteConfig = configJson.ContainsKey("cell_list") && configJson["cell_list"] != null; // 优化后(System.Text.Json) using var document = JsonDocument.Parse(ranConfig.ConfigContent); var root = document.RootElement; bool hasLteConfig = root.TryGetProperty("cell_list", out var cellListElement) && cellListElement.ValueKind != JsonValueKind.Null; ``` - **资源管理**: 使用 `using var document` 确保 `JsonDocument` 被正确释放 - **类型安全**: 使用 `TryGetProperty` 和 `ValueKind` 进行类型安全的属性访问 ### 容错性优化:处理缺失的 JSON 属性 - **问题**: 当 JSON 中缺少某些字段时,`JsonSerializer.Deserialize` 可能会失败 - **解决方案**: 配置 `JsonSerializerOptions` 来处理缺失的属性 - **配置选项**: ```csharp var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, // 属性名大小写不敏感 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 忽略 null 值 AllowTrailingCommas = true, // 允许尾随逗号 ReadCommentHandling = JsonCommentHandling.Skip // 跳过注释 }; ``` - **容错特性**: - **缺失属性**: 如果 JSON 中缺少某些字段,反序列化仍然会成功,缺失的字段会使用默认值 - **大小写不敏感**: 属性名大小写不匹配也能正确解析 - **格式容错**: 允许 JSON 中有尾随逗号和注释 - **使用方式**: 在 `JsonSerializer.Deserialize` 中传入 `jsonOptions` 参数 ### 执行顺序优化:调整方法调用顺序 - **问题**: 原来的执行顺序不合理,Redis 缓存设置需要 `runtimeCode`,但 `runtimeCode` 是在启动设备运行时后才获得的 - **解决方案**: 调整执行顺序,将网络配置解析和 Redis 缓存设置分离 - **新的执行顺序**: 1. **解析网络配置**: 先解析网络配置参数(不需要 `runtimeCode`) 2. **启动设备运行时**: 启动设备并获取 `runtimeCode` 3. **设置 Redis 缓存**: 使用获得的 `runtimeCode` 设置缓存 - **方法签名简化**: `ParseNetworkConfigurationAsync` 方法移除了不需要的参数 ```csharp // 优化前 private async Task<(bool IsSuccess, string ErrorMessage)> ParseNetworkConfigurationAsync( INetworkStackConfigRepository networkStackConfig, IRAN_ConfigurationRepository ranConfiguration, IRedisCacheService redisCache, string networkStackCode, string taskExecutionId, string deviceCode, string runtimeCode, CancellationToken cancellationToken) // 优化后 private async Task<(bool IsSuccess, string ErrorMessage)> ParseNetworkConfigurationAsync( INetworkStackConfigRepository networkStackConfig, IRAN_ConfigurationRepository ranConfiguration, string networkStackCode, CancellationToken cancellationToken) ``` - **职责分离**: 网络配置解析专注于配置验证,Redis 缓存设置在设备启动后进行 ### 兼容性修复:处理包含转义字符的 JSON - **问题**: 实际数据中包含转义的换行符(如 `\r\n`),`System.Text.Json` 的 `JsonDocument.Parse` 无法正确解析 - **示例数据**: `"{\r\n \"log_filename\": \"/tmp/ran.log\",\r\n \"log_options\": \"all.level=info..."` - **解决方案**: 使用 `Newtonsoft.Json.Linq.JObject.Parse` 替代 `JsonDocument.Parse` - **兼容性优势**: - `JObject.Parse` 可以正确处理包含转义字符的 JSON - 对格式要求更宽松,容错性更好 - 与现有数据格式完全兼容 - **代码改进**: ```csharp // 优化前(System.Text.Json - 会失败) using var document = JsonDocument.Parse(ranConfig.ConfigContent); var root = document.RootElement; bool hasLteConfig = root.TryGetProperty("cell_list", out var cellListElement); // 优化后(Newtonsoft.Json - 兼容转义字符) var configJson = JObject.Parse(ranConfig.ConfigContent); bool hasLteConfig = configJson["cell_list"] != null; ``` - **混合使用**: 使用 `JObject.Parse` 解析原始 JSON,然后用 `System.Text.Json` 反序列化为强类型对象 - **异常处理**: 同时捕获 `System.Text.Json.JsonException` 和 `Newtonsoft.Json.JsonException` ### 统一 JSON 库使用和对象集合处理 - **问题1**: 混合使用两种 JSON 库不一致,应该统一使用 `Newtonsoft.Json` - **问题2**: `cell_list` 和 `nr_cell_list` 可能是对象集合,需要正确处理 - **解决方案**: 统一使用 `Newtonsoft.Json` 并智能处理对象集合 - **配置优化**: 使用 `JsonSerializerSettings` 替代 `JsonSerializerOptions` ```csharp var jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore, DateParseHandling = DateParseHandling.None }; ``` - **对象集合处理**: - **LTE 配置**: 如果是数组解析为数组,如果是单个对象包装成数组 - **NR 配置**: 如果是数组解析为数组,如果是单个对象包装成数组 - **智能判断**: 使用 `JArray` 和 `JObject` 类型判断来处理不同的数据结构 - **代码改进**: ```csharp // LTE 配置处理(统一使用数组类型) LteCellConfiguration[]? lteCellConfigs = null; if (cellListToken is JArray cellArray && cellArray.Count > 0) { // 对象集合:解析为数组 lteCellConfigs = JsonConvert.DeserializeObject(cellArray.ToString(), jsonSettings); } else if (cellListToken is JObject) { // 单个对象:包装成数组 var singleLteCell = JsonConvert.DeserializeObject(cellListToken.ToString(), jsonSettings); if (singleLteCell != null) { lteCellConfigs = new LteCellConfiguration[] { singleLteCell }; } } // NR 配置处理(统一使用数组类型) NrCellConfiguration[]? nrCellConfigs = null; if (nrCellListToken is JArray nrArray && nrArray.Count > 0) { // 对象集合:解析为数组 nrCellConfigs = JsonConvert.DeserializeObject(nrArray.ToString(), jsonSettings); } else if (nrCellListToken is JObject) { // 单个对象:包装成数组 var singleNrCell = JsonConvert.DeserializeObject(nrCellListToken.ToString(), jsonSettings); if (singleNrCell != null) { nrCellConfigs = new NrCellConfiguration[] { singleNrCell }; } } ``` - **异常处理简化**: 统一使用 `Newtonsoft.Json` 后,异常处理也相应简化 ```csharp // 优化前(混合异常处理) catch (Exception ex) when (ex is JsonException || ex is System.Text.Json.JsonException) // 优化后(统一异常处理) catch (Newtonsoft.Json.JsonException ex) ``` - **命名空间冲突解决**: 由于同时引用了两个 JSON 库,需要使用完全限定名来避免 `JsonException` 的歧义 ### 类型转换错误处理 - **问题**: JSON 数据中的某些字段类型与 C# 类定义不匹配,例如 `k1` 字段在 JSON 中是单个数字,但类中定义为 `List` - **错误示例**: `Error converting value 4 to type 'System.Collections.Generic.List`1[System.Int32]'. Path '[0].pdsch.k1'` - **解决方案**: 添加自定义错误处理器,忽略类型转换错误 - **错误处理配置**: ```csharp var jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore, DateParseHandling = DateParseHandling.None, Error = HandleDeserializationError // 自定义错误处理 }; ``` - **错误处理方法**: ```csharp private static void HandleDeserializationError(object? sender, Newtonsoft.Json.Serialization.ErrorEventArgs e) { var currentError = e.ErrorContext.Error.Message; _logger.LogWarning("JSON 反序列化错误: {Error}, 路径: {Path}", currentError, e.ErrorContext.Path); // 对于类型转换错误,设置为已处理,继续反序列化 if (currentError.Contains("Error converting value") && currentError.Contains("to type")) { e.ErrorContext.Handled = true; } } ``` - **容错性**: 即使某些字段类型不匹配,也能继续完成反序列化过程 ### 灵活数据类型转换器 - **问题**: JSON 数据中某些字段有时是单个值,有时是数组,例如 `k1` 字段可能是 `4` 或 `[4, 8, 12]` - **解决方案**: 创建自定义 JSON 转换器 `FlexibleIntListConverter` 来处理这种灵活的数据格式 - **转换器功能**: - **读取时**: 单个整数转换为单元素列表,数组保持为列表 - **写入时**: 单元素列表转换为单个整数,多元素列表保持为数组 - **转换器实现**: ```csharp public class FlexibleIntListConverter : JsonConverter?> { public override List? ReadJson(JsonReader reader, Type objectType, List? existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; if (reader.TokenType == JsonToken.Integer) { // 单个整数 -> 单元素列表 var singleValue = Convert.ToInt32(reader.Value); return new List { singleValue }; } if (reader.TokenType == JsonToken.StartArray) { // 数组 -> 列表 var list = new List(); while (reader.Read() && reader.TokenType != JsonToken.EndArray) { if (reader.TokenType == JsonToken.Integer) list.Add(Convert.ToInt32(reader.Value)); } return list; } return null; } public override void WriteJson(JsonWriter writer, List? value, JsonSerializer serializer) { if (value == null) writer.WriteNull(); else if (value.Count == 1) writer.WriteValue(value[0]); // 单元素列表 -> 单个整数 else { writer.WriteStartArray(); foreach (var item in value) writer.WriteValue(item); writer.WriteEndArray(); } } } ``` - **使用方式**: 在 `JsonSerializerSettings` 中添加转换器 ```csharp var jsonSettings = new JsonSerializerSettings { // ... 其他设置 Converters = { new FlexibleIntListConverter() } }; ``` - **优势**: 统一处理灵活的数据格式,无需修改现有的类定义 - **架构改进**: 将转换器移动到 `X1.Domain/Common/Converters/` 目录,符合分层架构原则 - **位置**: `X1.Domain/Common/Converters/FlexibleIntListConverter.cs` - **命名空间**: `X1.Domain.Common.Converters` - **职责**: 作为通用的 JSON 转换工具,可被多个项目引用 - **引用**: 在 `StartFlowControllerHandler` 中通过 `using X1.Domain.Common.Converters;` 引用 ### 静态方法访问实例字段问题解决 - **问题**: 在静态方法 `HandleDeserializationError` 中无法访问实例字段 `_logger` - **错误信息**: "对象引用对于非静态的字段、方法或属性"BaseControllerHandler._logger"是必需的" - **解决方案**: 移除静态方法中的日志记录,因为错误处理逻辑本身更重要 - **代码改进**: ```csharp // 优化前(有编译错误) private static void HandleDeserializationError(object? sender, Newtonsoft.Json.Serialization.ErrorEventArgs e) { var currentError = e.ErrorContext.Error.Message; _logger.LogWarning("JSON 反序列化错误: {Error}, 路径: {Path}", currentError, e.ErrorContext.Path); // 编译错误 // ... } // 优化后(无编译错误) private static void HandleDeserializationError(object? sender, Newtonsoft.Json.Serialization.ErrorEventArgs e) { var currentError = e.ErrorContext.Error.Message; // 对于类型转换错误,设置为已处理,继续反序列化 if (currentError.Contains("Error converting value") && currentError.Contains("to type")) { e.ErrorContext.Handled = true; } } ``` - **最终解决方案**: 使用静态日志记录器,在静态方法中记录日志 ```csharp // 添加静态日志记录器 private static readonly ILogger _staticLogger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); // 在静态方法中使用静态日志记录器 private static void HandleDeserializationError(object? sender, Newtonsoft.Json.Serialization.ErrorEventArgs e) { var currentError = e.ErrorContext.Error.Message; if (currentError.Contains("Error converting value") && currentError.Contains("to type")) { _staticLogger.LogWarning("JSON 反序列化类型转换错误已处理: {Error}, 路径: {Path}", currentError, e.ErrorContext.Path); e.ErrorContext.Handled = true; } else { _staticLogger.LogError("JSON 反序列化错误: {Error}, 路径: {Path}", currentError, e.ErrorContext.Path); } } ``` - **优势**: - 不改变外部调用代码 - 在静态方法中成功记录日志 - 区分警告和错误日志级别 - 保持代码的简洁性 ### 编译错误修复 #### 问题 1: CanConvert 方法重写错误 - **错误**: `"FlexibleIntListConverter.CanConvert(Type)": 继承成员"JsonConverter?>.CanConvert(Type)"是密封的,无法进行重写` - **原因**: `JsonConverter.CanConvert` 方法是密封的,不能重写 - **解决方案**: 完全移除 `CanConvert` 方法的重写,因为基类 `JsonConverter?>` 已经提供了正确的实现 #### 问题 2: 可为 null 引用类型的 typeof 运算符错误 - **错误**: `不能在可为 null 的引用类型上使用 typeof 运算符` - **原因**: `typeof(List?)` 中的 `?` 导致编译错误 - **解决方案**: 由于移除了 `CanConvert` 方法,这个问题也同时解决了 #### 最终修复 - **移除**: 完全删除了 `CanConvert` 方法的重写 - **原因**: `JsonConverter?>` 基类已经自动处理了类型检查 - **结果**: 转换器现在可以正常工作,支持 `List?` 类型 ## 测试建议 1. **单元测试**: 为 `ParseNetworkConfigurationAsync` 方法编写单元测试 2. **集成测试**: 测试完整的网络设备启动流程 3. **错误场景测试**: 测试各种错误情况下的处理 4. **性能测试**: 验证解析性能是否满足要求