Home > Software design >  Xml deserialization to Object, Reflection to Type
Xml deserialization to Object, Reflection to Type

Time:11-05

I'm using Xml to store settings for an application, which are changed during runtime and serialized and deserialized multiple times during application execution.

There is an Xml element which could hold any serializable type, and should be serialized from and deserialized to a property of type Object, i.e.

[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>

I deserialize the Xml and parse the properties to find an instrument object by "InstrumentName", and that instrument will have a property named the same as the Xml attribute "Property", and my intent is to set that instrument.property = the Value element in the xml. It's trivial to convert an object using Reflection such as (in 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)

right, that works if point.Value is an object, however it is not. What gets serialized from the object turns out to be a string. In the case of when the property is a Double, we get

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

yields "3", and when a System.Drawing.Rectangle,

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

yields "{X=145,Y=114,Width=160,Height=75}"

So is there a way to convert the Xml representation of the value type or object, directly into the .NET equivalent?

(Or must I use the Reflection / System.Activator to instantiate the object manually and convert (in the case of primitives) or string parse the properties and values (in the case of non-primitives)?)

CodePudding user response:

Well, it went too far but I have managed to solve this issue I think. But the solution is not that pretty. Because it includes heavy usage of reflection. (IL Emit)

I have built a dynamic type builder that extends SetpointPoint and overrides the Value property so that you can set the custom attributes I mentioned in the comments. It looks like below :

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();
    }
}

A little modification to your class is to make the Value property virtual so that in the dynamic type we can override it.

[XmlElement]
public virtual object Value { get; set; }

What does DynamicTypeBuilder do is simply generate a class on the fly like this :

public class SetpointPoint_Double : SetpointPoint
{
    [XmlElement(typeof(double))]
    public override object Value { get; set; }
}

We also need a root class that contains our Point classes:

[Serializable]
public class Root
{
    [XmlElement("Point")]
    public SetpointPoint Point { get; set; }
}

And this is how we test our code :

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);
    }
}

CodePudding user response:

I decided to allow the serializer to serialize the classes (in the case of Rectangle it is a Struct) as a string with pairs of property name and value, as shown i.e. {X=145,Y=114,Width=160,Height=75}, and the primitives as values i.e. 3.

Then parse this Xml representation into pairs which can be iterated, and set properties and fields accordingly. Some maneuvering had to be done with boxing structs because their underlying type appears to not be recognized when boxed, so the solution in vb was to use Dim boxed As ValueType (credit to 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

I have not yet tried this on XmlSerializable classes (instead of structs) but that is in the works.

  • Related