De-serializing a flagged enum with a space results in SerializationException
Asked Answered
S

1

9

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


}
Sturgill answered 6/3, 2015 at 12:30 Comment(7)
I wouldnt say that "enums in datacontracts are a bad thing" but for me "using spaces in enums is a bad thing" ;-)Calle
Agreed :) I don't like spaces anywhere, but this is not my choice. If I would show you the values that need to go in the actual enums you would start to cry.Sturgill
look also at that: #1415640 but does not really help in your situation . otherwise you can just serialize the enum as int. but this breaks the datacontractCalle
Since space is used as the separator for flags enums in data contracts, what you're asking is simply not possible with the current implementation of data contracts. Regardless of whose choice it is, you're going to have to do things differently. What about not serializing that field and instead serializing it manually as a string or int property?Bobbibobbie
So there is no way to define a different separator or to enclose the values in quotes in the current implementation of datacontracts ? Converting the flagged enum in a list of strings or a list of the same type is definitely an option but I hoped for something configurable.Sturgill
A better question is why you have spaces in a flags enum at all? What kind of system enforces you to have that seeing as data contracts doesn't support it? Seems like a wrong choice to me so I would revisit the decision to include spaces in the serialized enum names.Bobbibobbie
When using a non flagged enum you can put spaces just fine in the enum member value. That value is used at the client side to fill drop down boxes in a human readable way. But when you need to be able to select multiple values this breaks down. Don't you think that is strange ? Who in his right mind uses a space as seperator, without any option to escape, quote or reconfigure ? I probably will need to use one of the workarounds I have suggested myself or sugested in the comments but still would like to know If I am not missing an option.Sturgill
W
5

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.

Weak answered 11/3, 2015 at 15:20 Comment(2)
Thank you for you detailed answer! Seeing the separator hardcoded gives me confidence enough that I am not missing a simple option. Your second option looks interesting, I don’t see all the implications of using it yet. I will look at that a bit better and see if it is worth the effort.Sturgill
I completed the EnumValue<T> class implementation. And you can use it now.Weak

© 2022 - 2024 — McMap. All rights reserved.