3

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)?)

djv
  • 15,168
  • 7
  • 48
  • 72
  • 1
    You can annotate `Value` property multiple times with `XmlElement` attribute specifying correct types like this: `[XmlElement(type=typeof(string)),XmlElement(type=typeof(System.Drawing.Rectangle))]` and then use pattern matching to read the value. – Eldar Nov 03 '21 at 20:43
  • `XmlSerializer` only supports serialization of derived types when they are statically pre-declared. To do that, see [Using XmlSerializer to serialize derived classes](https://stackoverflow.com/q/1643139/3744182). – dbc Nov 03 '21 at 22:08
  • @Eldar I want to leave this open-ended to support other types. The configuration being only in Xml, without the need to recompile. I really mean anything which can be represented in Xml like the Rectangle, and of course primitives. – djv Nov 04 '21 at 03:14
  • @dbc I appreciate the link, but don't see how that applies to my problem. – djv Nov 04 '21 at 03:16
  • *So is there a way to convert the Xml representation of the value type or object, directly into the .NET equivalent?* -- The point is that `XmlSerializer` doesn't provide support for serialization and deserialization of polymorphic fields to types defined only in runtime out of the box. If you can define the possible types statically, your life will be easier. But if not, you will need to do something tricky like implement `IXmlSerializable` on `SetpointPoint` and do it manually. (Also some types like `Color` can't be serialized directly by `XmlSerializer`.) – dbc Nov 04 '21 at 03:29
  • @dbc I haven't tried `Color` but I can live with it not being serializable, as I have *an Xml element which could hold any serializable type* so primitives and nice things like Rectangles and Points, etc. and other things which may work in the future. My codebase has instruments with many properties of different types and I want to leave it open to support any of those properties as long as they are xmlserializable, without recompiling. I know some types are not xmlserializable such as some (all?) Forms Controls and I can live without them... – djv Nov 04 '21 at 04:23
  • @dbc Given the instrumentname, I get an object of a class, and the property name, I can get that PropertyInfo, so I know the type that this string needs to be converted to. It's unfortunate that it comes in a string, because if it's an object at this point it's trivial further using Reflection. I think the best approach if there is nothing stock to convert to the string representation to an object is to use reflection, and IXmlSerializable may force a nice pattern, but I am not interested in polymorphism here per se. – djv Nov 04 '21 at 04:28
  • I think the immediate issue is that the way the serialization is working, it just dumps a `ToString` representation of the object, where you might really prefer that it serialized according to the runtime type. I'm not that familiar with the details of the built-in XML serialization, but it's not that difficult to roll your own, I did so in a day or two to get XML serialization into `XElement`-derived types instead of the built-in `XmlElement` types. – Craig Nov 04 '21 at 15:07

2 Answers2

2

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);
    }
}
Eldar
  • 9,781
  • 2
  • 10
  • 35
  • I really appreciate the work, but I want to use very loose typing, and this solution must be recompiled when one would want to deserialize a different type, such as `bool`, ok then say write support for all primitives, but also types such as `Rectangle` or `Line`, etc. so this can't be future-proof to the degree I need it to be. – djv Nov 04 '21 at 15:29
1

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.

djv
  • 15,168
  • 7
  • 48
  • 72