使用动态发射的POCO进行快速序列化和反序列化

我目前正在将SQL表行序列化为二进制格式以实现高效存储。 我将二进制数据序列化/反序列化为每行List 。 我正在尝试将其升级为使用POCO,这将通过每列一个字段动态生成(发出)。

我已经在网上搜索了几个小时,偶然发现了像EF,T4,ExpandoObject这样的ORM /框架,但所有这些都使用动态对象(可以动态添加/删除属性)或者在编译之前简单地生成POCO。 我不能使用模板,因为表的模式在编译时是未知的,并且使用动态对象会过度(和慢),因为我知道确切的属性集及其类型。 我需要为每个表生成一个POCO,其中Fields对应于列,并且相应地设置数据类型(INT – > int,TEXT – > string)。

在生成POCO之后,我将继续使用发出的CIL来获取/设置属性,就像PetaPoco对静态编译的POCO所做的那样 。 我希望这一切都比使用无类型列表更快,并给我高保真POCO,这些POCO是强类型的并且可以由CLR加速。 我认为这是正确的吗? 你可以在运行时创建POCO吗? 与使用List相比,使用POCO会更快或更节省内存吗? 基本上,值得麻烦吗? 我已经知道如何使用发出的CIL加速获取/设置字段。

从评论和聊天中,似乎关键部分仍然是创建动态类型; 好的,这是一个完整的示例,显示了完全可序列化(通过任何常见的序列化程序)类型。 您当然可以添加更多类型 – 可能是索引器按数字或名称获取属性, INotifyPropertyChanged等。

此外 – 关键点:您必须缓存并重新使用生成的Type实例。 不要让这些东西再生……你会出血记忆。

 using Newtonsoft.Json; using ProtoBuf; using System; using System.IO; using System.Reflection; using System.Reflection.Emit; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using System.Xml.Serialization; public interface IBasicRecord { object this[int field] { get; set; } } class Program { static void Main() { object o = 1; int foo = (int)o; string[] names = { "Id", "Name", "Size", "When" }; Type[] types = { typeof(int), typeof(string), typeof(float), typeof(DateTime?) }; var asm = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName("DynamicStuff"), AssemblyBuilderAccess.Run); var module = asm.DefineDynamicModule("DynamicStuff"); var tb = module.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Serializable); tb.SetCustomAttribute(new CustomAttributeBuilder( typeof(DataContractAttribute).GetConstructor(Type.EmptyTypes), new object[0])); tb.AddInterfaceImplementation(typeof(IBasicRecord)); FieldBuilder[] fields = new FieldBuilder[names.Length]; var dataMemberCtor = typeof(DataMemberAttribute).GetConstructor(Type.EmptyTypes); var dataMemberProps = new[] { typeof(DataMemberAttribute).GetProperty("Order") }; for (int i = 0; i < fields.Length; i++) { var field = fields[i] = tb.DefineField("_" + names[i], types[i], FieldAttributes.Private); var prop = tb.DefineProperty(names[i], PropertyAttributes.None, types[i], Type.EmptyTypes); var getter = tb.DefineMethod("get_" + names[i], MethodAttributes.Public | MethodAttributes.HideBySig, types[i], Type.EmptyTypes); prop.SetGetMethod(getter); var il = getter.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldfld, field); // .Foo il.Emit(OpCodes.Ret); // return var setter = tb.DefineMethod("set_" + names[i], MethodAttributes.Public | MethodAttributes.HideBySig, typeof(void), new Type[] { types[i] }); prop.SetSetMethod(setter); il = setter.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_1); // value il.Emit(OpCodes.Stfld, field); // .Foo = il.Emit(OpCodes.Ret); prop.SetCustomAttribute(new CustomAttributeBuilder( dataMemberCtor, new object[0], dataMemberProps, new object[1] { i + 1 })); } foreach (var prop in typeof(IBasicRecord).GetProperties()) { var accessor = prop.GetGetMethod(); if (accessor != null) { var args = accessor.GetParameters(); var argTypes = Array.ConvertAll(args, a => a.ParameterType); var method = tb.DefineMethod(accessor.Name, accessor.Attributes & ~MethodAttributes.Abstract, accessor.CallingConvention, accessor.ReturnType, argTypes); tb.DefineMethodOverride(method, accessor); var il = method.GetILGenerator(); if (args.Length == 1 && argTypes[0] == typeof(int)) { var branches = new Label[fields.Length]; for (int i = 0; i < fields.Length; i++) { branches[i] = il.DefineLabel(); } il.Emit(OpCodes.Ldarg_1); // key il.Emit(OpCodes.Switch, branches); // switch // default: il.ThrowException(typeof(ArgumentOutOfRangeException)); for (int i = 0; i < fields.Length; i++) { il.MarkLabel(branches[i]); il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldfld, fields[i]); // .Foo if (types[i].IsValueType) { il.Emit(OpCodes.Box, types[i]); // (object) } il.Emit(OpCodes.Ret); // return } } else { il.ThrowException(typeof(NotImplementedException)); } } accessor = prop.GetSetMethod(); if (accessor != null) { var args = accessor.GetParameters(); var argTypes = Array.ConvertAll(args, a => a.ParameterType); var method = tb.DefineMethod(accessor.Name, accessor.Attributes & ~MethodAttributes.Abstract, accessor.CallingConvention, accessor.ReturnType, argTypes); tb.DefineMethodOverride(method, accessor); var il = method.GetILGenerator(); if (args.Length == 2 && argTypes[0] == typeof(int) && argTypes[1] == typeof(object)) { var branches = new Label[fields.Length]; for (int i = 0; i < fields.Length; i++) { branches[i] = il.DefineLabel(); } il.Emit(OpCodes.Ldarg_1); // key il.Emit(OpCodes.Switch, branches); // switch // default: il.ThrowException(typeof(ArgumentOutOfRangeException)); for (int i = 0; i < fields.Length; i++) { il.MarkLabel(branches[i]); il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_2); // value il.Emit(types[i].IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, types[i]); // (SomeType) il.Emit(OpCodes.Stfld, fields[i]); // .Foo = il.Emit(OpCodes.Ret); // return } } else { il.ThrowException(typeof(NotImplementedException)); } } } var type = tb.CreateType(); var obj = Activator.CreateInstance(type); // we'll use the index (via a known interface) to set the values IBasicRecord rec = (IBasicRecord)obj; rec[0] = 123; rec[1] = "abc"; rec[2] = 12F; rec[3] = DateTime.Now; for (int i = 0; i < 4; i++) { Console.WriteLine("{0} = {1}", i, rec[i]); } using (var ms = new MemoryStream()) { var ser = new XmlSerializer(type); ser.Serialize(ms, obj); Console.WriteLine("XmlSerializer: {0} bytes", ms.Length); } using (var ms = new MemoryStream()) { using (var writer = new StreamWriter(ms, Encoding.UTF8, 1024, true)) { var ser = new JsonSerializer(); ser.Serialize(writer, obj); } Console.WriteLine("Json.NET: {0} bytes", ms.Length); } using (var ms = new MemoryStream()) { var ser = new DataContractSerializer(type); ser.WriteObject(ms, obj); Console.WriteLine("DataContractSerializer: {0} bytes", ms.Length); } using (var ms = new MemoryStream()) { Serializer.NonGeneric.Serialize(ms, obj); Console.WriteLine("protobuf-net: {0} bytes", ms.Length); } using (var ms = new MemoryStream()) { // note: NEVER do this unless you have a custom Binder; your // assembly WILL NOT deserialize in the next AppDomain (ie // the next time you load your app, you won't be able to load) // - shown only for illustration var bf = new BinaryFormatter(); bf.Serialize(ms, obj); Console.WriteLine("BinaryFormatter: {0} bytes", ms.Length); } } } 

输出:

 XmlSerializer: 246 bytes Json.NET: 81 bytes DataContractSerializer: 207 bytes protobuf-net: 25 bytes BinaryFormatter: 182 bytes 

这实际上是一个非常复杂的问题。 不幸的是,要完全回答它,你必须基本上写它并测试它,但是 – 我强烈建议你不要看任何即时的POCO代,直到你得到你的答案! 基本上,你现在应该忽略这一步。

性能中另一个基本问题是:它需要多快? 我要做的绝对第一件事就是绝对最简单的工作 ,并衡量它。 最简单的方法是:将其加载到DataTable并序列化该DataTable (使用RemotingFormat = RemotingFormat.Binary; )。 在10行代码中,您将在沙子中找到一条线:

 var dt = new DataTable(); dt.Load(yourDataReader); //... any access tests dt.RemotingFormat = SerializationFormat.Binary; using (var file = File.Create(path)) { var bf = new BinaryFormatter(); bf.Serialize(file, dt); } // ... also check deserialize, if that is perf-critical 

通常我不建议使用DataTableBinaryFormatter ,但是……在这种情况下它似乎并不牵强。

就个人而言,我怀疑你会发现二进制 – 远程模式下的DataTable实际上并不可怕。

下一步是看看还有什么工作没有任何巨大的努力。 例如:

  • 使用dapper等工具将数据源加载到对象中是一个已解决的问题
  • 使用protobuf-net等工具以一种非常有效的方式序列化一组对象是一个已解决的问题

所以我很想创建一个说明性的类(纯粹是为了看看它是否更好):

 [DataContract] public class Foo { [DataMember(Order=1)] public int Id {get;set;} [DataMember(Order=2)] public string Name {get;set;} // ... more props // IMPORTANT: make this representative - basically, the same data // that you had in the data-table // note also include any supporting info - any indexers and interface // support that your core code needs } [DataContract] public class FooWrapper { // just to help in the test [DataMember(Order=1)] public List Items {get;set;} } 

并执行相同的测试(您的主代码只会使用索引器访问,但让dapper现在使用.Query(...) API:

 var data = conn.Query(...).ToList(); // dapper //... any access tests, just using the indexer API using (var file = File.Create(path)) { var wrapper = new FooWrapper { Items = data }; Serializer.Serialize(file, wrapper); // protobuf-net } // note that you deserialize via Serializer.Deserialize(file) 

这一点的重点在于,这将为您提供一些可以达到的合理预期的界限。 您可以随意使用自己的材料化程序/序列化程序代替dapper / protobuf-net,但我谦卑地认为这两个程序已针对大部分情况进行了大量优化。

当你有一个下限和上限时,你有合理的数据来回答“它是否值得”的问题。 在运行时生成对象并不是很难,但它比大多数人需要做的更多。 您还要非常小心地尽可能重用生成的类型。 请注意,如果你这条路线,protobuf-net有一个完全非通用的API,通过Serializer.NonGenericRuntimeTypeModel.Default (所有三个选项最终都在同一个核心)。 Dapper 没有 ,但我很乐意添加一个 (接受一个Type实例)。 在此期间,您还可以使用MakeGenericMethod / Invoke执行该步骤。

我意识到我没有直接回答“它是否值得”,但这是故意的:如果没有直接应用到您的场景,这是无法回答的。 希望我已经提供了一些关于如何为您的场景回答它的提示。 我很想听听你的发现。

只有当你知道它是值得的时候(并且上面我希望花费大约一个小时的努力)我才会遇到生成类型的麻烦。 如果你这样做,我建议使用Sigil – 这将使你的IL生成更不令人沮丧。