Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

When de-serializing a flagged enum that is decorated with a EnumMemberAttribute with a value containing a space a SerializationException is thrown. The space in the value is treated as a separator.

Is there a way to change the separator or put the values in quotes ? Or is there even a more simple solution ?

Options I already am considering are :

  • Replacing the flagged enum with a list of this enum type
  • Replacing the spaces with underscores
  • This is used in a WCF service, and I am aware that enums in datacontracts by some are considered a bad thing. So I am also thinking about losing the enum’s all together.

But I really feel that this should be something configurable or something other people already solved. But I can't find anything.

I have boiled the problem down to a simple unit test. The code below results in:

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


}
like image 488
KeesDijk Avatar asked Mar 06 '15 12:03

KeesDijk


1 Answers

You can't use space in values because DataContractSerializer uses it and it is hardcoded. See the source and the post. But if you really want to use space between words, then use one of the listed solutions:

The first way. Use other whitespace characters such as three-per-em space in values. But you will have another problem - there is no visual separator between values.

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

The second way is to use IDataContractSurrogate. This way will produce the XML listed below:

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

How does it work? We will just wrap our enumeration in the process of serialization and unwrap in case of deserialization. In order to do that we should use 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
}

The third way is to emit dynamically the class, if base class has flagged Enum properties, replace them with string properties and use instances of the generated class as surrogates.

like image 118
Yoh Deadfall Avatar answered Oct 09 '22 05:10

Yoh Deadfall