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

2025-01-21 提取网络配置解析方法

概述

StartNetworkDeviceAsync 方法中提取网络配置解析逻辑,创建独立的 ParseNetworkConfigurationAsync 方法,专门用于解析 LTE 和 NR 小区配置参数。

主要变更

1. 新增网络配置解析方法

  • 文件: X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs
  • 方法: ParseNetworkConfigurationAsync
  • 功能: 解析 RAN 配置中的 cell_listnr_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 配置
  • 解析配置内容: 从 ConfigContent JSON 中解析:
    • cell_listLteCellConfiguration
    • nr_cell_listNrCellConfigurationCollection

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: 主要修改文件
  • 网络配置解析: 提取为独立方法,提高代码可维护性
  • 日志记录: 增加网络配置解析相关的日志信息
  • 错误处理: 改进错误处理和异常信息

修改文件列表

  1. 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.JsonDictionary<string, object> 仍然有性能开销
  • 解决方案: 使用 System.Text.JsonJsonDocument,这是 .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 被正确释放
  • 类型安全: 使用 TryGetPropertyValueKind 进行类型安全的属性访问

容错性优化:处理缺失的 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 缓存设置分离
  • 新的执行顺序:
    1. 解析网络配置: 先解析网络配置参数(不需要 runtimeCode
    2. 启动设备运行时: 启动设备并获取 runtimeCode
    3. 设置 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.JsonJsonDocument.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.JsonExceptionNewtonsoft.Json.JsonException

统一 JSON 库使用和对象集合处理

  • 问题1: 混合使用两种 JSON 库不一致,应该统一使用 Newtonsoft.Json
  • 问题2: cell_listnr_cell_list 可能是对象集合,需要正确处理
  • 解决方案: 统一使用 Newtonsoft.Json 并智能处理对象集合
  • 配置优化: 使用 JsonSerializerSettings 替代 JsonSerializerOptions
    var jsonSettings = new JsonSerializerSettings
    {
        NullValueHandling = NullValueHandling.Ignore,
        MissingMemberHandling = MissingMemberHandling.Ignore,
        DateParseHandling = DateParseHandling.None
    };
    
  • 对象集合处理:
    • LTE 配置: 如果是数组解析为数组,如果是单个对象包装成数组
    • NR 配置: 如果是数组解析为数组,如果是单个对象包装成数组
  • 智能判断: 使用 JArrayJObject 类型判断来处理不同的数据结构
  • 代码改进:
    // 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>? 类型

测试建议

  1. 单元测试: 为 ParseNetworkConfigurationAsync 方法编写单元测试
  2. 集成测试: 测试完整的网络设备启动流程
  3. 错误场景测试: 测试各种错误情况下的处理
  4. 性能测试: 验证解析性能是否满足要求