使用 space 反序列化标记的枚举会导致 SerializationException

De-serializing a flagged enum with a space results in SerializationException

当反序列化带有 EnumMemberAttribute 且其值包含 space 的标记枚举时,将抛出 SerializationException。值中的 space 被视为分隔符。

有没有办法更改分隔符或将值放在引号中?或者有更简单的解决方案吗?

我已经在考虑的选项是:

但我真的觉得这应该是可以配置的东西或者其他人已经解决的东西。但是我什么也找不到。

我已将问题归结为一个简单的单元测试。下面的代码导致:

Message=Invalid enum value 'Test' cannot be deserialized into type 'UnitTests.TestEnum'. Ensure that the necessary enum values are present and are marked with EnumMemberAttribute attribute if the type has DataContractAttribute attribute. Source=System.Runtime.Serialization

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Xml;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
    [TestClass]
    public class EnumSerizalizationTests
    {
        [TestMethod]
        public void SerializingAndDesrializingAFlaggedEnumShouldResultInSameEnumValues()
        {
            //Arrange
            var orgObject = new TestClass { Value = TestEnum.TestValue1 | TestEnum.TestValue2 };
            //Act
            var temp = DataContractSerializeObject(orgObject);
            var newObject = DataContractDeSerializeObject<TestClass>(temp);

            //Assert
            newObject.ShouldBeEquivalentTo(orgObject, "Roundtripping serialization should result in same value");
        }

        public string DataContractSerializeObject<T>(T objectToSerialize)
        {
            using (var output = new StringWriter())
            {
                using (var writer = new XmlTextWriter(output) {Formatting = Formatting.Indented})
                {
                    new DataContractSerializer(typeof (T)).WriteObject(writer, objectToSerialize);
                    return output.GetStringBuilder().ToString();
                }
            }
        }

        public T DataContractDeSerializeObject<T>(string stringToDeSerialize)
        {
            DataContractSerializer ser = new DataContractSerializer(typeof(T));
            T result;
            using (StringReader stringReader = new StringReader(stringToDeSerialize))
            {
                using (XmlReader xmlReader = XmlReader.Create(stringReader))
                {
                    result = (T)ser.ReadObject(xmlReader);
                }
            }
            return result;
        }

    }

    [DataContract]
    [KnownType(typeof(TestEnum))]
    public class TestClass
    {
        [DataMember]
        public TestEnum Value { get; set; }
    }

    [Flags]
    [DataContract]
    public enum TestEnum
    {
        [EnumMember(Value = "Test value one")]
        TestValue1 = 1,
        [EnumMember(Value = "Test value two")]
        TestValue2 = 2,
        [EnumMember]
        TestValue3 = 4,
        [EnumMember]
        TestValue4 = 8,
    }


}

您不能在值中使用 space,因为 DataContractSerializer 使用它并且它是硬编码的。参见source and the post。但是如果你真的想在单词之间使用 space,那么使用列出的解决方案之一:

第一种方式。在值中使用其他白色space字符,例如三个字符space。但是你会遇到另一个问题 - 值之间没有视觉分隔符。

<TestClass xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication">
  <Value>Test value one Test value two</Value>
</TestClass>

第二种方式是使用IDataContractSurrogate。这种方式将产生下面列出的 XML:

<TestClass xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication">
  <Value i:type="EnumValueOfTestEnum9cBcd6LT">Test value one, Test value two</Value>
</TestClass>

它是如何工作的?我们将在序列化过程中包装我们的枚举,并在反序列化的情况下解包。为此,我们应该使用 IDataContractSurrogate:

new DataContractSerializerSettings()
{
    DataContractSurrogate = new EnumSurrogate(),
    KnownTypes = new Type[] { typeof(EnumValue<TestEnum>) }
};

public class EnumSurrogate : IDataContractSurrogate
{
    #region IDataContractSurrogate Members

    public object GetCustomDataToExport(Type clrType, Type dataContractType)
    {
        return null;
    }

    public object GetCustomDataToExport(MemberInfo memberInfo, Type dataContractType)
    {
        return null;
    }

    public Type GetDataContractType(Type type)
    {
        return type;
    }

    public object GetDeserializedObject(object obj, Type targetType)
    {
        IEnumValue enumValue = obj as IEnumValue;

        if (enumValue!= null)
        { return enumValue.Value; }

        return obj;
    }

    public void GetKnownCustomDataTypes(Collection<Type> customDataTypes)
    {
    }

    public object GetObjectToSerialize(object obj, Type targetType)
    {
        if (obj != null)
        {
            Type type = obj.GetType();

            if (type.IsEnum && Attribute.IsDefined(type, typeof(FlagsAttribute)))
            { return Activator.CreateInstance(typeof(EnumValue<>).MakeGenericType(type), obj); }
        }

        return obj;
    }

    public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData)
    {
        return null;
    }

    public CodeTypeDeclaration ProcessImportedType(CodeTypeDeclaration typeDeclaration, CodeCompileUnit compileUnit)
    {
        return null;
    }

    #endregion
}

public interface IEnumValue : IXmlSerializable
{
    object Value { get; }
}

[Serializable]
public class EnumValue<T> : IEnumValue
    where T : struct
{
    #region Fields

    private Enum value;
    private static Type enumType;
    private static long[] values;
    private static string[] names;
    private static bool isULong;

    #endregion

    #region Constructors

    static EnumValue()
    {
        enumType = typeof(T);

        if (!enumType.IsEnum)
        { throw new InvalidOperationException(); }

        FieldInfo[] fieldInfos = enumType.GetFields(BindingFlags.Static | BindingFlags.Public);

        values = new long[fieldInfos.Length];
        names = new string[fieldInfos.Length];
        isULong = Enum.GetUnderlyingType(enumType) == typeof(ulong);

        for (int i = 0; i < fieldInfos.Length; i++)
        {
            FieldInfo fieldInfo = fieldInfos[i];
            EnumMemberAttribute enumMemberAttribute = (EnumMemberAttribute)fieldInfo
                .GetCustomAttributes(typeof(EnumMemberAttribute), false)
                .FirstOrDefault();
            IConvertible value = (IConvertible)fieldInfo.GetValue(null);

            values[i] = (isULong)
                ? (long)value.ToUInt64(null)
                : value.ToInt64(null);
            names[i] = (enumMemberAttribute == null || string.IsNullOrEmpty(enumMemberAttribute.Value))
                ? fieldInfo.Name
                : enumMemberAttribute.Value;
        }
    }

    public EnumValue()
    {
    }

    public EnumValue(Enum value)
    {
        this.value = value;
    }

    #endregion

    #region IXmlSerializable Members

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        string stringValue = reader.ReadElementContentAsString();

        long longValue = 0;
        int i = 0;

        // Skip initial spaces
        for (; i < stringValue.Length && stringValue[i] == ' '; i++) ;

        // Read comma-delimited values
        int startIndex = i;
        int nonSpaceIndex = i;
        int count = 0;

        for (; i < stringValue.Length; i++)
        {
            if (stringValue[i] == ',')
            {
                count = nonSpaceIndex - startIndex + 1;

                if (count > 1)
                { longValue |= ReadEnumValue(stringValue, startIndex, count); }

                nonSpaceIndex = ++i;

                // Skip spaces
                for (; i < stringValue.Length && stringValue[i] == ' '; i++) ;

                startIndex = i;

                if (i == stringValue.Length)
                { break; }
            }
            else
            {
                if (stringValue[i] != ' ')
                { nonSpaceIndex = i; }
            }
        }

        count = nonSpaceIndex - startIndex + 1;

        if (count > 1)
            longValue |= ReadEnumValue(stringValue, startIndex, count);

        value = (isULong)
            ? (Enum)Enum.ToObject(enumType, (ulong)longValue)
            : (Enum)Enum.ToObject(enumType, longValue);
    }

    public void WriteXml(XmlWriter writer)
    {
        long longValue = (isULong)
            ? (long)((IConvertible)value).ToUInt64(null)
            : ((IConvertible)value).ToInt64(null);

        int zeroIndex = -1;
        bool noneWritten = true;

        for (int i = 0; i < values.Length; i++)
        {
            long current = values[i];

            if (current == 0)
            {
                zeroIndex = i;
                continue;
            }

            if (longValue == 0)
            { break; }

            if ((current & longValue) == current)
            {
                if (noneWritten)
                { noneWritten = false; }
                else
                { writer.WriteString(","); }

                writer.WriteString(names[i]);
                longValue &= ~current;
            }
        }

        if (longValue != 0)
        { throw new InvalidOperationException(); }

        if (noneWritten && zeroIndex >= 0)
        { writer.WriteString(names[zeroIndex]); }
    }

    #endregion

    #region IEnumValue Members

    public object Value
    {
        get { return value; }
    }

    #endregion

    #region Private Methods

    private static long ReadEnumValue(string value, int index, int count)
    {
        for (int i = 0; i < names.Length; i++)
        {
            string name = names[i];

            if (count == name.Length && string.CompareOrdinal(value, index, name, 0, count) == 0)
            { return values[i]; }
        }

        throw new InvalidOperationException();
    }

    #endregion
}

第三种方法 是动态发出 class,如果基础 class 标记了 Enum 属性,将它们替换为 string 属性并使用生成的 class 的实例作为代理项。