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

Я хотел бы, чтобы XML сериализовал объект, у которого есть (среди прочих) свойство типа IModelObject (который является интерфейсом).

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

Когда я пытаюсь сериализовать объект этого класса, я получаю следующую ошибку:
" Невозможно выполнить сериализацию элемента Example.Model типа Пример, потому что это интерфейс".

Я понимаю, что проблема в том, что интерфейс не может быть сериализован. Однако конкретный тип объекта модели неизвестен до времени выполнения.

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

Любые предложения?

Ответ 1

Это просто неотъемлемое ограничение декларативной сериализации, где информация о типе не встроена в выход.

При попытке конвертировать <Flibble Foo="10" /> обратно в

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

Как сериализатор знает, должен ли он быть int, строка, double (или что-то еще)...

Чтобы сделать эту работу, у вас есть несколько вариантов, но если вы действительно не знаете до времени выполнения, самый простой способ сделать это, скорее всего, будет использовать 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

Подобно первому варианту в том, что вы полностью контролируете вещи, но

  • Pros
    • У вас нет неприятных "поддельных" свойств, которые висят вокруг.
    • вы можете напрямую взаимодействовать с структурой xml, добавляя гибкость/управление версиями
  • Cons
    • вам может потребоваться повторное использование колеса для всех других свойств класса

Вопросы дублирования усилий схожи с первыми.

Измените свойство, чтобы использовать тип упаковки

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 в свойство "поддержки" первой техники. Таким образом, большая часть ворчания выполняется для вас, но потребители этого класса не страдают от путаницы с интроспекцией.

Ответ 2

Решением этого является использование отражения с помощью 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, перечисляет каждое свойство, возвращает каждое свойство, являющееся интерфейсом, получает значение этого свойства (базовый объект), получает тип этого конкретного объекта помещает его в массив и добавляет это в список сортировщиков известных типов.

Теперь сериализатор знает, как о типах, которые он сериализует, чтобы он мог выполнять свою работу.

Ответ 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;
}

Ответ 4

Вы можете использовать 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.

Ответ 5

К сожалению, нет простого ответа, поскольку сериализатор не знает, что сериализовать для интерфейса. Я нашел более полное объяснение того, как обходить это на MSDN

Ответ 6

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

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

Ответ 7

в моем проекте, у меня есть
Список <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));
        }
    }

Ответ 8

К несчастью для меня, у меня был случай, когда класс, который должен быть сериализован, имел свойства, которые имели интерфейсы как свойства, поэтому мне нужно было рекурсивно обрабатывать каждое свойство. Кроме того, некоторые свойства интерфейса были отмечены как [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;
}