【问题标题】:How can I serialize an object to C# object initializer code?如何将对象序列化为 C# 对象初始化程序代码?
【发布时间】:2011-08-13 05:25:30
【问题描述】:

我希望获取内存中的对象(或对象的 JSON 序列化)并发出 C# 代码以生成等效对象。

这对于从存储库中提取已知良好的示例以用作单元测试的起点非常有用。我们考虑过对 JSON 进行反序列化,但 C# 代码在重构方面会有优势。

【问题讨论】:

  • 我假设你不能使用 xml serlizer 是有原因的。
  • 当然可以,我们也可以。但是代码比 XML 更可取,原因与我提到的关于 JSON 的原因相同。轻松重构。

标签: c# unit-testing code-generation


【解决方案1】:

对象可能有一个支持转换为InstanceDescriptor 的 TypeConverter,这是 WinForms 设计者在发出 C# 代码生成对象时使用的。如果它无法转换为 InstanceDescriptor,它将尝试使用无参数构造函数并简单地设置公共属性。 InstanceDescriptor 机制很方便,因为它允许您指定各种构造选项,例如带参数的构造函数,甚至是静态工厂方法调用。

我编写了一些实用程序代码,它使用 IL 发出内存中对象的加载,它基本上遵循上述模式(如果可能,使用 InstanceDescriptor,如果没有,只需编写公共属性。)请注意,这将仅当正确实现 InstanceDescriptor 或设置公共属性足以恢复对象状态时才生成等效对象。如果你发出 IL,你也可以直接作弊和读/写字段值(这是 DataContractSerializer 支持的),但是有很多讨厌的极端情况需要考虑。

【讨论】:

  • 更多关于如何做到这一点的细节会很棒。我对没有自定义类型转换器特别感兴趣
  • 详细说明哪一部分怎么做?
  • 我没有从 InstanceDescriptor(只是 IL)生成 C# 代码,但基本概念相当简单。 InstanceDescriptor 描述了构造函数、静态工厂方法/属性或静态字段,可用于获取初始实例。它还指定是否完全指定对象状态;如果没有,您需要设置单独的属性(通常只是公共设置器属性),通常使用一些元数据来确定该值是否为非默认值(无论如何,这就是 WinForms 设计器所做的。)
【解决方案2】:

如果您的模型很简单,您可以使用反射和字符串生成器直接输出 C#。我这样做是为了完全按照您的讨论填充单元测试数据。

下面的代码示例是在几分钟内编写的,并生成了一个需要手动调整的对象初始化程序。如果您打算经常这样做,可以编写一个更健壮/错误更少的函数。

第二个函数是递归的,遍历对象中的任何列表,并为它们生成代码。

免责声明:这适用于具有基本数据类型的简单模型。它生成了需要清理的代码,但让我可以快速前进。这里只是作为如何做到这一点的一个例子。希望它能激发人们编写自己的灵感。

就我而言,我有一个从数据库加载的大型数据集(结果)的实例。为了从我的单元测试中删除对数据库的依赖,我将对象交给了这个函数,它会吐出允许我在测试类中模拟对象的代码。

    private void WriteInstanciationCodeFromObject(IList results)
    {

        //declare the object that will eventually house C# initialization code for this class
        var testMockObject = new System.Text.StringBuilder();

        //start building code for this object
        ConstructAndFillProperties(testMockObject, results);

        var codeOutput = testMockObject.ToString();
    }


    private void ConstructAndFillProperties(StringBuilder testMockObject, IList results)
    {

        testMockObject.AppendLine("var testMock = new " + results.GetType().ToString() + "();");

        foreach (object obj in results)
        {

            //if this object is a list, write code for its contents

            if (obj.GetType().GetInterfaces().Contains(typeof(IList)))
            {
                ConstructAndFillProperties(testMockObject, (IList)obj);
            }

            testMockObject.AppendLine("testMock.Add(new " + obj.GetType().Name + "() {");

            foreach (var property in obj.GetType().GetProperties())
            {

               //if this property is a list, write code for its contents
                if (property.PropertyType.GetInterfaces().Contains(typeof(IList)))
                {
                    ConstructAndFillProperties(testMockObject, (IList)property.GetValue(obj, null));
                }

                testMockObject.AppendLine(property.Name + " = (" + property.PropertyType + ")\"" + property.GetValue(obj, null) + "\",");
            }

            testMockObject.AppendLine("});");
        }
    }

【讨论】:

    【解决方案3】:

    有一个类似于what Evan proposed 的解决方案,但更适合我的特定任务。

    在玩了一下 CodeDOM 和 Reflection 之后,我发现这对我来说太复杂了。

    对象被序列化为 XML,因此自然的解决方案是使用 XSLT 将其简单地转换为对象创建表达式。

    当然,它仅涵盖某些类型的案例,但可能适用于其他人。

    【讨论】:

      【解决方案4】:

      我也是这方面的新手,但我还需要获取一个定义层次结构的 C# 对象并将其提取到对象初始化程序中,以简化单元测试的设置。我从上面借了很多钱,最后得到了这个。我想改进它处理识别用户类的方式。

      http://github.com/jefflomax/csharp-object-to-object-literal/blob/master/Program.cs

      using System;
      using System.Collections;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
      using System.Threading.Tasks;
      
      namespace ObjectInitializer
      {
          public class Program
          {
              public enum Color { Red, Green, Blue, Yellow, Fidget } ;
      
              public class Foo
              {
                  public int FooId { get; set; }
                  public string FooName { get; set; }
              }
      
              public class Thing
              {
                  public int ThingId { get; set; }
                  public string ThingName { get; set; }
                  public List<Foo> Foos { get; set; }
              }
      
              public class Widget
              {
                  public long Sort { get; set; }
                  public char FirstLetter { get; set; }
              }
      
              public class TestMe
              {
                  public Color Color { get; set; }
                  public long Key { get; set; }
                  public string Name { get; set; }
                  public DateTime Created { get; set; }
                  public DateTime? NCreated { get; set; }
                  public bool Deleted { get; set; }
                  public bool? NDeleted { get; set; }
                  public double Amount { get; set; }
                  public Thing MyThing { get; set; }
                  public List<Thing> Things { get; set; }
                  public List<Widget> Widgets { get; set; }
              }
      
              static void Main(string[] args)
              {
                  var testMe = new TestMe
                  {
                      Color = Program.Color.Blue,
                      Key = 3,
                      Name = "SAK",
                      Created = new DateTime(2013,10,20,8,0,0),
                      NCreated = (DateTime?)null,
                      Deleted = false,
                      NDeleted = null,
                      Amount = 13.1313,
                      MyThing = new Thing(){ThingId=1,ThingName="Thing 1"},
                      Things = new List<Thing>
                      {
                          new Thing
                          {
                              ThingId=4,
                              ThingName="Thing 4",
                              Foos = new List<Foo>
                              {
                                  new Foo{FooId=1, FooName="Foo 1"},
                                  new Foo{FooId=2,FooName="Foo2"}
                              }
                          },
                          new Thing
                          {
                              ThingId=5,
                              ThingName="Thing 5",
                              Foos = new List<Foo>()
                          }
                      },
                      Widgets = new List<Widget>()
                  };
      
                  var objectInitializer = ToObjectInitializer(testMe);
                  Console.WriteLine(objectInitializer);
      
                  // This is the returned C# Object Initializer
                  var x = new TestMe { Color = Program.Color.Blue, Key = 3, Name = "SAK", Created = new DateTime(2013, 10, 20, 8, 0, 0), NCreated = null, Deleted = false, NDeleted = null, Amount = 13.1313, MyThing = new Thing { ThingId = 1, ThingName = "Thing 1", Foos = new List<Foo>() }, Things = new List<Thing> { new Thing { ThingId = 4, ThingName = "Thing 4", Foos = new List<Foo> { new Foo { FooId = 1, FooName = "Foo 1" }, new Foo { FooId = 2, FooName = "Foo2" } } }, new Thing { ThingId = 5, ThingName = "Thing 5", Foos = new List<Foo>() } }, Widgets = new List<Widget>() };
                  Console.WriteLine("");
              }
      
              public static string ToObjectInitializer(Object obj)
              {
                  var sb = new StringBuilder(1024);
      
                  sb.Append("var x = ");
                  sb = WalkObject(obj, sb);
                  sb.Append(";");
      
                  return sb.ToString();
              }
      
              private static StringBuilder WalkObject(Object obj, StringBuilder sb)
              {
                  var properties = obj.GetType().GetProperties();
      
                  var type = obj.GetType();
                  var typeName = type.Name;
                  sb.Append("new " + type.Name + " {");
      
                  bool appendComma = false;
                  DateTime workDt;
                  foreach (var property in properties)
                  {
                      if (appendComma) sb.Append(", ");
                      appendComma = true;
      
                      var pt = property.PropertyType;
                      var name = pt.Name;
      
                      var isList = property.PropertyType.GetInterfaces().Contains(typeof(IList));
      
                      var isClass = property.PropertyType.IsClass;
      
                      if (isList)
                      {
                          IList list = (IList)property.GetValue(obj, null);
                          var listTypeName = property.PropertyType.GetGenericArguments()[0].Name;
      
                          if (list != null && list.Count > 0)
                          {
                              sb.Append(property.Name + " = new List<" + listTypeName + ">{");
                              sb = WalkList( list, sb );
                              sb.Append("}");
                          }
                          else
                          {
                              sb.Append(property.Name + " = new List<" + listTypeName + ">()");
                          }
                      }
                      else if (property.PropertyType.IsEnum)
                      {
                          sb.AppendFormat("{0} = {1}", property.Name, property.GetValue(obj));
                      }
                      else
                      {
                          var value = property.GetValue(obj);
                          var isNullable = pt.IsGenericType && pt.GetGenericTypeDefinition() == typeof(Nullable<>);
                          if (isNullable)
                          {
                              name = pt.GetGenericArguments()[0].Name;
                              if (property.GetValue(obj) == null)
                              {
                                  sb.AppendFormat("{0} = null", property.Name);
                                  continue;
                              }
                          }
      
                          switch (name)
                          {
                              case "Int64":
                              case "Int32":
                              case "Int16":
                              case "Double":
                              case "Float":
                                  sb.AppendFormat("{0} = {1}", property.Name, value);
                                  break;
                              case "Boolean":
                                  sb.AppendFormat("{0} = {1}", property.Name, Convert.ToBoolean(value) == true ? "true" : "false");
                                  break;
                              case "DateTime":
                                  workDt = Convert.ToDateTime(value);
                                  sb.AppendFormat("{0} = new DateTime({1},{2},{3},{4},{5},{6})", property.Name, workDt.Year, workDt.Month, workDt.Day, workDt.Hour, workDt.Minute, workDt.Second);
                                  break;
                              case "String":
                                  sb.AppendFormat("{0} = \"{1}\"", property.Name, value);
                                  break;
                              default:
                                  // Handles all user classes, should likely have a better way
                                  // to detect user class
                                  sb.AppendFormat("{0} = ", property.Name);
                                  WalkObject(property.GetValue(obj), sb);
                                  break;
                          }
                      }
                  }
      
                  sb.Append("}");
      
                  return sb;
              }
      
              private static StringBuilder WalkList(IList list, StringBuilder sb)
              {
                  bool appendComma = false;
                  foreach (object obj in list)
                  {
                      if (appendComma) sb.Append(", ");
                      appendComma = true;
                      WalkObject(obj, sb);
                  }
      
                  return sb;
              }
          }
      }
      

      【讨论】:

      • 很酷!显然有点前卫,但仍然是一个很棒的工具!
      【解决方案5】:

      我在寻找 Matthew 描述的同一种方法时偶然发现了这一点,并受到 Evan 的回答的启发,编写了我自己的扩展方法。它将可编译的 C# 代码生成为可以复制/粘贴到 Visual Studio 中的字符串。我没有打扰任何特定的格式,只是在一行上输出代码并使用 ReSharper 很好地格式化它。我已经将它与我们正在传递的一些大型 DTO 一起使用,到目前为止它就像一个魅力。

      这是扩展方法和几个辅助方法:

      public static string ToCreationMethod(this object o)
      {
          return String.Format("var newObject = {0};", o.CreateObject());
      }
      
      private static StringBuilder CreateObject(this object o)
      {
          var builder = new StringBuilder();
          builder.AppendFormat("new {0} {{ ", o.GetClassName());
      
          foreach (var property in o.GetType().GetProperties())
          {
              var value = property.GetValue(o);
              if (value != null)
              {
                  builder.AppendFormat("{0} = {1}, ", property.Name, value.GetCSharpString());
              }
          }
      
          builder.Append("}");
          return builder;
      }
      
      private static string GetClassName(this object o)
      {
          var type = o.GetType();
      
          if (type.IsGenericType)
          {
              var arg = type.GetGenericArguments().First().Name;
              return type.Name.Replace("`1", string.Format("<{0}>", arg));
          }
      
          return type.Name;
      }
      

      GetCSharpString 方法包含逻辑,它对任何特定类型的扩展都是开放的。对我来说,它处理字符串、整数、小数、日期任何实现 IEnumerable 的东西就足够了:

      private static string GetCSharpString(this object o)
      {
          if (o is String)
          {
              return string.Format("\"{0}\"", o);
          }
          if (o is Int32)
          {
              return string.Format("{0}", o);
          }
          if (o is Decimal)
          {
              return string.Format("{0}m", o);
          }
          if (o is DateTime)
          {
              return string.Format("DateTime.Parse(\"{0}\")", o);
          }
          if (o is IEnumerable)
          {
              return String.Format("new {0} {{ {1}}}", o.GetClassName(), ((IEnumerable)o).GetItems());
          }
      
          return string.Format("{0}", o.CreateObject());
      }
      
      private static string GetItems(this IEnumerable items)
      {
          return items.Cast<object>().Aggregate(string.Empty, (current, item) => current + String.Format("{0}, ", item.GetCSharpString()));
      }
      

      我希望有人觉得这很有用!

      【讨论】:

        【解决方案6】:

        有一个有趣的 Visual Studio 扩展可以解决这个问题; Object Exporter。它允许将内存中的对象序列化为 C# 对象初始化代码、JSON 和 XML。我还没有尝试过,但看起来很有趣;试用后会更新。

        【讨论】:

        • 好主意!!我可以使用诸如Bogus 之类的工具在内存中生成对象。当我想初始化固定对象时,我可以使用 Object Exporter。
        【解决方案7】:

        这是@revlucio 解决方案的更新,增加了对布尔值和枚举的支持。

        public static class ObjectInitializationSerializer
        {
            private static string GetCSharpString(object o)
            {
                if (o is bool)
                {
                    return $"{o.ToString().ToLower()}";
                }
                if (o is string)
                {
                    return $"\"{o}\"";
                }
                if (o is int)
                {
                    return $"{o}";
                }
                if (o is decimal)
                {
                    return $"{o}m";
                }
                if (o is DateTime)
                {
                    return $"DateTime.Parse(\"{o}\")";
                }
                if (o is Enum)
                {
                    return $"{o.GetType().FullName}.{o}";
                }
                if (o is IEnumerable)
                {
                    return $"new {GetClassName(o)} \r\n{{\r\n{GetItems((IEnumerable)o)}}}";
                }
        
                return CreateObject(o).ToString();
            }
        
            private static string GetItems(IEnumerable items)
            {
                return items.Cast<object>().Aggregate(string.Empty, (current, item) => current + $"{GetCSharpString(item)},\r\n");
            }
        
            private static StringBuilder CreateObject(object o)
            {
                var builder = new StringBuilder();
                builder.Append($"new {GetClassName(o)} \r\n{{\r\n");
        
                foreach (var property in o.GetType().GetProperties())
                {
                    var value = property.GetValue(o);
                    if (value != null)
                    {
                        builder.Append($"{property.Name} = {GetCSharpString(value)},\r\n");
                    }
                }
        
                builder.Append("}");
                return builder;
            }
        
            private static string GetClassName(object o)
            {
                var type = o.GetType();
        
                if (type.IsGenericType)
                {
                    var arg = type.GetGenericArguments().First().Name;
                    return type.Name.Replace("`1", $"<{arg}>");
                }
        
                return type.Name;
            }
        
            public static string Serialize(object o)
            {
                return $"var newObject = {CreateObject(o)};";
            }
        }
        

        【讨论】:

          【解决方案8】:

          可能来得有点晚,但这是我在这个问题上的 5cents。

          提到的 Visual Studio 扩展 (OmarElabd/ObjectExporter) 是个好主意,但我需要在运行时,在单元测试执行期间从内存中的对象生成 C# 代码。这是从原始问题演变而来的:https://www.nuget.org/packages/ObjectDumper.NET/

          ObjectDumper.Dump(obj, DumpStyle.CSharp); 从变量返回 C# 初始化代码。如果您发现问题,请告诉我,您可能想在 github 上报告。

          【讨论】:

          • 非常好。我真的没有理由再使用它了,但它总是让我印象深刻,因为它是一个很好的实用程序,特别是用于自动化测试。您是否使用 Roslyn 来执行此操作?
          • 不客气。我主要使用它来生成 C# 测试对象,稍后我将其用作单元测试中的测试数据。它不使用 Roslyn,该库逐个处理每个属性/字段...在打印 C# 初始化程序代码时,每个属性/字段类型都有不同的格式。看看github上的代码:github.com/thomasgalliker/ObjectDumper/blob/master/ObjectDumper/…
          猜你喜欢
          • 2010-10-27
          • 1970-01-01
          • 2011-05-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-10-11
          相关资源
          最近更新 更多