You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
21 KiB
21 KiB
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. 方法签名
private async Task<(bool IsSuccess, string ErrorMessage, LteCellConfiguration? LteCellConfig, NrCellConfigurationCollection? NrCellConfigs)> ParseNetworkConfigurationAsync(string networkStackCode, CancellationToken cancellationToken)
3. 解析逻辑
- 获取网络栈配置: 通过
networkStackCode获取网络栈配置 - 获取 RAN 配置: 通过
RanId获取 RAN 配置 - 解析配置内容: 从
ConfigContentJSON 中解析:cell_list→LteCellConfigurationnr_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管理服务作用域 - 错误快速返回: 遇到错误立即返回,避免不必要的处理
使用示例
// 解析网络配置
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: 主要修改文件
- 网络配置解析: 提取为独立方法,提高代码可维护性
- 日志记录: 增加网络配置解析相关的日志信息
- 错误处理: 改进错误处理和异常信息
修改文件列表
X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs- 添加
ParseNetworkConfigurationAsync方法 - 更新
StartNetworkDeviceAsync方法 - 添加必要的 using 引用
- 添加
重要修复
避免嵌套 _scopeExecutor.ExecuteAsync
- 问题: 最初的实现中,
ParseNetworkConfigurationAsync方法内部使用了_scopeExecutor.ExecuteAsync,而调用它的StartNetworkDeviceAsync方法也在使用_scopeExecutor.ExecuteAsync,造成不必要的嵌套 - 解决方案: 修改
ParseNetworkConfigurationAsync方法签名,直接接收所需的仓储接口作为参数,避免嵌套使用_scopeExecutor.ExecuteAsync - 新方法签名:
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 缓存设置集成到解析方法中 - 新方法签名:
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<dynamic>会影响性能,因为动态类型在运行时需要进行类型解析和方法调用 - 解决方案: 使用
JsonConvert.DeserializeObject<Dictionary<string, object>>替代dynamic类型 - 性能优势:
- 避免动态类型的运行时开销
- 减少反射操作
- 降低内存分配
- 提高类型安全性
- 代码改进:
// 优化前 var configJson = JsonConvert.DeserializeObject<dynamic>(ranConfig.ConfigContent); bool hasLteConfig = configJson?.cell_list != null; // 优化后 var configJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(ranConfig.ConfigContent); bool hasLteConfig = configJson.ContainsKey("cell_list") && configJson["cell_list"] != null;
进一步性能优化:使用 System.Text.Json
- 问题: 使用
Newtonsoft.Json的Dictionary<string, object>仍然有性能开销 - 解决方案: 使用
System.Text.Json的JsonDocument,这是 .NET 原生的高性能 JSON 解析器 - 性能优势:
- 零分配解析(Zero-allocation parsing)
- 更快的序列化/反序列化速度
- 更低的内存使用
- 更好的缓存局部性
- 代码改进:
// 优化前(Newtonsoft.Json) var configJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(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来处理缺失的属性 - 配置选项:
var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, // 属性名大小写不敏感 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 忽略 null 值 AllowTrailingCommas = true, // 允许尾随逗号 ReadCommentHandling = JsonCommentHandling.Skip // 跳过注释 }; - 容错特性:
- 缺失属性: 如果 JSON 中缺少某些字段,反序列化仍然会成功,缺失的字段会使用默认值
- 大小写不敏感: 属性名大小写不匹配也能正确解析
- 格式容错: 允许 JSON 中有尾随逗号和注释
- 使用方式: 在
JsonSerializer.Deserialize中传入jsonOptions参数
执行顺序优化:调整方法调用顺序
- 问题: 原来的执行顺序不合理,Redis 缓存设置需要
runtimeCode,但runtimeCode是在启动设备运行时后才获得的 - 解决方案: 调整执行顺序,将网络配置解析和 Redis 缓存设置分离
- 新的执行顺序:
- 解析网络配置: 先解析网络配置参数(不需要
runtimeCode) - 启动设备运行时: 启动设备并获取
runtimeCode - 设置 Redis 缓存: 使用获得的
runtimeCode设置缓存
- 解析网络配置: 先解析网络配置参数(不需要
- 方法签名简化:
ParseNetworkConfigurationAsync方法移除了不需要的参数// 优化前 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- 对格式要求更宽松,容错性更好
- 与现有数据格式完全兼容
- 代码改进:
// 优化前(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替代JsonSerializerOptionsvar jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore, DateParseHandling = DateParseHandling.None }; - 对象集合处理:
- LTE 配置: 如果是数组解析为数组,如果是单个对象包装成数组
- NR 配置: 如果是数组解析为数组,如果是单个对象包装成数组
- 智能判断: 使用
JArray和JObject类型判断来处理不同的数据结构 - 代码改进:
// LTE 配置处理(统一使用数组类型) LteCellConfiguration[]? lteCellConfigs = null; if (cellListToken is JArray cellArray && cellArray.Count > 0) { // 对象集合:解析为数组 lteCellConfigs = JsonConvert.DeserializeObject<LteCellConfiguration[]>(cellArray.ToString(), jsonSettings); } else if (cellListToken is JObject) { // 单个对象:包装成数组 var singleLteCell = JsonConvert.DeserializeObject<LteCellConfiguration>(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<NrCellConfiguration[]>(nrArray.ToString(), jsonSettings); } else if (nrCellListToken is JObject) { // 单个对象:包装成数组 var singleNrCell = JsonConvert.DeserializeObject<NrCellConfiguration>(nrCellListToken.ToString(), jsonSettings); if (singleNrCell != null) { nrCellConfigs = new NrCellConfiguration[] { singleNrCell }; } } - 异常处理简化: 统一使用
Newtonsoft.Json后,异常处理也相应简化// 优化前(混合异常处理) 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<int> - 错误示例:
Error converting value 4 to type 'System.Collections.Generic.List1[System.Int32]'. Path '[0].pdsch.k1'` - 解决方案: 添加自定义错误处理器,忽略类型转换错误
- 错误处理配置:
var jsonSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore, DateParseHandling = DateParseHandling.None, Error = HandleDeserializationError // 自定义错误处理 }; - 错误处理方法:
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来处理这种灵活的数据格式 - 转换器功能:
- 读取时: 单个整数转换为单元素列表,数组保持为列表
- 写入时: 单元素列表转换为单个整数,多元素列表保持为数组
- 转换器实现:
public class FlexibleIntListConverter : JsonConverter<List<int>?> { public override List<int>? ReadJson(JsonReader reader, Type objectType, List<int>? 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<int> { singleValue }; } if (reader.TokenType == JsonToken.StartArray) { // 数组 -> 列表 var list = new List<int>(); 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<int>? 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中添加转换器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"是必需的"
- 解决方案: 移除静态方法中的日志记录,因为错误处理逻辑本身更重要
- 代码改进:
// 优化前(有编译错误) 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; } } - 最终解决方案: 使用静态日志记录器,在静态方法中记录日志
// 添加静态日志记录器 private static readonly ILogger<StartFlowControllerHandler> _staticLogger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger<StartFlowControllerHandler>(); // 在静态方法中使用静态日志记录器 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<List<int>?>.CanConvert(Type)"是密封的,无法进行重写 - 原因:
JsonConverter<T>.CanConvert方法是密封的,不能重写 - 解决方案: 完全移除
CanConvert方法的重写,因为基类JsonConverter<List<int>?>已经提供了正确的实现
问题 2: 可为 null 引用类型的 typeof 运算符错误
- 错误:
不能在可为 null 的引用类型上使用 typeof 运算符 - 原因:
typeof(List<int>?)中的?导致编译错误 - 解决方案: 由于移除了
CanConvert方法,这个问题也同时解决了
最终修复
- 移除: 完全删除了
CanConvert方法的重写 - 原因:
JsonConverter<List<int>?>基类已经自动处理了类型检查 - 结果: 转换器现在可以正常工作,支持
List<int>?类型
测试建议
- 单元测试: 为
ParseNetworkConfigurationAsync方法编写单元测试 - 集成测试: 测试完整的网络设备启动流程
- 错误场景测试: 测试各种错误情况下的处理
- 性能测试: 验证解析性能是否满足要求