Xml 反序列化到对象,反射到类型

Posted

技术标签:

【中文标题】Xml 反序列化到对象,反射到类型【英文标题】:Xml deserialization to Object, Reflection to Type 【发布时间】:2021-12-18 04:51:09 【问题描述】:

我正在使用 Xml 存储应用程序的设置,这些设置在运行时更改,并在应用程序执行期间多次序列化和反序列化。

有一个 Xml 元素可以保存任何可序列化的类型,并且应该从 Object 类型的属性进行序列化和反序列化,即

[Serializable]
public class SetpointPoint

    [XmlAttribute]
    public string InstrumentName  get; set; 
    [XmlAttribute]
    public string Property  get; set; 
    [XmlElement]
    public object Value  get; set; 

 // (not comprehensive, only important properties displayed)

Xml,

<?xml version="1.0" encoding="utf-8"?>
<StationSetpoints xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:schemaLocation="http://www.w3schools.com StationSetpoints.xsd">
  <Setpoint PartNumber="107983">
    <Point InstrumentName="PD Stage" Property="SetPoint">
      <Value xsi:type="xsd:string">3</Value>
    </Point>
    <Point InstrumentName="TR Camera" Property="MeasurementRectangle" StationSetpointMemberType="Property">
      <Value xsi:type="xsd:string">X=145,Y=114,Width=160,Height=75</Value>
    </Point>
  </Setpoint>
</StationSetpoints>

我反序列化 Xml 并解析属性以通过“InstrumentName”查找仪器对象,并且该仪器将具有与 Xml 属性“Property”同名的属性,我的意图是设置该 instrument.property = xml 中的 Value 元素。使用反射转换对象很简单,例如(在 vb.net 中)

Dim ii = InstrumentLoader.Factory.GetNamed(point.InstrumentName)
Dim pi = ii.GetType().GetProperty(point.Property)
Dim tt = pi.PropertyType
Dim vt = Convert.ChangeType(point.Value, tt)
pi.SetValue(ii, vt)

是的,如果 point.Value 是一个对象,它就可以工作,但它不是。从对象中序列化的结果是一个字符串。在属性是 Double 的情况下,我们得到

<Value xsi:type="xsd:string">3</Value>

产生"3",当一个System.Drawing.Rectangle,

<Value xsi:type="xsd:string">X=145,Y=114,Width=160,Height=75</Value>

收益"X=145,Y=114,Width=160,Height=75"

那么有没有办法将值类型或对象的 Xml 表示形式直接转换为 .NET 等效项?

(或者我必须使用 Reflection / System.Activator 手动实例化对象并转换(在基元的情况下)或字符串解析属性和值(在非基元的情况下)?)

【问题讨论】:

您可以使用XmlElement 属性多次注释Value 属性,指定正确的类型,如下所示:[XmlElement(type=typeof(string)),XmlElement(type=typeof(System.Drawing.Rectangle))],然后使用模式匹配来读取值。 XmlSerializer 仅支持静态预先声明的派生类型的序列化。为此,请参阅Using XmlSerializer to serialize derived classes。 @Eldar 我想保留这个开放式以支持其他类型。配置仅在xml中,无需重新编译。我的意思是任何可以用 XML 表示的东西,比如 Rectangle,当然还有原语。 @dbc 我很欣赏这个链接,但看不到它如何适用于我的问题。 那么有没有办法将值类型或对象的 Xml 表示形式直接转换为 .NET 等效项? -- 关键是 XmlSerializer 没有t 支持将多态字段序列化和反序列化为开箱即用的仅在运行时定义的类型。如果您可以静态定义可能的类型,您的生活会更轻松。但如果没有,您将需要做一些棘手的事情,例如在SetpointPoint 上实现IXmlSerializable 并手动执行。 (还有一些像Color这样的类型不能直接被XmlSerializer序列化。) 【参考方案1】:

好吧,它走得太远了,但我认为我已经设法解决了这个问题。但解决方案并不那么漂亮。因为它包括大量使用反射。 (IL 发射)

我已经构建了一个动态类型构建器,它扩展了SetpointPoint 并覆盖了Value 属性,以便您可以设置我在 cmets 中提到的自定义属性。如下所示:

public class DynamicTypeBuilder

    private static readonly MethodAttributes getSetAttr =
        MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName |
            MethodAttributes.HideBySig;

    private static readonly AssemblyName aName = new AssemblyName("DynamicAssemblyExample");
    private static readonly AssemblyBuilder ab =
         AssemblyBuilder.DefineDynamicAssembly(
            aName,
            AssemblyBuilderAccess.Run);
    private static readonly ModuleBuilder mb =
        ab.DefineDynamicModule(aName.Name + ".dll");

    public Type BuildCustomPoint(Type valueType)
    
        var tb = mb.DefineType(
            "SetpointPoint_" + valueType.Name,
             TypeAttributes.Public, typeof(SetpointPoint));

        var propertyBuilder = tb.DefineProperty("Value",
                                                       PropertyAttributes.HasDefault,
                                                       typeof(object),
                                                       null);
        var fieldBuilder = tb.DefineField("_value",
                                                   typeof(object),
                                                   FieldAttributes.Private);
        var getBuilder =
      tb.DefineMethod("get_Value",
                                 getSetAttr,
                                 typeof(object),
                                 Type.EmptyTypes);

        var getIL = getBuilder.GetILGenerator();

        getIL.Emit(OpCodes.Ldarg_0);
        getIL.Emit(OpCodes.Ldfld, fieldBuilder);
        getIL.Emit(OpCodes.Ret);

        var setBuilder =
            tb.DefineMethod("set_Value",
                                       getSetAttr,
                                       null,
                                       new Type[]  typeof(object) );

        var setIL = setBuilder.GetILGenerator();

        setIL.Emit(OpCodes.Ldarg_0);
        setIL.Emit(OpCodes.Ldarg_1);
        setIL.Emit(OpCodes.Stfld, fieldBuilder);
        setIL.Emit(OpCodes.Ret);

        // Last, we must map the two methods created above to our PropertyBuilder to
        // their corresponding behaviors, "get" and "set" respectively.
        propertyBuilder.SetGetMethod(getBuilder);
        propertyBuilder.SetSetMethod(setBuilder);

        var xmlElemCtor = typeof(XmlElementAttribute).GetConstructor(new[]  typeof(Type) );
        var attributeBuilder = new CustomAttributeBuilder(xmlElemCtor, new[]  valueType );
        propertyBuilder.SetCustomAttribute(attributeBuilder);

        return tb.CreateType();
    

对您的类稍作修改就是将Value 属性设为虚拟,以便在动态类型中我们可以覆盖它。

[XmlElement]
public virtual object Value  get; set; 

DynamicTypeBuilder 所做的只是像这样动态生成一个类:

public class SetpointPoint_Double : SetpointPoint

    [XmlElement(typeof(double))]
    public override object Value  get; set; 


我们还需要一个包含 Point 类的根类:

[Serializable]
public class Root

    [XmlElement("Point")]
    public SetpointPoint Point  get; set; 

这就是我们测试代码的方式:

var builder = new DynamicTypeBuilder();
var doublePoint = builder.BuildCustomPoint(typeof(double));
var pointPoint = builder.BuildCustomPoint(typeof(Point));
var rootType = typeof(Root);
var root = new Root();
var root2 = new Root();
var instance1 = (SetpointPoint)Activator.CreateInstance(doublePoint);
var instance2 = (SetpointPoint)Activator.CreateInstance(pointPoint);

instance1.Value = 1.2;
instance2.Value = new Point(3, 5);

root.Point = instance1;
root2.Point = instance2;

// specifying used types here as the second parameter is crucial
// DynamicTypeBuilder can also expose a property for derived types.
var serialzer = new XmlSerializer(rootType, new[]  doublePoint, pointPoint );
TextWriter textWriter = new StringWriter();
serialzer.Serialize(textWriter, root);
var r = textWriter.ToString();
/*
 output :
<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <Point xsi:type="SetpointPoint_Double">
      <Value xsi:type="xsd:double">1.2</Value>
   </Point>
</Root>
 */
textWriter.Dispose();
textWriter = new StringWriter();
serialzer.Serialize(textWriter, root2);

var x = textWriter.ToString();
/*
 output 
<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <Point xsi:type="SetpointPoint_Point">
      <Value xsi:type="Point">
         <X>3</X>
         <Y>5</Y>
      </Value>
   </Point>
</Root>
 */

var d = (Root)serialzer.Deserialize(new StringReader(x));
var d2 = (Root)serialzer.Deserialize(new StringReader(r));

PrintTheValue(d);
PrintTheValue(d2);

void PrintTheValue(Root r)

    // you can use reflection here
    if (r.Point.Value is Point p)
    
        Console.WriteLine(p.X);
    
    else if (r.Point.Value is double db)
    
        Console.WriteLine(db);
    

【讨论】:

我非常感谢这项工作,但我想使用非常松散的类型,当想要反序列化不同类型时必须重新编译此解决方案,例如bool,好的,然后说写支持适用于所有原语,但也适用于 RectangleLine 等类型,因此在我需要的程度上,这不可能是面向未来的。【参考方案2】:

我决定允许序列化程序将类(在 Rectangle 的情况下它是一个结构)序列化为具有属性名称和值对的字符串,如图所示 X=145,Y=114,Width=160,Height=75 和原语作为值,即 @987654323 @。

然后将这个 Xml 表示解析成可以迭代的对,并相应地设置属性和字段。必须对装箱结构进行一些操作,因为装箱时它们的底层类型似乎无法识别,因此 vb 中的解决方案是使用Dim boxed As ValueType(归功于this comment)

Dim ii = InstrumentLoader.Factory.GetNamed(point.InstrumentName)
Dim pi = ii.GetType().GetProperty(point.Property)
Dim tt = pi.PropertyType
If valueString.StartsWith("") Then
    Dim instance = CTypeDynamic(Activator.CreateInstance(tt), tt)
    Dim instanceType = instance.GetType()
    Dim boxed As ValueType = CType(instance, ValueType)
    Dim propertiesAndValues =
        valueString.Replace("", "").Replace("", "").Split(","c).
        ToDictionary(Function(s) s.Split("="c)(0), Function(s) s.Split("="c)(1))
    For Each p In instanceType.GetProperties()
        If propertiesAndValues.ContainsKey(p.Name) Then
            Dim t = p.PropertyType
            Dim v = Convert.ChangeType(propertiesAndValues(p.Name), t)
            p.SetValue(boxed, v, Nothing)
        End If
    Next
    For Each f In instanceType.GetFields()
        If propertiesAndValues.ContainsKey(f.Name) Then
            Dim t = f.FieldType
            Dim v = Convert.ChangeType(propertiesAndValues(f.Name), t)
            f.SetValue(boxed, v)
        End If
    Next
    pi.SetValue(ii, boxed)
Else
    Dim vt1 = Convert.ChangeType(valueString, tt)
    pi.SetValue(ii, vt1)
End If

我还没有在 XmlSerializable 类(而不是结构)上尝试过这个,但这是在工作中。

【讨论】:

以上是关于Xml 反序列化到对象,反射到类型的主要内容,如果未能解决你的问题,请参考以下文章

概述反射和序列化?

使用 C# Xml.Serialization 库反序列化每个重复的 XML 节点并将其映射到对象的属性

Xml 反序列化 - 将两个元素合并到一个 List<T> 对象中

序列化与反序列化之JSON

如何从 nusoap 服务返回的 XML 反序列化对象?

将 Web 服务调用的结果反序列化到子元素类型