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


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

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

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

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

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

какие предложения?

9 70

9 ответов:

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

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

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

как сериализатор знает, должен ли он быть int, string, 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

похоже на первый вариант в том, что вы берете полный контроль над вещами, но

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

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

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

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

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

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

замена интерфейса 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;
}

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

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

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

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

но 2 проблемы могут возникнуть в этой реализации:

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

(2) что, если мы только есть база классов как dll, поэтому снова база не может XMLInclude DerivedBase

до сих пор ...

таким образом, решение двух проблем заключается в использовании XmlSerializer конструктор (тип, массив []):

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

подробный пример приведен здесь на MSDN: конструктор XmlSerializer (Type, extraTypesArray [])

Мне кажется, что для DataContracts или Soap XMLs вам нужно Регистрация XmlRoot, как упоминалось здесь в этом вопросе SO.

A подобный ответ здесь на SO но он не помечен как один, так как это не OP, похоже, уже рассмотрел его.