Serializacja XML i typy dziedziczone

85

Kontynuując moje poprzednie pytanie , pracowałem nad uzyskaniem serializacji mojego modelu obiektowego do XML. Ale teraz mam problem (quelle niespodzianka!).

Problem polega na tym, że mam kolekcję, która jest abstrakcyjnym typem klasy bazowej, która jest zapełniana przez konkretne typy pochodne.

Pomyślałem, że byłoby dobrze po prostu dodać atrybuty XML do wszystkich zaangażowanych klas i wszystko byłoby brzoskwiniowe. Niestety tak nie jest!

Poszperałem więc trochę w Google i teraz rozumiem, dlaczego to nie działa. W tym jest w rzeczywistości robi jakąś sprytną odbicie w celu serializacji obiektów do / z XML, a od czasu jej podstawie abstrakcyjnego typu, nie może dowiedzieć się, co u diabła on rozmawia . W porządku.XmlSerializer

Natknąłem się na tę stronę w CodeProject, która wygląda na to, że może bardzo pomóc (jeszcze do przeczytania / skonsumowania), ale pomyślałem, że chciałbym również przenieść ten problem do tabeli StackOverflow, aby sprawdzić, czy masz jakieś schludne hacki / triki, aby to uruchomić w możliwie najszybszy / najlżejszy sposób.

Dodam jeszcze, że NIE CHCĘ zjeżdżać tą XmlIncludetrasą. Jest z nim po prostu za dużo sprzężenia, a ten obszar systemu jest intensywnie rozwijany, więc byłby to prawdziwy ból głowy związany z konserwacją!

Rob Cooper
źródło
1
Przydałoby się zobaczyć kilka odpowiednich fragmentów kodu wyodrębnionych z klas, które próbujesz serializować.
Rex M
Mate: Ponownie otworzyłem, ponieważ uważam, że inni ludzie mogą uznać to za przydatne, ale możesz zamknąć, jeśli się nie zgadzasz
JamesSugrue
Trochę zdezorientowany, skoro tak długo nic nie było w tym wątku?
Rob Cooper,

Odpowiedzi:

54

Problem rozwiązany!

OK, więc w końcu tam dotarłem (trzeba przyznać, że z dużą pomocą stąd !).

Więc podsumuj:

Cele:

  • Nie chciałem iść w dół trasy XmlInclude z powodu bólu głowy związanego z utrzymaniem.
  • Po znalezieniu rozwiązania chciałem, aby można je było szybko wdrożyć w innych aplikacjach.
  • Można stosować kolekcje typów abstrakcyjnych, a także indywidualne właściwości abstrakcyjne.
  • Naprawdę nie chciałem zawracać sobie głowy koniecznością robienia „specjalnych” rzeczy na konkretnych zajęciach.

Zidentyfikowane problemy / punkty, na które należy zwrócić uwagę:

  • XmlSerializer robi całkiem fajną refleksję, ale jest bardzo ograniczony, jeśli chodzi o typy abstrakcyjne (tj. Będzie działać tylko z instancjami samego typu abstrakcyjnego, a nie z podklasami).
  • Dekoratory atrybutów Xml definiują sposób, w jaki XmlSerializer traktuje znalezione właściwości. Można również określić typ fizyczny, ale tworzy to ścisłe powiązanie między klasą a serializatorem (niedobre).
  • Możemy zaimplementować własny XmlSerializer, tworząc klasę, która implementuje IXmlSerializable .

Rozwiązanie

Stworzyłem klasę ogólną, w której określasz typ ogólny jako typ abstrakcyjny, z którym będziesz pracować. Daje to klasie możliwość „tłumaczenia” między typem abstrakcyjnym a typem konkretnym, ponieważ możemy na stałe zakodować rzutowanie (tj. Możemy uzyskać więcej informacji niż XmlSerializer).

Następnie zaimplementowałem interfejs IXmlSerializable , jest to dość proste, ale podczas serializacji musimy upewnić się, że zapisujemy typ konkretnej klasy do XML, abyśmy mogli odrzucić go z powrotem podczas deserializacji. Należy również zauważyć, że musi być w pełni kwalifikowany, ponieważ zespoły, w których znajdują się dwie klasy, mogą się różnić. Jest oczywiście trochę sprawdzania typu i rzeczy, które muszą się tutaj wydarzyć.

Ponieważ XmlSerializer nie może rzutować, musimy dostarczyć kod, aby to zrobić, więc niejawny operator jest następnie przeciążany (nawet nie wiedziałem, że możesz to zrobić!).

Oto kod AbstractXmlSerializer:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

namespace Utility.Xml
{
    public class AbstractXmlSerializer<AbstractType> : IXmlSerializable
    {
        // Override the Implicit Conversions Since the XmlSerializer
        // Casts to/from the required types implicitly.
        public static implicit operator AbstractType(AbstractXmlSerializer<AbstractType> o)
        {
            return o.Data;
        }

        public static implicit operator AbstractXmlSerializer<AbstractType>(AbstractType o)
        {
            return o == null ? null : new AbstractXmlSerializer<AbstractType>(o);
        }

        private AbstractType _data;
        /// <summary>
        /// [Concrete] Data to be stored/is stored as XML.
        /// </summary>
        public AbstractType Data
        {
            get { return _data; }
            set { _data = value; }
        }

        /// <summary>
        /// **DO NOT USE** This is only added to enable XML Serialization.
        /// </summary>
        /// <remarks>DO NOT USE THIS CONSTRUCTOR</remarks>
        public AbstractXmlSerializer()
        {
            // Default Ctor (Required for Xml Serialization - DO NOT USE)
        }

        /// <summary>
        /// Initialises the Serializer to work with the given data.
        /// </summary>
        /// <param name="data">Concrete Object of the AbstractType Specified.</param>
        public AbstractXmlSerializer(AbstractType data)
        {
            _data = data;
        }

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null; // this is fine as schema is unknown.
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            // Cast the Data back from the Abstract Type.
            string typeAttrib = reader.GetAttribute("type");

            // Ensure the Type was Specified
            if (typeAttrib == null)
                throw new ArgumentNullException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because no 'type' attribute was specified in the XML.");

            Type type = Type.GetType(typeAttrib);

            // Check the Type is Found.
            if (type == null)
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the type specified in the XML was not found.");

            // Check the Type is a Subclass of the AbstractType.
            if (!type.IsSubclassOf(typeof(AbstractType)))
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the Type specified in the XML differs ('" + type.Name + "').");

            // Read the Data, Deserializing based on the (now known) concrete type.
            reader.ReadStartElement();
            this.Data = (AbstractType)new
                XmlSerializer(type).Deserialize(reader);
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            // Write the Type Name to the XML Element as an Attrib and Serialize
            Type type = _data.GetType();

            // BugFix: Assembly must be FQN since Types can/are external to current.
            writer.WriteAttributeString("type", type.AssemblyQualifiedName);
            new XmlSerializer(type).Serialize(writer, _data);
        }

        #endregion
    }
}

Jak więc od tego momentu mamy powiedzieć XmlSerializer, aby współpracował z naszym serializatorem, a nie z domyślnym? Musimy przekazać nasz typ w ramach właściwości typu atrybutów XML, na przykład:

[XmlRoot("ClassWithAbstractCollection")]
public class ClassWithAbstractCollection
{
    private List<AbstractType> _list;
    [XmlArray("ListItems")]
    [XmlArrayItem("ListItem", Type = typeof(AbstractXmlSerializer<AbstractType>))]
    public List<AbstractType> List
    {
        get { return _list; }
        set { _list = value; }
    }

    private AbstractType _prop;
    [XmlElement("MyProperty", Type=typeof(AbstractXmlSerializer<AbstractType>))]
    public AbstractType MyProperty
    {
        get { return _prop; }
        set { _prop = value; }
    }

    public ClassWithAbstractCollection()
    {
        _list = new List<AbstractType>();
    }
}

Tutaj możesz zobaczyć, mamy kolekcję i pojedynczą właściwość, które są ujawniane, a wszystko, co musimy zrobić, to dodać parametr o nazwie typu do deklaracji Xml, łatwe! :RE

UWAGA: Jeśli użyjesz tego kodu, byłbym wdzięczny za wiadomość. Pomoże również przyciągnąć więcej osób do społeczności :)

Teraz, ale nie jestem pewien, co zrobić z odpowiedziami tutaj, ponieważ wszyscy mieli swoje plusy i minusy. Zmodyfikuję te, które uważam za przydatne (bez obrazy dla tych, które nie były) i zakończę to, gdy będę miał przedstawiciela :)

Ciekawy problem i dobra zabawa do rozwiązania! :)

Rob Cooper
źródło
Sam napotkałem ten problem jakiś czas temu. Osobiście ostatecznie porzuciłem XmlSerializer i bezpośrednio korzystałem z interfejsu IXmlSerializable, ponieważ wszystkie moje klasy i tak musiały go zaimplementować. W przeciwnym razie rozwiązania są dość podobne. Dobry
opis
Używamy właściwości XML_, gdzie konwertujemy listę na tablice :)
Arcturus
2
Ponieważ do dynamicznego tworzenia instancji klasy potrzebny jest konstruktor bez parametrów.
Silas Hansen
1
Cześć! Od jakiegoś czasu szukałem takiego rozwiązania. Myślę, że to genialne! Chociaż nie jestem w stanie wymyślić, jak go używać, czy mógłbyś podać przykład? Czy serializujesz swoją klasę czy listę zawierającą twoje obiekty?
Daniel,
1
Niezły kod. Należy zauważyć, że konstruktor bez parametrów można zadeklarować privatelub protectedwymusić, aby nie był dostępny dla innych klas.
tcovo
9

Jedną z rzeczy, na które należy zwrócić uwagę, jest fakt, że w konstruktorze XmlSerialiser można przekazać tablicę typów, które serializator może mieć trudności z rozpoznaniem. Musiałem tego używać kilka razy, gdy zbiór lub złożony zestaw struktur danych wymagał serializacji, a te typy żyły w różnych zespołach itp.

XmlSerialiser Constructor z parametrem extraTypes

EDYCJA: Dodałbym, że to podejście ma tę przewagę nad atrybutami XmlInclude itp., Że można wypracować sposób wykrywania i kompilowania listy możliwych konkretnych typów w czasie wykonywania i umieszczać je.

Shaun Austin
źródło
To jest to, co próbuję zrobić, ale nie jest to łatwe, tak jak myślałem: stackoverflow.com/questions/3897818/…
Luca
To jest bardzo stary post, ale dla każdego, kto chce go zaimplementować tak jak my, pamiętaj, że konstruktor XmlSerializer z parametrem extraTypes nie buforuje zestawów, które generuje w locie. To kosztuje nas tygodnie debugowania tego wycieku pamięci. Więc jeśli chcesz używać dodatkowych typów z kodem zaakceptowanej odpowiedzi, buforuj serializator . To zachowanie jest udokumentowane tutaj: support.microsoft.com/en-us/kb/886385
Julien Lebot
3

Poważnie, rozszerzalna struktura POCO nigdy nie zostanie niezawodnie serializowana do XML. Mówię to, ponieważ mogę zagwarantować, że ktoś przyjdzie, rozszerzy twoją klasę i zepsuje.

Należy przyjrzeć się użyciu języka XAML do serializacji wykresów obiektów. Jest przeznaczony do tego, podczas gdy serializacja XML nie.

Serializator i deserializator Xaml bez problemu obsługuje typy generyczne, a także kolekcje klas bazowych i interfejsów (o ile same kolekcje implementują IListlub IDictionary). Istnieją pewne zastrzeżenia, takie jak oznaczanie właściwości kolekcji tylko do odczytu za pomocą DesignerSerializationAttribute, ale przerobienie kodu w celu obsługi tych narożnych przypadków nie jest takie trudne.


źródło
Link wydaje się być martwy
bkribbs
No cóż. Niszczę to trochę. Wiele innych źródeł na ten temat.
2

Tylko szybka aktualizacja, nie zapomniałem!

Po prostu robię więcej badań, wygląda na to, że jestem na drodze do zwycięzcy, po prostu muszę posortować kod.

Jak dotąd mam:

  • XmlSeralizer jest po prostu klasa, która ma jakieś ładne odbicie na zajęciach jest identyfikatorów seryjnych. Określa właściwości, które są serializowane na podstawie Type .
  • Przyczyną problemu jest to, że występuje niezgodność typów, oczekuje on typu BaseType, ale w rzeczywistości otrzymuje typ DerivedType . Chociaż możesz pomyśleć, że potraktowałby go polimorficznie, tak się nie dzieje, ponieważ wymagałoby to całego dodatkowego obciążenia refleksja i sprawdzanie typu, do czego nie jest przeznaczone.

Wydaje się, że to zachowanie można przesłonić (oczekujący kod) przez utworzenie klasy proxy, która będzie działać jako pośrednik dla serializatora. Zasadniczo określi typ klasy pochodnej, a następnie serializuje ją w normalny sposób. Ta klasa proxy przekaże następnie ten kod XML do kopii zapasowej wiersza do głównego serializatora.

Patrz na przestrzeń! ^ _ ^

Rob Cooper
źródło
2

Z pewnością jest to rozwiązanie twojego problemu, ale jest inny problem, który nieco podważa twoją intencję używania „przenośnego” formatu XML. Złe dzieje się, gdy zdecydujesz się zmienić klasy w następnej wersji programu i musisz obsługiwać oba formaty serializacji - nowy i stary (ponieważ Twoi klienci nadal używają swoich starych plików / baz danych lub łączą się z Twój serwer przy użyciu starej wersji produktu). Ale nie możesz już używać tego serializatora, ponieważ używałeś

type.AssemblyQualifiedName

który wygląda jak

TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089

czyli zawiera atrybuty i wersję zestawu ...

Teraz, jeśli spróbujesz zmienić wersję zestawu lub zdecydujesz się go podpisać, ta deserializacja nie zadziała ...

Max Galkin
źródło
1

Robiłem podobne rzeczy. To, co zwykle robię, to upewnienie się, że wszystkie atrybuty serializacji XML znajdują się w konkretnej klasie i po prostu mam wywołanie właściwości tej klasy do klas podstawowych (jeśli jest to wymagane) w celu pobrania informacji, które zostaną de / serializowane, gdy wywoła serializator te właściwości. To trochę więcej pracy z kodowaniem, ale działa znacznie lepiej niż próba zmuszenia serializatora do zrobienia właściwej rzeczy.

Smerf
źródło
1

Jeszcze lepiej, używając notacji:

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}
user2009677
źródło
2
To świetnie, jeśli znasz swoje zajęcia, to najbardziej eleganckie rozwiązanie. Jeśli ładujesz nowe odziedziczone klasy z zewnętrznego źródła, nie możesz go niestety używać.
Vladimir,