当前位置: 首页 > news >正文

C# 统一处理mongodb中Protobuf中只读属性(RepeatedField和MapField)的序列化和反序列化映射

前言

众所周知,C#中的mongodb驱动默认是不会序列化和反序列只读属性的。所以当我们存储Protobuf的类型时,如果有属性是RepeatedField和MapField类型,那么该属性并不会被存储到mongodb数据库中。如果要正常存储,则需要自己调用RegisterClassMap方法注册该Protobuf类型的映射。类似下面的代码:

class Test
{public RepeatedField<int> RepeatedField { get;  }
}static Test CreateTest(RepeatedField<int> repeatedField)
{var res = new Test();res.RepeatedField.AddRange(repeatedField);return res;
}
static void RegisterTestClassMap()
{BsonClassMap.RegisterClassMap<Test>(cm =>{cm.AutoMap();// SetDefaultValue是为了防止Protobuf新加了属性,但是数据库的老数据没有该属性,导致反序列化失败的问题cm.MapProperty(x => x.RepeatedField).SetDefaultValue(new RepeatedField<int>());cm.MapCreator(p => CreateTest(p.RepeatedField));});
}

我们需要为每一个类型都注册类似的映射,非常的麻烦。而且如果protobuf的字段有新增,还需要改动代码,如果修改不及时还会导致线上的数据丢失。麻烦且不安全。

自动映射

我们自定义一个IClassMapConvention,自动生成并注册包含只读属性的类的构造函数的委托。
核心是利用表达式树,自动生成对应类的构造函数的委托。
生成的委托只支持protobuf里的RepeatedField和MapField类型。需要支持其他的需要自己修改。

    /// <summary>///  Protobuf的默认映射(映射只读属性及其默认值和构造函数)/// </summary>public class ProtobufDefaultMapConvention : ConventionBase, IClassMapConvention{private readonly BindingFlags _bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly;public ProtobufDefaultMapConvention() { }private string RepeatedFieldName = typeof(RepeatedField<>).Name;private string MapFieldName = typeof(MapField<,>).Name;/// <summary>/// 应用/// </summary>/// <param name="classMap"></param>public void Apply(BsonClassMap classMap){var readOnlyProperties = classMap.ClassType.GetTypeInfo().GetProperties(_bindingFlags).Where(p => IsReadOnlyProperty(classMap, p)).ToList();foreach (var property in readOnlyProperties){//<c cref="RepeatedField{T}">和<c cref="MapField{TKey, TValue}">和值类型所有对象共用同一个默认实例。要自己映射请覆盖默认值。// 必须设置默认值。否则会无法匹配构造函数if (property.PropertyType.Name == RepeatedFieldName || property.PropertyType.Name == MapFieldName || property.PropertyType.IsValueType){var defaultVal = Activator.CreateInstance(property.PropertyType);classMap.MapMember(property).SetDefaultValue(defaultVal);}else{classMap.MapMember(property).SetDefaultValue(() => Activator.CreateInstance(property.PropertyType));LogUtil.Warn($"ProtobufDefaultMapConvention->PB发现异常只读类型:{property.PropertyType.Name}");}}var res = GetProtobufCreatorFunc(classMap.ClassType, readOnlyProperties);classMap.MapCreator(res.Item1, res.Item2);}/// <summary>/// 获取protobuf的构造函数的委托(赋值只读属性)/// </summary>/// <param name="targetType"></param>/// <param name="readOnlyPropList"></param>/// <returns></returns>public (Delegate, string[]) GetProtobufCreatorFunc(Type targetType, List<PropertyInfo> readOnlyPropList){// 返回值var result = Expression.Variable(targetType, "result");// 初始化要创建的对象var initializeResult = Expression.Assign(result, Expression.New(targetType));var ctorParamList = new List<ParameterExpression>();// 构造函数func的方法体语句var ctorBlockExperssionList = new List<Expression>() { initializeResult };// 构造函数func的泛型类型列表var ctorFuncTypeList = new List<Type>(readOnlyPropList.Count + 1);// 构造函数func的参数名称列表var paramNames = new List<string>(readOnlyPropList.Count);foreach (var item in readOnlyPropList){ctorFuncTypeList.Add(item.PropertyType);paramNames.Add(item.Name);// 将需要赋值的只读属性设置为参数var ctorParam = Expression.Parameter(item.PropertyType, item.Name);ctorParamList.Add(ctorParam);// 取要赋值的只读属性var readOnlyMember = Expression.Property(result, item.Name);// 赋值语句Expression methodCall = null;if (item.PropertyType.Name == RepeatedFieldName){// 如果传入的参数不为null的话,则调用RepeatedField的AddRange方法//methodCall = Expression.IfThen(Expression.NotEqual(ctorParam, Expression.Constant(null)), Expression.Call(readOnlyMember, nameof(RepeatedField<int>.AddRange), null, ctorParam));// 不判null,如果要自己设置默认值请勿设置为nullmethodCall = Expression.Call(readOnlyMember, nameof(RepeatedField<int>.AddRange), null, ctorParam);}else if (item.PropertyType.Name == MapFieldName){// 如果传入的参数不为null的话,则调用MapField的Add方法//methodCall = Expression.IfThen(Expression.NotEqual(ctorParam, Expression.Constant(null)), Expression.Call(readOnlyMember, nameof(MapField<string, int>.Add), null, ctorParam));// 不判null,如果要自己设置默认值请勿设置为nullmethodCall = Expression.Call(readOnlyMember, nameof(MapField<string, int>.Add), null, ctorParam);}if (methodCall != null){ctorBlockExperssionList.Add(methodCall);}}ctorFuncTypeList.Add(targetType);ctorBlockExperssionList.Add(result);// 构造函数func的方法块var ctorFuncBody = Expression.Block(new[] { result }, ctorBlockExperssionList);// 构造函数func的Typevar ctorFuncType = Expression.GetFuncType(ctorFuncTypeList.ToArray());// 构造函数func的lambda表达式var ctorlambda = Expression.Lambda(ctorFuncType, ctorFuncBody, $"create{targetType.Name}", true, ctorParamList).Compile();return (ctorlambda, paramNames.ToArray());}/// <summary>/// 只读属性/// </summary>/// <param name="classMap"></param>/// <param name="propertyInfo"></param>/// <returns></returns>private static bool IsReadOnlyProperty(BsonClassMap classMap, PropertyInfo propertyInfo){if (!propertyInfo.CanRead) return false;if (propertyInfo.CanWrite) return false; // already handled by default conventionif (propertyInfo.GetIndexParameters().Length != 0) return false; // skip indexersvar getMethodInfo = propertyInfo.GetMethod;if (getMethodInfo == null){return false;}// skip overridden properties (they are already included by the base class)if (getMethodInfo.IsVirtual && getMethodInfo.GetBaseDefinition().DeclaringType != classMap.ClassType) return false;return true;}}

最后注册该Convention

ConventionRegistry.Register("PBConventionsPack", new ConventionPack { new ProtobufDefaultMapConvention() }, t => t.IsAssignableTo(typeof(IMessage)) && t is { IsAbstract: false });
http://www.jsqmd.com/news/982881/

相关文章:

  • 短视频运营必备:视频号竞品账号数据分析的一种实现思路
  • 成都旅游网站的设计与实现
  • 3大痛点1个工具:猫抓如何让你告别网页视频下载焦虑
  • 嵌入式硬件设计:从MCU时序参数到信号完整性的实战指南
  • 从‘阿帕网’到‘云服务’:分组交换是如何一步步成为互联网基石的?
  • 怎样实现终极数据安全:vaultwarden-backup多远程目标备份方案
  • 小程序毕设选题推荐:基于微信小程序校园二手交易平台系统小程序基于spring boot的校园二手交易平台系统小程序【附源码、mysql、文档、调试+代码讲解+全bao等】
  • 如何构建基于YOLOv8的智能瞄准系统:从技术原理到实战配置
  • 苹果 iOS 27 发布:应用启动提 30%、老机型性能优化,秋季正式推送!
  • NXP KMA320车规级角度传感器:AMR原理、SENT协议与ASIL安全设计详解
  • 流放之路离线Build规划神器:Path of Building终极使用指南
  • 深度解锁AMD Ryzen性能:揭秘硬件级调优的终极实战指南
  • 深度解析Slick轮播dots分页指示器的架构设计与实现机制
  • 如何为Xshell选择最佳配色方案:250+专业主题的完整指南
  • LeetDown终极指南:如何在macOS上为A6/A7设备降级iOS系统
  • Node.js 环境、NPM Yarn 安装与镜像源配置(优化精简版)
  • 英雄联盟玩家必备的三大效率工具:从新手到高手的进阶之路
  • 告别昂贵门槛,BeeWorks低成本赋能中小微企业IM私有化
  • 粉笔事业单位和中公哪个好?事业编备考看公基、职测、综应和学习方式
  • i.MX 6处理器引脚复位状态详解:硬件设计避坑与PCB布局指南
  • 告别迭代器对!C++20 Ranges 库(`<ranges>`)颠覆性深度指南
  • 全能型 AI论文写作软件排名(2026 最新)
  • 如何一劳永逸解决Windows运行库问题:VisualCppRedist AIO终极指南
  • 维基百科温室气体数据爬取实战:轻量级可追溯环境数据采集方案
  • 别听销售忽悠!团购小程序哪个好用?看这两个指标就够了
  • ARM Cortex-M4 MCU引脚配置与数据手册修订实战解析
  • 小程序毕设选题推荐:nodejs基于微信小程序印象台院大学资讯新闻设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】
  • tikv故障排查4 - 小镇
  • 告别传统 for 循环:C++20 std::views::iota 深度指南
  • ssm亚盛汽车配件销售业绩管理统(10164)