9

Summary

.NET Core apps fail to XML serialize an object which contains an enum value, while .NET Framework (4.7.2) succeeds. Is this a known breaking change, and if so, how can I work around it?

Code Example

The following console application does not throw an exception in .NET Framework 4.7.2 project:

public enum MyEnum
{
    One,
}

public class ValueContainer
{
    public object Value;
}
class Program
{
    static void Main(string[] args)
    {
        XmlSerializer newSerializer = XmlSerializer.FromTypes(
            new[] { typeof(ValueContainer)})[0];

        var instance = new ValueContainer();
        instance.Value = MyEnum.One;

        using (var memoryStream = new MemoryStream())
        {
            newSerializer.Serialize(memoryStream, instance);
        }
    }
}

The exact same code in a .NET Core 3.0 Console Application throws the following exception when calling Serialize:

System.InvalidOperationException
  HResult=0x80131509
  Message=There was an error generating the XML document.
  Source=System.Private.Xml
  StackTrace:
   at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id)
   at System.Xml.Serialization.XmlSerializer.Serialize(Stream stream, Object o, XmlSerializerNamespaces namespaces)
   at System.Xml.Serialization.XmlSerializer.Serialize(Stream stream, Object o)
   at CoreXml.Program.Main(String[] args) in C:\Users\vchel\source\repos\CoreXml\CoreXml\Program.cs:line 28

Inner Exception 1:
InvalidOperationException: The type CoreXml.MyEnum may not be used in this context.

Am I doing something wrong in my code? Is this a breaking change between .NET Framework and .NET Core?

Is there a workaround?

Update

I should have pointed out that when serializing in .NET 4.7.2, I get the following (desired) output for Value:

 <Value xsi:type="xsd:int">0</Value>

I would like whatever solution is proposed for .NET Core to also output the same XML, as I need to maintain compatibility with existing files and older versions of the app which aren't using .NET Standard.

Update 2

I should have included this information in the original question, but now that I'm attempting to implement an answer, I see that there are a few requirements I didn't think of at first.

First, the object which is being serialized is also being used in logic, and the logic depends on the object stored in the value being an enumeration. Therefore, converting the value permanently to an integer (such as by casting in a setter) will impact the logic of the application so it's something I can't do.

Second, even though my example has been simplified to show a difference between .NET Framework and .NET Core, the real application uses many enumerations. Therefore, the solution should allow multiple enumeration values to be used.

dbc
  • 104,963
  • 20
  • 228
  • 340
Victor Chelaru
  • 4,491
  • 3
  • 35
  • 49
  • This does appear to be a bug or at least a breaking change between Framework and Core. The way that the XmlSerializer implementations check object types internally is quite different. – yaakov Dec 30 '19 at 05:08

2 Answers2

4

This breaking change is due to a difference in implementations in XmlSerializationWriter.WriteTypedPrimitive(string name, string ns, object o, bool xsiType) between .NET Core and .NET Framework.

This can be seen in the following two demo fiddles:

  1. .NET Core 3.1.0, which throws an exception as follows:

    System.InvalidOperationException: There was an error generating the XML document.
    ---> System.InvalidOperationException: The type MyEnum may not be used in this context.
    at System.Xml.Serialization.XmlSerializationWriter.WriteTypedPrimitive(String name, String ns, Object o, Boolean xsiType)
    
  2. .NET Framework 4.7.3460.0, which serializes a new ValueContainer { Value = MyEnum.One } as follows:

    <ValueContainer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
      <Value xsi:type="xsd:int">0</Value>
    </ValueContainer>
    

    Note that, while XML is generated, information about the specific enum type present in Value is not included, and instead only the underlying type int is shown in the xsi:type attribute.

So, where does the difference arise? The full framework reference source can be seen here, and begins:

    protected void WriteTypedPrimitive(string name, string ns, object o, bool xsiType) {
        string value = null;
        string type;
        string typeNs = XmlSchema.Namespace;
        bool writeRaw = true;
        bool writeDirect = false;
        Type t = o.GetType();
        bool wroteStartElement = false;

        switch (Type.GetTypeCode(t)) {
        case TypeCode.String:
            value = (string)o;
            type = "string";
            writeRaw = false;
            break;
        case TypeCode.Int32:
            value = XmlConvert.ToString((int)o);
            type = "int";
            break;

Given that the incoming object o is actually a boxed Enum.One, then Type.GetTypeCode(Type type) returns a TypeCode appropriate for the underlying type of the enum, here TypeCode.Int32, thus serializing your value successfully.

The current .Net core reference source is here and looks superficially similar:

    protected void WriteTypedPrimitive(string name, string ns, object o, bool xsiType)
    {
        string value = null;
        string type;
        string typeNs = XmlSchema.Namespace;
        bool writeRaw = true;
        bool writeDirect = false;
        Type t = o.GetType();
        bool wroteStartElement = false;

        switch (t.GetTypeCode())
        {
            case TypeCode.String:
                value = (string)o;
                type = "string";
                writeRaw = false;
                break;
            case TypeCode.Int32:
                value = XmlConvert.ToString((int)o);
                type = "int";
                break;

But wait - what is this method t.GetTypeCode()? There is no instance method GetTypeCode() on Type so it must be some sort of extension method. But where? A quick search of the reference source turned up at least three different, inconsistent public static TypeCode GetTypeCode(this Type type) methods:

  1. System.Runtime.Serialization.TypeExtensionMethods.GetTypeCode(this Type type).

  2. System.Dynamic.Utils.TypeExtensions.GetTypeCode(this Type type).

  3. System.Xml.Serialization.TypeExtensionMethods.GetTypeCode(this Type type).

    Since System.Xml.Serialization is the namespace of XmlSerializationWriter I believe that this is the one used. And it doesn't call Type.GetTypeCode():

    public static TypeCode GetTypeCode(this Type type)
    {
        if (type == null)
        {
            return TypeCode.Empty;
        }
        else if (type == typeof(bool))
        {
            return TypeCode.Boolean;
        }
        else if (type == typeof(char))
        {
            return TypeCode.Char;
        }
        else if (type == typeof(sbyte))
        {
            return TypeCode.SByte;
        }
        else if (type == typeof(byte))
        {
            return TypeCode.Byte;
        }
        else if (type == typeof(short))
        {
            return TypeCode.Int16;
        }
        else if (type == typeof(ushort))
        {
            return TypeCode.UInt16;
        }
        else if (type == typeof(int))
        {
            return TypeCode.Int32;
        }
        else if (type == typeof(uint))
        {
            return TypeCode.UInt32;
        }
        else if (type == typeof(long))
        {
            return TypeCode.Int64;
        }
        else if (type == typeof(ulong))
        {
            return TypeCode.UInt64;
        }
        else if (type == typeof(float))
        {
            return TypeCode.Single;
        }
        else if (type == typeof(double))
        {
            return TypeCode.Double;
        }
        else if (type == typeof(decimal))
        {
            return TypeCode.Decimal;
        }
        else if (type == typeof(DateTime))
        {
            return TypeCode.DateTime;
        }
        else if (type == typeof(string))
        {
            return TypeCode.String;
        }
        else
        {
            return TypeCode.Object;
        }
    }
    

    Thus when passed an enum type, TypeCode.Object will be returned.

The replacement of System.Type.GetTypeCode(Type t) with System.Xml.Serialization.TypeExtensionMethods.GetTypeCode(this Type type) is the breaking change that is causing your serialization failure.

All this begs the question, is this breaking change a bug, or a bug fix?

XmlSerializer is designed for round-tripping of serializable objects: it generally refuses to serialize any type that it cannot also deserialize without data loss. But in your case, data is being lost, as enum values are getting degraded into integer values. So this behavior change may be intentional. Nevertheless, you could open an issue here asking whether the breaking change was intentional.

To avoid the exception, you should properly declare all expected enum types (and other types) with [XmlInclude(typeof(TEnum))] attributes on ValueContainer:

[XmlInclude(typeof(MyEnum)), XmlInclude(typeof(SomeOtherEnum)), XmlInclude(typeof(SomeOtherClass)) /* Include all other expected custom types here*/]
public class ValueContainer
{
    public object Value;
}

This is the intended way to serialize polymorphic members using XmlSerializer, and ensures that type information is round-tripped. It works in both .NET Core and .NET Full Framework. For related questions, see Serializing a class with a generic Enum that can be different Enum types and Using XmlSerializer to serialize derived classes.

Demo fiddle #3 here.

The workarounds suggested in this answer by Eldar also avoid the exception but converting the enum to an int will cause loss of type information.

dbc
  • 104,963
  • 20
  • 228
  • 340
1

Well, not knowing the reason why it differs like this. But i have a workaround like below :


public enum MyEnum
{        
   One,
}

public class ValueContainer
    {
        [XmlIgnore]
        private object _value;

        public object Value
        {
            get
            {
                return _value;
            }
            set
            {
                var type = value.GetType();
                _value = type.IsEnum ? (int)value : value;
            }
        }
    }

class Program
{
   static void Main(string[] args)
   {
      var newSerializer = XmlSerializer.FromTypes(
           new[] { typeof(ValueContainer))[0];
           var instance = new ValueContainer();
           instance.Value = MyEnum.One;
           var memoryStream = new MemoryStream();
           newSerializer.Serialize(memoryStream, instance);
           var str = Encoding.Default.GetString(memoryStream.ToArray());
     }
}

Output

<?xml version="1.0"?>
<ValueContainer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Value xsi:type="xsd:int">0</Value>
</ValueContainer>

EDIT : I fail to notice that value serialized as <Value>One</Value> this work around is dirtier than previous but it works.

Fiddle

EDIT 2 : As @Victor Chelaru mentioned in the comments i decided to keep both workarounds but have to state that they both have the same drawback which is loosing type information on enum with serialized xml output.

[XmlType(typeName: "int",Namespace="http://www.w3.org/2001/XMLSchema")]
public enum MyEnum : int
{
    [XmlEnum("0")]
    One,
}

public class ValueContainer
{
    public object Value;
}

public static void Main()
{
    var newSerializer = XmlSerializer.FromTypes(new[]{typeof(ValueContainer), typeof(MyEnum)})[0];
    var instance = new ValueContainer();
    instance.Value = MyEnum.One;
    var memoryStream = new MemoryStream();
    newSerializer.Serialize(memoryStream, instance);
    var str = Encoding.Default.GetString(memoryStream.ToArray());
    str.Dump();
}

Fiddle

Edit 3: As @Simon Mourier mentioned in the comments above workaround can be achieved without modifying enum directly with usage of XmlAttributeOverrides as below :

[XmlType(typeName: "int")]
public enum MyEnum : int
{       
    One,
}

public class ValueContainer
{
    public object Value;
}

public static void Main()
{               
    var ov = new XmlAttributeOverrides(); 
    ov.Add(typeof(MyEnum), nameof(MyEnum.One), new XmlAttributes { XmlEnum = new XmlEnumAttribute("0") }); 
    var newSerializer = new XmlSerializer(typeof(ValueContainer), ov, new[] { typeof(MyEnum) }, null, null);
    var instance = new ValueContainer();
    instance.Value = MyEnum.One;
    var memoryStream = new MemoryStream();
    newSerializer.Serialize(memoryStream, instance);
    var str = Encoding.Default.GetString(memoryStream.ToArray());
    str.Dump();
}

Fiddle

Eldar
  • 9,781
  • 2
  • 10
  • 35
  • This gives the same exception. Have you noticed that `ValueContainer.Value` type is `object`? – Ivan Stoev Dec 29 '19 at 15:41
  • @IvanStoev i copy paste your example and made changes mentioned above. – Eldar Dec 29 '19 at 15:42
  • It's not mine :) But I'm getting the same exception mentioned by OP with or without your modification. – Ivan Stoev Dec 29 '19 at 15:46
  • Notice that this serializes to `<...>One` whereas I'm looking to serialize to `<...>0`. This can be achieved by tagging the One enumeration value with `XmlEnum("0")]` – Victor Chelaru Dec 29 '19 at 16:22
  • @VictorChelaru my bad i didn't notice that it is serialized as `One` but i updated with a different workaround see if it helps. – Eldar Dec 29 '19 at 17:16
  • I see the edit that you made; however, the problem is that if you interact with that object in code by assigning an enumeration value, later on you get an integer out instead of the enumeration value, so the "round trip" on the property does not maintain the same behavior. Apologies for being so particular, but this is core code in a fairly large application that has a lot of existing data, so keeping the functionality the same is important. I think your previous answer was better, it just needed the [XmlEnum] attribute. – Victor Chelaru Dec 29 '19 at 18:08
  • I tought you might want to avoid marking every enum member with `[XmlEnum]` but sure i can revert it back. But since your property is object type and your xml will have `` either way it will be converted to `int` and if you want the enum you have to boxed it with correct enum type. – Eldar Dec 29 '19 at 18:18
  • 1
    You can avoid modifying the original source using XmlAttributeOverrides, something like this: `var ov = new XmlAttributeOverrides(); ov.Add(typeof(MyEnum), nameof(MyEnum.One), new XmlAttributes { XmlEnum = new XmlEnumAttribute("0") }); var serializer = new XmlSerializer(typeof(ValueContainer), ov, new[] { typeof(MyEnum) }, null, null);` + code for XmType as wall – Simon Mourier Dec 30 '19 at 09:43
  • Thanks for all of your help on this. It *almost* works, but the last problem I can see is if you use multiple enumerations, the serializer gets confused if you give them all the type of "int". I think this can be solved by giving them different namespaces like `[XmlType(typeName: "int", Namespace = "a")]`, `[XmlType(typeName: "int", Namespace = "b")]` . Real-world examples may need multiple enumerations so that's probably worth including in the answer. – Victor Chelaru Dec 30 '19 at 14:13
  • @VictorChelaru you are right about namespace collision but you have issues with the deserialization process. Assigning namespace `http://www.w3.org/2001/XMLSchema` with type `int` tells deserializer convert values that reside as `int` to specified enum value. If you don't have that namespace on your enum it wont be converted as enum but int. If you define different namespaces, your existing data cant be converted into enums and the next serializations wont have backwards compatibility. You can produce same format with dynamic usage of solution 3 but in deserialization part i have no idea – Eldar Dec 30 '19 at 19:42
  • @VictorChelaru with your requirements saving enums as integers may not be feasible if your existing data consists of this format : `0`. As i said you may use solution 3 dynamically setting one enum to be deserialized as int but you wont be knowing which enum is stored in xml. – Eldar Dec 30 '19 at 19:48
  • In my case I handle that by also storing the enum type as a property on the object (again omitted from the example above). That was necessary whether I was using .NET Framework or Core to deserialize the values as enums. – Victor Chelaru Dec 30 '19 at 22:23
  • If you construct an `XmlSerializer` using the non-default constructor `new XmlSerializer(typeof(ValueContainer), ov, new[] { typeof(MyEnum) }, null, null);` **you must statically cache and reuse it to avoid a severe memory leak**. For why see [Memory Leak using StreamReader and XmlSerializer](https://stackoverflow.com/a/23897411/3744182). – dbc Dec 31 '19 at 00:06