Serializacja XML właściwości interfejsu

83

Chciałbym zserializować XML obiekt, który ma (między innymi) właściwość typu IModelObject (czyli interfejs).

public class Example
{
    public IModelObject Model { get; set; }
}

Gdy próbuję serializować obiekt tej klasy,
pojawia się następujący błąd: „Nie można serializować elementu członkowskiego Example.Model typu Example, ponieważ jest to interfejs.”

Rozumiem, że problem polega na tym, że nie można serializować interfejsu. Jednak konkretny typ obiektu Model jest nieznany do czasu wykonania.

Zastąpienie interfejsu IModelObject typem abstrakcyjnym lub konkretnym i użycie dziedziczenia z XMLInclude jest możliwe, ale wydaje się brzydkim obejściem.

Jakieś sugestie?

Elad
źródło

Odpowiedzi:

116

Jest to po prostu nieodłączne ograniczenie serializacji deklaratywnej, w której informacje o typie nie są osadzone w danych wyjściowych.

Podczas próby konwersji z <Flibble Foo="10" />powrotem do

public class Flibble { public object Foo { get; set; } }

Skąd serializator wie, czy powinien to być int, string, double (lub coś innego) ...

Aby to zadziałało, masz kilka opcji, ale jeśli naprawdę nie wiesz do czasu wykonania, najłatwiejszym sposobem na to jest użycie XmlAttributeOverrides .

Niestety będzie to działać tylko z klasami bazowymi, a nie interfejsami. Najlepsze, co możesz zrobić, to zignorować właściwość, która nie jest wystarczająca dla Twoich potrzeb.

Jeśli naprawdę musisz pozostać przy interfejsach, masz trzy prawdziwe opcje:

Ukryj to i zajmij się nim w innej nieruchomości

Brzydka, nieprzyjemna płyta kotłowa i dużo powtórzeń, ale większość konsumentów tej klasy nie będzie musiała radzić sobie z problemem:

[XmlIgnore()]
public object Foo { get; set; }

[XmlElement("Foo")]
[EditorVisibile(EditorVisibility.Advanced)]
public string FooSerialized 
{ 
  get { /* code here to convert any type in Foo to string */ } 
  set { /* code to parse out serialized value and make Foo an instance of the proper type*/ } 
}

To prawdopodobnie stanie się koszmarem związanym z konserwacją ...

Zaimplementuj IXmlSerializable

Podobnie jak w przypadku pierwszej opcji, w której przejmujesz pełną kontrolę nad rzeczami, ale

  • Plusy
    • Nie masz w pobliżu paskudnych „fałszywych” nieruchomości.
    • możesz współdziałać bezpośrednio ze strukturą XML, dodając elastyczność / wersjonowanie
  • Cons
    • może się okazać, że będziesz musiał ponownie zaimplementować koło dla wszystkich innych właściwości w klasie

Kwestie powielania wysiłków są podobne do pierwszego.

Zmodyfikuj właściwość, aby użyć typu zawijania

public sealed class XmlAnything<T> : IXmlSerializable
{
    public XmlAnything() {}
    public XmlAnything(T t) { this.Value = t;}
    public T Value {get; set;}

    public void WriteXml (XmlWriter writer)
    {
        if (Value == null)
        {
            writer.WriteAttributeString("type", "null");
            return;
        }
        Type type = this.Value.GetType();
        XmlSerializer serializer = new XmlSerializer(type);
        writer.WriteAttributeString("type", type.AssemblyQualifiedName);
        serializer.Serialize(writer, this.Value);   
    }

    public void ReadXml(XmlReader reader)
    {
        if(!reader.HasAttributes)
            throw new FormatException("expected a type attribute!");
        string type = reader.GetAttribute("type");
        reader.Read(); // consume the value
        if (type == "null")
            return;// leave T at default value
        XmlSerializer serializer = new XmlSerializer(Type.GetType(type));
        this.Value = (T)serializer.Deserialize(reader);
        reader.ReadEndElement();
    }

    public XmlSchema GetSchema() { return(null); }
}

Użycie tego wymagałoby czegoś takiego (w projekcie P):

public namespace P
{
    public interface IFoo {}
    public class RealFoo : IFoo { public int X; }
    public class OtherFoo : IFoo { public double X; }

    public class Flibble
    {
        public XmlAnything<IFoo> Foo;
    }


    public static void Main(string[] args)
    {
        var x = new Flibble();
        x.Foo = new XmlAnything<IFoo>(new RealFoo());
        var s = new XmlSerializer(typeof(Flibble));
        var sw = new StringWriter();
        s.Serialize(sw, x);
        Console.WriteLine(sw);
    }
}

co daje:

<?xml version="1.0" encoding="utf-16"?>
<MainClass 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <Foo type="P.RealFoo, P, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
  <RealFoo>
   <X>0</X>
  </RealFoo>
 </Foo>
</MainClass>

Jest to oczywiście bardziej uciążliwe dla użytkowników tej klasy, ale pozwala uniknąć zbytniej płyty kotłowej.

Wesołym medium może być scalanie pomysłu XmlAnything z właściwością „backing” pierwszej techniki. W ten sposób większość podstawowej pracy jest wykonywana za Ciebie, ale konsumenci tej klasy nie mają żadnego wpływu poza pomyłką z introspekcją.

ShuggyCoUk
źródło
Próbowałem zaimplementować twoje podejście z właściwościami zawijania, ale niestety mam problem :( Czy możesz
rzucić
Czy są jakieś artykuły wprowadzające właściwość FooSerialized?
Gqqnbig
42

Rozwiązaniem tego problemu jest użycie odbicia z DataContractSerializer. Nie musisz nawet oznaczać swojej klasy za pomocą [DataContract] lub [DataMember]. Będzie serializować dowolny obiekt, niezależnie od tego, czy ma właściwości typu interfejsu (w tym słowniki) do XML. Oto prosta metoda rozszerzenia, która spowoduje serializację dowolnego obiektu do XML, nawet jeśli ma on interfejsy (pamiętaj, że możesz to zmienić, aby również działał rekurencyjnie).

    public static XElement ToXML(this object o)
    {
        Type t = o.GetType();

        Type[] extraTypes = t.GetProperties()
            .Where(p => p.PropertyType.IsInterface)
            .Select(p => p.GetValue(o, null).GetType())
            .ToArray();

        DataContractSerializer serializer = new DataContractSerializer(t, extraTypes);
        StringWriter sw = new StringWriter();
        XmlTextWriter xw = new XmlTextWriter(sw);
        serializer.WriteObject(xw, o);
        return XElement.Parse(sw.ToString());
    }

Wyrażenie LINQ wylicza każdą właściwość, zwraca każdą właściwość, która jest interfejsem, pobiera wartość tej właściwości (obiekt bazowy), pobiera typ tego konkretnego obiektu, umieszcza go w tablicy i dodaje ją do lista znanych typów.

Teraz serializator wie, jakie typy serializuje, aby mógł wykonać swoją pracę.

Despertar
źródło
Bardzo eleganckie i łatwe rozwiązanie problemu. Dzięki!
Ghlouw
2
Wydaje się, że nie działa to w przypadku ogólnego IList of and interface. np. IList <IMyInterface>. Jednak wartość concreate dla IMyInterface musi zostać dodana do KnownTypes, zamiast tego zostanie dodany IList <IMyInterface>.
galford13x
6
@ galford13x Próbowałem uczynić ten przykład tak prostym, jak to tylko możliwe, jednocześnie demonstrując punkt. Dodanie w każdym przypadku pojedynczego przypadku, takiego jak rekursja lub typy interfejsów, sprawia, że ​​czytanie jest mniej przejrzyste i odejmuje główny punkt. Możesz dodać dodatkowe kontrole, aby wyciągnąć potrzebne znane typy. Szczerze mówiąc, nie sądzę, żeby było coś, czego nie można uzyskać za pomocą refleksji. To na przykład pozwoli uzyskać typ parametru ogólnego, stackoverflow.com/questions/557340/ ...
Despertar
Rozumiem, wspomniałem o tym dopiero od czasu pytania o serializację interfejsu. Pomyślałem, że dam innym znać, że błąd byłby oczekiwany bez modyfikacji, aby zapobiec uderzaniu głową z ich strony. Doceniłem jednak Twój kod, ponieważ dodałem atrybut [KnownType ()], a Twój kod doprowadził mnie do wyniku.
galford13x
1
Czy istnieje sposób na pominięcie nazwy przy serializacji? Próbowałem użyć xmlwriterSettings, używając zamiast tego xmlwriter, używam przeciążenia, w którym mogę przekazać dodatkowe typy, ale to nie działa ...
Legends
9

Jeśli znasz z góry implementatorów interfejsu, możesz skorzystać z dość prostego hacka, aby uzyskać typ interfejsu do serializacji bez pisania kodu parsującego:

public interface IInterface {}
public class KnownImplementor01 : IInterface {}
public class KnownImplementor02 : IInterface {}
public class KnownImplementor03 : IInterface {}
public class ToSerialize {
  [XmlIgnore]
  public IInterface InterfaceProperty { get; set; }
  [XmlArray("interface")]
  [XmlArrayItem("ofTypeKnownImplementor01", typeof(KnownImplementor01))]
  [XmlArrayItem("ofTypeKnownImplementor02", typeof(KnownImplementor02))]
  [XmlArrayItem("ofTypeKnownImplementor03", typeof(KnownImplementor03))]
  public object[] InterfacePropertySerialization {
    get { return new[] { InterfaceProperty }; ; }
    set { InterfaceProperty = (IInterface)value.Single(); }
  }
}

Wynikowy plik XML powinien wyglądać podobnie do

 <interface><ofTypeKnownImplementor01><!-- etc... -->
hannasm
źródło
1
Bardzo przydatne, dzięki. W większości sytuacji znam klasy implementujące interfejs. Ta odpowiedź powinna być wyższa imo.
Jonasz
To najłatwiejsze rozwiązanie. Dziękuję Ci!
mKay
8

Możesz użyć ExtendedXmlSerializer . Ten serializator obsługuje serializację właściwości interfejsu bez żadnych sztuczek.

var serializer = new ConfigurationContainer().UseOptimizedNamespaces().Create();

var obj = new Example
                {
                    Model = new Model { Name = "name" }
                };

var xml = serializer.Serialize(obj);

Twój xml będzie wyglądał następująco:

<?xml version="1.0" encoding="utf-8"?>
<Example xmlns:exs="https://extendedxmlserializer.github.io/v2" xmlns="clr-namespace:ExtendedXmlSerializer.Samples.Simple;assembly=ExtendedXmlSerializer.Samples">
    <Model exs:type="Model">
        <Name>name</Name>
    </Model>
</Example>

ExtendedXmlSerializer obsługuje .net 4.5 i .net Core.

Wojtpl2
źródło
3

Zastąpienie interfejsu IModelObject abstrakcyjnym lub konkretnym typem i użycie dziedziczenia z XMLInclude jest możliwe, ale wydaje się brzydkim obejściem.

Jeśli można skorzystać z abstrakcyjnej bazy, to poleciłbym tę trasę. Nadal będzie czystszy niż przy użyciu serializacji ręcznej. Jedynym problemem, jaki widzę z abstrakcyjną podstawą, jest to, że nadal będziesz potrzebować konkretnego typu? Przynajmniej tak go używałem w przeszłości, coś takiego:

public abstract class IHaveSomething
{
    public abstract string Something { get; set; }
}

public class MySomething : IHaveSomething
{
    string _sometext;
    public override string Something 
    { get { return _sometext; } set { _sometext = value; } }
}

[XmlRoot("abc")]
public class seriaized
{
    [XmlElement("item", typeof(MySomething))]
    public IHaveSomething data;
}
csharptest.net
źródło
2

Niestety nie ma prostej odpowiedzi, ponieważ serializator nie wie, co serializować dla interfejsu. Znalazłem pełniejsze wyjaśnienie, jak obejść ten problem w witrynie MSDN

MattH
źródło
1

Niestety, miałem przypadek, w którym klasa do serializacji miała właściwości, które miały również interfejsy jako właściwości, więc musiałem rekurencyjnie przetwarzać każdą właściwość. Ponadto niektóre właściwości interfejsu zostały oznaczone jako [XmlIgnore], więc chciałem je pominąć. Wziąłem pomysły, które znalazłem w tym wątku i dodałem do niego kilka rzeczy, aby był rekurencyjny. Pokazany jest tylko kod deserializacji:

void main()
{
    var serializer = GetDataContractSerializer<MyObjectWithCascadingInterfaces>();
    using (FileStream stream = new FileStream(xmlPath, FileMode.Open))
    {
        XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader(stream, new XmlDictionaryReaderQuotas());
        var obj = (MyObjectWithCascadingInterfaces)serializer.ReadObject(reader);

        // your code here
    }
}

DataContractSerializer GetDataContractSerializer<T>() where T : new()
{
    Type[] types = GetTypesForInterfaces<T>();

    // Filter out duplicates
    Type[] result = types.ToList().Distinct().ToList().ToArray();

    var obj = new T();
    return new DataContractSerializer(obj.GetType(), types);
}

Type[] GetTypesForInterfaces<T>() where T : new()
{
    return GetTypesForInterfaces(typeof(T));
}

Type[] GetTypesForInterfaces(Type T)
{
    Type[] result = new Type[0];
    var obj = Activator.CreateInstance(T);

    // get the type for all interface properties that are not marked as "XmlIgnore"
    Type[] types = T.GetProperties()
        .Where(p => p.PropertyType.IsInterface && 
            !p.GetCustomAttributes(typeof(System.Xml.Serialization.XmlIgnoreAttribute), false).Any())
        .Select(p => p.GetValue(obj, null).GetType())
        .ToArray();

    result = result.ToList().Concat(types.ToList()).ToArray();

    // do the same for each of the types identified
    foreach (Type t in types)
    {
        Type[] embeddedTypes = GetTypesForInterfaces(t);
        result = result.ToList().Concat(embeddedTypes.ToList()).ToArray();
    }
    return result;
}
acordner
źródło
1

Znalazłem prostsze rozwiązanie (nie potrzebujesz DataContractSerializer), dzięki temu blogowi tutaj: serializacja XML typów pochodnych, gdy typ podstawowy znajduje się w innej przestrzeni nazw lub DLL

Jednak w tej implementacji mogą pojawić się 2 problemy:

(1) Co się stanie, jeśli DerivedBase nie znajduje się w przestrzeni nazw klasy Base, a nawet gorzej w projekcie, który zależy od Base namespace, więc Base nie może XMLInclude DerivedBase

(2) Co jeśli mamy tylko klasę Base jako bibliotekę dll, więc ponownie Base nie może XMLInclude DerivedBase

Do teraz, ...

Zatem rozwiązaniem dwóch problemów jest użycie konstruktora XmlSerializer (Type, array []) :

XmlSerializer ser = new XmlSerializer(typeof(A), new Type[]{ typeof(DerivedBase)});

Szczegółowy przykład znajduje się tutaj w witrynie MSDN: XmlSerializer Constructor (Type, extraTypesArray [])

Wydaje mi się, że w przypadku DataContracts lub Soap XML trzeba sprawdzić XmlRoot, jak wspomniano tutaj w tym pytaniu SO .

Podobna odpowiedź jest tutaj na SO , ale to nie jest oznaczony jako jeden, a nie PO wydaje się uznać go już.

B Charles H.
źródło
0

w moim projekcie mam
List <IFormatStyle> FormatStyleTemplates;
zawierające różne typy.

Następnie używam rozwiązania „XmlAnything” z góry, aby serializować tę listę różnych typów. Wygenerowany xml jest piękny.

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [XmlArray("FormatStyleTemplates")]
    [XmlArrayItem("FormatStyle")]
    public XmlAnything<IFormatStyle>[] FormatStyleTemplatesXML
    {
        get
        {
            return FormatStyleTemplates.Select(t => new XmlAnything<IFormatStyle>(t)).ToArray();
        }
        set
        {
            // read the values back into some new object or whatever
            m_FormatStyleTemplates = new FormatStyleProvider(null, true);
            value.ForEach(t => m_FormatStyleTemplates.Add(t.Value));
        }
    }
Detlef Kroll
źródło