为什么 XmlSerializer 无法序列化 .Net Core 中的枚举值,但在 .NET Framework 中运行良好

Why does XmlSerializer fail to serialize enum value in .Net Core but works fine in .NET Framework

总结

.NET Core 应用无法 XML 序列化包含枚举值的对象,而 .NET Framework (4.7.2) 成功。这是已知的重大更改吗?如果是,我该如何解决?

代码示例

以下控制台应用程序不会在 .NET Framework 4.7.2 项目中引发异常:

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

.NET Core 3.0 控制台应用程序中完全相同的代码在调用 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.

我的代码是不是做错了什么?这是 .NET Framework 和 .NET Core 之间的重大变化吗?

有解决办法吗?

更新

我应该指出,在 .NET 4.7.2 中进行序列化时,我得到以下(期望的)值输出:

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

我希望为 .NET Core 提出的任何解决方案也能输出相同的 XML,因为我需要保持与现有文件和未使用 .NET 标准的应用程序的旧版本的兼容性.

更新 2

我应该在最初的问题中包含这些信息,但现在我正在尝试实现一个答案,我发现有一些我一开始没有想到的要求。

首先,被序列化的对象也在逻辑上被使用,逻辑依赖于存储在值中的对象是一个枚举。因此,将值永久转换为整数(例如通过转换为 setter)将影响应用程序的逻辑,所以这是我不能做的事情。

其次,尽管我的示例已被简化以显示 .NET Framework 和 .NET Core 之间的区别,但实际应用程序使用了许多枚举。因此,解决方案应该允许使用多个枚举值。

嗯,不知道为什么会这样不同。但我有如下解决方法:


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>

编辑: 我没有注意到序列化为 <Value>One</Value> 的值这个解决方法比以前更脏,但它有效。

Fiddle

编辑 2: 正如@Victor Chelaru 在评论中提到的,我决定保留这两种解决方法,但必须声明它们都有相同的缺点,即丢失枚举类型信息序列化 xml 输出。

[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

编辑 3: 正如@Simon Mourier 在上面的评论中提到的解决方法可以在不直接修改枚举的情况下使用 XmlAttributeOverrides 实现,如下所示:

[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

此重大更改是由于 .NET Core 和 .NET Framework 在 XmlSerializationWriter.WriteTypedPrimitive(string name, string ns, object o, bool xsiType) 中的实现差异所致。

这个可以看下面两个demo fiddles:

  1. .NET Core 3.1.0,抛出异常如下:

    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,序列化一个new ValueContainer { Value = MyEnum.One }如下:

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

    请注意,在生成 XML 时,不包括有关 Value 中存在的特定 enum 类型的信息,而是仅包含基础类型 intxsi:type 属性中显示。

那么,差异出现在哪里呢?完整框架参考源码可见here,开头为:

    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;

鉴于传入的object o其实是一个盒装的Enum.One,那么Type.GetTypeCode(Type type) returns a TypeCode适合枚举的底层类型,这里TypeCode.Int32,从而成功序列化您的值。

当前的 .Net 核心参考源是 here,表面上看起来很相似:

    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;

但是等等 - 这个方法是什么 t.GetTypeCode()Type 上没有实例方法 GetTypeCode() 所以它一定是某种扩展方法。但是哪里?快速搜索参考源至少发现三种不同的、不一致的 public static TypeCode GetTypeCode(this Type type) 方法:

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

    因为 System.Xml.SerializationXmlSerializationWriter 的命名空间,所以我相信这是被使用的。 它不调用 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;
        }
    }
    

    因此,当传递 enum 类型时,将返回 TypeCode.Object

System.Type.GetTypeCode(Type t) 替换为 System.Xml.Serialization.TypeExtensionMethods.GetTypeCode(this Type type) 是导致序列化失败的重大更改。

所有这些都引出了一个问题,这个重大更改是错误还是错误修复?

XmlSerializer 是为可序列化对象的往返而设计的:它通常拒绝序列化任何它也不能在不丢失数据的情况下反序列化的类型。但在您的情况下,数据正在丢失,因为 enum 值正在退化为整数值。所以这种行为改变可能是有意的。不过,您可以打开一个问题 here 询问重大更改是否是故意的。

为了避免异常,您应该正确声明所有预期的 enum 类型(和其他类型),在 ValueContainer 上具有 [XmlInclude(typeof(TEnum))] 属性:

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

这是使用 XmlSerializer 序列化多态成员的预期方式,并确保类型信息是双向的。它适用于 .NET Core 和 .NET Full Framework。相关问题见 and Using XmlSerializer to serialize derived classes.

演示 fiddle #3 here.

by Eldar 中建议的解决方法也可以避免异常,但将 enum 转换为 int 会导致类型信息丢失。