文章目錄
C# - 將 Object 的 Property 與 Value 轉換為 Dictionary
之前筆記 使用 C# 存取 InfluxDB 提到正在嘗試導入 InfluxDB,POC 使用到的 library - InfluxData.Net 在儲存資料時僅接受 Dictionary,如果需要將整個 c# object 儲存起來就必需做轉換,考量到專案規劃用量大,因此希望能儘量降低無謂地效能損耗,所以特別針對 c# object 的 Property 與 Value 轉型為 Dictionary 幾種方式的效能進行比較測試
我個人才疏學淺絕對是找不到最佳解,我就先紀錄一下測試的情境與方式,相信強大的同事們一定會幫忙找出更好的解法
基本環境說明
- macOS Mojave 10.14.6
- .NET Core SDK 2.2.301 (.NET Core Runtime 2.2.6)
NuGet package
- BenchmarkDotNet 0.11.5
- Bogus 28.3.1
測試用基礎程式碼
User.cs
public class User { public Guid UserId { get; set; } public string Name { get; set; } public DateTime Birthday { get; set; } public decimal CurrentSalary { get; set; } }
轉換的備選方式
使用 Reflection
public List<IDictionary<string, object>> ByReflection() { var h = 0; var result= new List<IDictionary<string, object>>(); for (var i = 0; i < N; i++) { var dict=_user.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(_user, null)); h = $"{h}{dict.GetHashCode()}".GetHashCode(); result.Add(dict); } return result; }
使用TypeDescriptor
public List<IDictionary<string, object>> ByTypeDescriptor() { var result= new List<IDictionary<string, object>>(); var h = 0; for (var i = 0; i < N; i++) { var dict=_user.TypeDescriptorToDictionary(); h = $"{h}{dict.GetHashCode()}".GetHashCode(); result.Add(dict); } return result; }
extension method
public static class ObjectToDictionaryHelper { public static IDictionary<string, object> TypeDescriptorToDictionary(this object source) { return source.TypeDescriptorToDictionary<object>(); } public static IDictionary<string, T> TypeDescriptorToDictionary<T>(this object source) { if (source == null) ThrowExceptionWhenSourceArgumentIsNull(); var dictionary = new Dictionary<string, T>(); foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source)) { object value = property.GetValue(source); if (IsOfType<T>(value)) { dictionary.Add(property.Name, (T)value); } } return dictionary; } private static bool IsOfType<T>(object value) { return value is T; } private static void ThrowExceptionWhenSourceArgumentIsNull() { throw new NullReferenceException("Unable to convert anonymous object to a dictionary. The source anonymous object is null."); } }
使用 Expression
public List<IDictionary<string, object>> ByPocoToDictionary() { var result= new List<IDictionary<string, object>>(); var h = 0; for (var i = 0; i < N; i++) { var dict=_user.ExpressionToDictionary(); h = $"{h}{dict.GetHashCode()}".GetHashCode(); result.Add(dict); } return result; }
extension method
public static class PocoToDictionary { private static readonly MethodInfo AddToDictionaryMethod = typeof(IDictionary<string, object>).GetMethod("Add"); private static readonly ConcurrentDictionary<Type, Func<object, IDictionary<string, object>>> Converters = new ConcurrentDictionary<Type, Func<object, IDictionary<string, object>>>(); private static readonly ConstructorInfo DictionaryConstructor = typeof(Dictionary<string, object>).GetConstructors().FirstOrDefault(c => c.IsPublic && !c.GetParameters().Any()); public static IDictionary<string, object> ExpressionToDictionary(this object obj) => obj == null ? null : Converters.GetOrAdd(obj.GetType(), o => { var outputType = typeof(IDictionary<string, object>); var inputType = obj.GetType(); var inputExpression = Expression.Parameter(typeof(object), "input"); var typedInputExpression = Expression.Convert(inputExpression, inputType); var outputVariable = Expression.Variable(outputType, "output"); var returnTarget = Expression.Label(outputType); var body = new List<Expression> { Expression.Assign(outputVariable, Expression.New(DictionaryConstructor)) }; body.AddRange( from prop in inputType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy) where prop.CanRead && (prop.PropertyType.IsPrimitive || prop.PropertyType == typeof(string)) let getExpression = Expression.Property(typedInputExpression, prop.GetMethod) select Expression.Call(outputVariable, AddToDictionaryMethod, Expression.Constant(prop.Name), getExpression)); body.Add(Expression.Return(returnTarget, outputVariable)); body.Add(Expression.Label(returnTarget, Expression.Constant(null, outputType))); var lambdaExpression = Expression.Lambda<Func<object, IDictionary<string, object>>>( Expression.Block(new[] { outputVariable }, body), inputExpression); return lambdaExpression.Compile(); })(obj); }
實際測試程式
測試用 class
public class ObjectToDictionary { private const int N = 1000000; private readonly User _user; public ObjectToDictionary() { _user= new Faker<User>() .RuleFor(a => a.UserId, f => f.Random.Guid()) .RuleFor(a => a.Name, f => f.Name.FirstName()) .RuleFor(a => a.Birthday, f => f.Date.Past()) .RuleFor(a => a.CurrentSalary, f => f.Random.Decimal(0M, 10000M)) .Generate(); } [Benchmark] public List<IDictionary<string, object>> ByReflection() { var h = 0; var result= new List<IDictionary<string, object>>(); for (var i = 0; i < N; i++) { var dict=_user.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(_user, null)); h = $"{h}{dict.GetHashCode()}".GetHashCode(); result.Add(dict); } return result; } [Benchmark] public List<IDictionary<string, object>> ByExpression() { var result= new List<IDictionary<string, object>>(); var h = 0; for (var i = 0; i < N; i++) { var dict=_user.ExpressionToDictionary(); h = $"{h}{dict.GetHashCode()}".GetHashCode(); result.Add(dict); } return result; } [Benchmark] public List<IDictionary<string, object>> ByTypeDescriptor() { var result= new List<IDictionary<string, object>>(); var h = 0; for (var i = 0; i < N; i++) { var dict=_user.TypeDescriptorToDictionary(); h = $"{h}{dict.GetHashCode()}".GetHashCode(); result.Add(dict); } return result; } }
執行測試
var summary = BenchmarkRunner.Run<ObjectToDictionary>();
效能數據
Method | Mean | Error | StdDev |
---|---|---|---|
ByReflection | 5.017 s | 0.3736 s | 1.0957 s |
ByExpression | 1.733 s | 0.0365 s | 0.0838 s |
ByTypeDescriptor | 7.087 s | 0.1385 s | 0.2982 s |
心得
還沒執行效能測試前就猜測應該會是 expression 勝出,雖然也想過使用 emit,但誠如忠成老師 C# 快速動態建立物件實體技巧 提到的可讀性,加上我也不確定 emit 寫出來後自己可以記得多久,想想還是別找自己麻煩了XD,不過如果之後有多些時間,也會做個 emit 版本的效能比較,畢竟量大的情境下 任何一點效能提升都會有幫助,對系統也都是正面的
參考資訊
文章作者 Yowko Tsai
上次更新 2020-12-11
授權合約
本部落格 (Yowko's Notes) 所有的文章內容(包含圖片),任何轉載行為,必須通知並獲本部落格作者 (Yowko Tsai) 的同意始得轉載,且轉載皆須註明出處與作者。
Yowko's Notes 由 Yowko Tsai 製作,以創用CC 姓名標示-非商業性-相同方式分享 3.0 台灣 授權條款 釋出。