XML-серіалізація властивостей інтерфейсу


83

Я хотів би XML серіалізувати об'єкт, який має (серед іншого) властивість типу IModelObject (що є інтерфейсом).

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

Коли я намагаюся серіалізувати об'єкт цього класу, я отримую таку помилку:
"Не вдається серіалізувати член Example. Модель типу Example, оскільки це інтерфейс."

Я розумію, що проблема в тому, що інтерфейс не можна серіалізувати. Однак конкретний тип об'єкта Model невідомий до часу виконання.

Заміна інтерфейсу IModelObject абстрактним або конкретним типом та використання успадкування на XMLInclude можлива, але видається потворним обхідним шляхом.

Будь-які пропозиції?

Відповіді:


116

Це просто невід'ємне обмеження декларативної серіалізації, коли інформація про тип не вбудована у вихідні дані.

На спробу перетворити <Flibble Foo="10" />назад на

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

Звідки серіалізатор знає, чи це int, рядок, дубль (чи щось інше) ...

Щоб зробити цю роботу, у вас є кілька варіантів, але якщо ви справді не знаєте до часу виконання, найпростіший спосіб зробити це, швидше за все, буде використовувати XmlAttributeOverrides .

На жаль, це буде працювати лише з базовими класами, а не з інтерфейсами. Найкраще, що ви можете там зробити, - це ігнорувати властивість, яка недостатня для ваших потреб.

Якщо ви дійсно повинні залишатися з інтерфейсами, у вас є три реальні варіанти:

Сховайте це і впорайтеся з цим в іншому майні

Потворна, неприємна плита котла і багато повторень, але більшості споживачів класу не доведеться стикатися з проблемою:

[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*/ } 
}

Це, мабуть, стане кошмаром для обслуговування ...

Впровадити IXmlSerializable

Подібно до першого варіанту, ви берете повний контроль над речами, але

  • Плюси
    • У вас немає неприємних "фальшивих" властивостей, що тусуються навколо.
    • Ви можете взаємодіяти безпосередньо зі структурою xml, додаючи гнучкість / керування версіями
  • Мінуси
    • можливо, вам доведеться повторно застосувати колесо для всіх інших властивостей класу

Питання дублювання зусиль подібні до першого.

Змініть своє властивість, щоб використовувати тип обтікання

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); }
}

Використання цього залучило б щось на зразок (у проекті 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);
    }
}

що дає вам:

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

Очевидно, це є більш громіздким для користувачів класу, хоча уникає великої кількості котлових плит.

Щасливим носієм може бути злиття ідеї XmlAnything у властивість «підкладки» першої техніки. Таким чином, більша частина бурчання робиться за вас, але споживачі класу не зазнають ніякого впливу, окрім плутанини з самоаналізом.


Я намагався реалізувати свої властивості обгортання підхід відьмами , але , до жаль , є проблеми :( Чи не могли б ви поглянути на цей пост, будь ласка: stackoverflow.com/questions/7584922 / ...
SOReader

Чи є якісь мистецькі знайомства з FooSerialized?
Gqqnbig

42

Рішенням цього є використання відображення за допомогою DataContractSerializer. Вам навіть не потрібно позначати свій клас за допомогою [DataContract] або [DataMember]. Він буде серіалізувати будь-який об’єкт, незалежно від того, чи має він властивості типу інтерфейсу (включаючи словники), у xml. Ось простий метод розширення, який дозволить серіалізувати будь-який об'єкт у XML, навіть якщо він має інтерфейси (зауважте, ви можете налаштувати це, щоб він також запускався рекурсивно).

    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());
    }

те, що робить вираз LINQ, це перераховує кожну властивість, повертає кожну властивість, яка є інтерфейсом, отримує значення цієї властивості (базовий об'єкт), отримує тип цього конкретного об'єкта, поміщає його в масив і додає це до серіалізатора список відомих типів.

Тепер серіалізатор знає, як щодо типів, які він серіалізує, щоб він міг робити свою роботу.


Дуже елегантне та просте рішення проблеми. Дякую!
Ghlouw

2
Здається, це не працює для загального IList of та інтерфейсу. наприклад, IList <IMyInterface>. Значення створення для IMyInterface потрібно додати до KnownTypes, однак замість цього буде додано IList <IMyInterface>.
galford13x

6
@ galford13x Я намагався зробити цей приклад якомога простішим, проте демонструючи свою суть. Додавання в будь-якому одиничному випадку, як-от рекурсія або типи інтерфейсу, робить його менш зрозумілим для читання та відводить від основної точки. Будь ласка, додайте будь-які додаткові чеки для отримання необхідних відомих типів. Чесно кажучи, я не думаю, що є щось, що ти не можеш отримати, використовуючи роздуми. Це, наприклад, отримає тип загального параметра, stackoverflow.com/questions/557340/…
Despertar

Я розумію, я згадав про це лише з моменту запитання про серіалізацію інтерфейсу. Я вирішив, що повідомлю іншим, що помилку можна буде очікувати без змін, щоб запобігти удару головою з їхнього боку. Однак я оцінив ваш код, оскільки додав атрибут [KnownType ()], і ваш код привів мене до результату.
galford13x

1
Чи є спосіб опустити namesapce при серіалізації? Я намагався використовувати xmlwriterSettings, використовуючи замість цього xmlwriter, я використовую перевантаження, де я можу передавати додаткові типи, але це не працює ...
Легенди

9

Якщо ви знаєте, як реалізовувати ваші інтерфейси, існує досить простий хак, який ви можете використати для того, щоб ваш тип інтерфейсу серіалізувався без написання коду синтаксичного аналізу:

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(); }
  }
}

Отриманий xml повинен виглядати приблизно так, як

 <interface><ofTypeKnownImplementor01><!-- etc... -->

1
Дуже корисно, дякую. У більшості ситуацій я знаю класи, що реалізують інтерфейс. Ця відповідь повинна бути вище imo.
Йона

Це найпростіше рішення. Дякую!
mKay

8

Ви можете використовувати ExtendedXmlSerializer . Цей серіалізатор підтримує серіалізацію властивостей інтерфейсу без будь-яких хитрощів.

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

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

var xml = serializer.Serialize(obj);

Ваш xml буде виглядати так:

<?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 .net 4.5 та .net Core.


3

Заміна інтерфейсу IModelObject абстрактним або конкретним типом та використання успадкування на XMLInclude можлива, але видається потворним обхідним шляхом.

Якщо можливо використовувати абстрактну базу, я б рекомендував цей маршрут. Це все одно буде чистішим, ніж використання ручної рулонної серіалізації. Єдина проблема, яку я бачу з абстрактною базою, полягає в тому, що вам все одно знадобиться конкретний тип? Принаймні так я використовував у минулому, приблизно так:

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

2

На жаль, немає простої відповіді, оскільки серіалізатор не знає, що серіалізувати для інтерфейсу. Я знайшов більш повне пояснення того, як це обійти на MSDN


1

На жаль, у мене був випадок, коли клас, що підлягає серіалізації, мав властивості, які також мали інтерфейси як властивості, тому мені потрібно було рекурсивно обробляти кожну властивість. Крім того, деякі властивості інтерфейсу були позначені як [XmlIgnore], тому я хотів пропустити їх. Я взяв ідеї, які знайшов у цій темі, і додав до неї деякі речі, щоб зробити її рекурсивною. Тут показано лише код десериалізації:

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

1

Я знайшов більш просте рішення (вам не потрібен DataContractSerializer), завдяки цьому блогу тут: XML серіалізує похідні типи, коли базовий тип знаходиться в іншому просторі імен або DLL

Але при цьому можуть виникнути 2 проблеми:

(1) Що робити, якщо DerivedBase не знаходиться в просторі імен класу Base, або навіть гірше в проекті, який залежить від простору імен Base, тому Base не може XMLInclude DerivedBase

(2) Що робити, якщо ми маємо лише клас Base як dll, тому знову ж таки Base не може XMLInclude DerivedBase

Дотепер, ...

Тож вирішення двох проблем полягає у використанні конструктора XmlSerializer (Тип, масив []) :

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

Детальний приклад наведено тут на MSDN: XmlSerializer Constructor (Type, extraTypesArray [])

Мені здається, що для DataContracts або Soap XML вам потрібно перевірити XmlRoot, як згадано тут у цьому запитанні SO .

Аналогічну відповідь тут на SO , але він не відзначений , як один, так як це не ОП , здається, вважав це вже.


0

у своєму проекті я маю
List <IFormatStyle> FormatStyleTemplates;
містять різні типи.

Потім я використовую рішення «XmlAnything» зверху, щоб серіалізувати цей список різних типів. Згенерований xml чудовий.

    [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));
        }
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.