修改現有訊息類讓.net core專案支援Protobuf - 【無需使用 [ProtoBuf.ProtoContract] 的方法】
前言
第二次發部落格,希望大家多多鼓勵!!!
又接無上老闆的一個需求,需要讓.net core訊息傳送端跟訊息接收端通訊的訊息是protobuf格式的(基於protobuf比json小一倍資料量,獨特的編碼、沒有fieldname等),但現有專案的訊息類數量巨多,按照網上的方案是安裝protobuf.net 這個nuget包,然後需要給訊息類一個一個新增[ProtoBuf.ProtoContract]、[ProtoBuf.ProtoMember(index)]等Attributes,更可悲的是,還得處理繼承的問題,也就是要有類似如下這種程式碼:
[ProtoContract] [ProtoInclude(10, typeof(Male))] public class Person { [ProtoMember(1)] public int Id { get; set; } [ProtoMember(2)] public string Name { get; set; } [ProtoMember(3)] public Address Address { get; set;} } [ProtoContract] public class Male : Person { } [ProtoContract] public class Address { [ProtoMember(1)] public string Line1 {get;set;} [ProtoMember(2)] public string Line2 {get;set;} }
關於為什麼要設定上面這些attributes,跟protobuf的原理息息相關,有興趣的朋友可以看看這篇文章,而關於protobuf.net的基本用法,可以參考這裡
找解決方案,咱們不幹體力活
對於專案存在巨多訊息類,顯然這麼一個一個的加attributes既費時又容易出錯。我拿著這個需求,懷著忐忑的心,一通操作,終於找到了想要的方案,也就是找到了without attributes的方法,順便悄悄的告訴您,貌似國內還沒誰發現這個方法 :
使用RuntimeTypeModel.Default進行型別及其Properties的配置
動動腦筋,上面的程式碼,如果不用attributes而是用RuntimeTypeModel.Default進行型別及其Properties的配置的話,程式碼就是的:
var personMetaType = RuntimeTypeModel.Default.Add(typeof (Person), false); personMetaType.Add(1, "Id"); personMetaType.Add(2, "Name"); personMetaType.Add(3, "Address"); var addressMetaType = RuntimeTypeModel.Default.Add(typeof(Address), false); addressMetaType.Add(1, "Line1"); addressMetaType.Add(2, "Line2"); // 給父類metaType新增子型別 personMetaType.AddSubType(10, typeof (Male)); // 然後新增子型別 RuntimeTypeModel.Default.Add(typeof(Male), false); RuntimeTypeModel.Default.Add(typeof(Female), false);
但是仔細想想其實原理跟新增attributes是一個道理,
具體實現
有了上面這個方法,我們就會自然而然想到對所有訊息類使用RuntimeTypeModel.Default進行型別及其Properties的配置,但我們又不可能費時費力的給專案的每個訊息實體類新增這些程式碼,那麼這裡就想到了使用反射找出專案中所有訊息實體類,然後一個一個的操作
先看看我們的訊息基類:
/// <summary>
/// 使用MQ佇列的訊息基類
/// </summary>
public class MsgBase
{
/// <summary>
/// 訊息編碼、接入系統編碼
/// </summary>
public string MessageCode { get; set; }
/// <summary>
/// 訊息型別 (業務相關的一個列舉)
/// </summary>
public MessageTypeCode MessageType { get; set; }
}
很簡單吧,然後看看我們給類動態新增“[ProtoBuf.*]”這些attributes的核心程式碼:
static bool isInit = false; // 避免重複初始化
/// <summary>
/// 初始化,訊息傳送跟處理程式在啟動後就需要呼叫
/// </summary>
public static void Init()
{
if (!isInit)
{
var msgAssemblyName = "Msg Model 所在的 assemly long name";
// 需要處理MsgBase本身跟繼承它的所有訊息型別
var msgTypes = (from t in Assembly.Load(msgAssemblyName).GetTypes()
where (t.BaseType == typeof(MsgBase) || t.Name == "MsgBase")
select t).OrderBy(t=>t.Name).ToList();
foreach (var msgType in msgTypes)
{
AddTypeToModel(msgType, RuntimeTypeModel.Default);
}
isInit = true;
}
}
/// <summary>
/// 新增型別以及欄位到模型中
/// </summary>
/// <param name="type"></param>
/// <param name="typeModel"></param>
/// <returns></returns>
private static void AddTypeToModel(Type type, RuntimeTypeModel typeModel)
{
if (typeModel.IsDefined(type))
{
return;
}
typeModel.IncludeDateTimeKind = true;
// 1. 進行型別配置
var metaType = typeModel.Add(type, true);
// Protobuf的順序很重要,在序列化跟反序列化都需要保持一致的順序,否則反序列化的時候就會出錯
var publicProperties = type.GetProperties().Where(h => h.SetMethod != null).OrderBy(h => h.Name);
var complexPropertiesInfo = publicProperties.Where(f => !IsSimpleType(f.PropertyType)).OrderBy(h=>h.Name);
// 2. 進行此型別的Properties的配置
foreach (var simplePropertyInfo in publicProperties)
{
metaType.Add(simplePropertyInfo.Name);
}
// 複雜型別需要處理裡面的每個簡單型別,使用了遞迴操作
foreach (var complexPropertyInfo in complexPropertiesInfo)
{
if (complexPropertyInfo.PropertyType.IsGenericType)
{
// Protobuf的順序很重要,在序列化跟反序列化都需要保持一致的順序,否則反序列化的時候就會出錯
foreach (var genericArgumentType in complexPropertyInfo.PropertyType.GetGenericArguments().OrderBy(h=>h.Name))
{
if (!IsSimpleType(genericArgumentType))
{
AddTypeToModel(genericArgumentType, typeModel);
}
}
}
else
{
AddTypeToModel(complexPropertyInfo.PropertyType, typeModel);
}
}
}
/// <summary>
/// 是否為簡單型別
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static bool IsSimpleType(Type type)
{
var underlyingType = Nullable.GetUnderlyingType(type);
var newType = underlyingType ?? type;
var simpleTypes = new List<Type>
{
typeof(byte),
typeof(sbyte),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(float),
typeof(double),
typeof(decimal),
typeof(bool),
typeof(string),
typeof(char),
typeof(Guid),
typeof(DateTime),
typeof(DateTimeOffset),
typeof(byte[]),
typeof(string[])
};
return simpleTypes.Contains(newType) || newType.GetTypeInfo().IsEnum;
}
其實上面就是所有程式碼了,使用的話,就是在訊息傳送跟訊息接收程式啟動後,就呼叫上面的Init方法,僅需要呼叫一次額。當然聰明的你,肯定已經想到將它封裝成一個工具類了,哈哈。
注意事項
細心的朋友可以注意到,我並沒有呼叫AddSubType(其實我訊息類的某些property確實是複雜型別且有父子關係的)以及可能你也發現了在上面的“想辦法解決,咱們不幹體力活”章節中父子型別註冊到RuntimeTypeModel中有一個先後順序,但上面的程式碼在實際使用過程中也就是訊息接收端反序列化protobuf訊息時並沒出現問題。如果你的專案使用了上面的程式碼,結果發現反序列化不了,特別是拋了不能識別型別的錯誤,那麼很可能就是我所說的兩點要處理下。
希望大家多多評論,2020年身體健康,過得順心!!