2015-03-06 14 views
9

Po odfałszowaniu oznaczonego enum, które jest ozdobione atrybutem EnumMemberAttribute z wartością zawierającą spację, generowany jest wyjątek SerializationException. Przestrzeń w wartości jest traktowana jako separator.Odsumerowanie oznaczonego enum z spacją powoduje SerializationException

Czy istnieje sposób na zmianę separatora lub umieszczenie wartości w cudzysłowach? Czy istnieje nawet bardziej proste rozwiązanie?

Opcje ja już rozważam to:

  • Wymiana oznaczonej enum z listy tego typu Enum
  • Wymiana spacje podkreślenia
  • ta jest wykorzystywana w usług WCF, a ja am świadomy, że wyliczenia w datacontracts przez niektórych są uważane za złe. Więc także myślę o utracie enum wszystko razem.

Ale naprawdę uważam, że powinno to być coś konfigurowalnego lub coś, co inni już rozwiązali. Ale nie mogę znaleźć niczego.

Ugotowałem ten problem w prosty test jednostkowy. Poniższy kod powoduje:

Message = Nieprawidłowa wartość enum 'test' nie może być rozszeregować do typu 'UnitTests.TestEnum'. Upewnij się, że niezbędne wartości wyliczeniowe są obecne i są oznaczone atrybutem EnumMemberAttribute, jeśli typ ma atrybut DataContractAttribute. 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, 
    } 


} 
+2

Nie powiedziałbym, że "wyliczenia w datacontracts są złe", ale dla mnie "używanie spacji w enumach to zła rzecz" ;-) – fixagon

+0

Uzgodnione :) Nie lubię spacji gdziekolwiek, ale to nie jest mój wybór . Gdybym pokazał ci wartości, które muszą znaleźć się w rzeczywistych podsumowaniach, zacząłbyś płakać. – KeesDijk

+0

spójrz także na to: http://stackoverflow.com/questions/1415140/can-my-enums-have-friendly-names, ale tak naprawdę nie pomaga w twojej sytuacji. w przeciwnym razie możesz po prostu serializować enum jako int. ale to niszczy datacontract – fixagon

Odpowiedz

5

nie można używać w przestrzeni wartości ponieważ DataContractSerializer go używa i jest ustalony. Zobacz source i post. Ale jeśli naprawdę chcesz użyć spacji między wyrazami, użyj jednego z wymienionych rozwiązań:

Pierwszy sposób. Użyj innych wartości białych znaków, takich jak trzy-na-wiele spacji. Ale będziesz miał inny problem - nie ma wizualnego separatora między wartościami.

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

Drugim sposobem jest użycieIDataContractSurrogate. W ten sposób zostanie wygenerowany poniższy kod 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> 

Jak to działa? Po prostu zapakujemy nasze wyliczenie w proces serializacji i rozwiniemy w przypadku deserializacji. W tym celu należy użyć 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 
} 

Trzecim sposobem ma emitować dynamicznie klasy, jeśli klasa bazowa oflagowany Enum właściwości, zastąpić je string właściwości i użyć instancji generowanej klasy jako surogaty.

+0

Dziękuję za szczegółową odpowiedź! Widok zakodowanego na sztywno separatora daje mi pewność, że nie brakuje mi prostej opcji. Twoja druga opcja wygląda interesująco, nie widzę jeszcze wszystkich konsekwencji jej używania. Popatrzę na to nieco lepiej i zobaczę, czy to jest warte wysiłku. – KeesDijk

+0

Ukończyłem implementację klasy 'EnumValue '. I możesz go teraz użyć. –