接口属性的XML序列化


83

我想对一个具有IModelObject类型的属性(这是一个接口)的对象进行XML序列化。

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

当我尝试序列化此类的对象时,出现以下错误:
“无法序列化Example类型的成员Example.Model,因为它是接口。”

我知道问题是接口无法序列化。但是,直到运行时才知道具体的Model对象类型。

可以将IModelObject接口替换为抽象类型或具体类型,并通过XMLInclude使用继承,但这似乎是一个丑陋的解决方法。

有什么建议么?

Answers:


116

这只是声明性序列化的固有限制,其中类型信息未嵌入输出中。

在尝试转换<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

与第一个选项类似,您可以完全控制事物,但是

  • 优点
    • 您没有令人讨厌的“假”属性。
    • 您可以直接与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的想法合并到第一个技术的“ backing”属性中。这样,大多数繁琐的工作都可以为您完成,但是该类的消费者不会因为自省而感到困惑。


我试图实现你的方法巫婆包装特性,但不幸的是有问题:(你能看看这篇文章,请: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 2012年

2
这似乎不适用于和接口的通用IList。例如IList <IMyInterface>。IMyInterface的concreate值需要添加到KnownTypes中,但是,将添加IList <IMyInterface>。
galford13x 2013年

6
@ galford13x我试图使此示例尽可能简单,同时仍要说明这一点。加上递归或接口类型之类的单一情况,使读取变得不那么清晰,并且远离要点。请随时添加任何其他检查以提取所需的已知类型。老实说,我认为使用反射无法获得任何东西。例如,这将得到泛型参数的类型,stackoverflow.com/questions/557340/...
神电

我了解,因为问题要求接口序列化,所以我只提到了这一点。我想让其他人知道如果不进行修改以防止头部撞到他们,就会发生该错误。但是,我确实感谢您的代码,因为我添加了[KnownType()]属性,并且您的代码将我引向了结果。
galford13x 2013年

1
有没有一种方法可以在序列化时忽略命名空间?我尝试使用xmlwriter代替使用xmlwriterSettings,我在重载处传递附加类型,但它不起作用...
传说

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
非常有用,谢谢。在大多数情况下,我知道实现接口的类。这个答案应该更高。
约拿(Jonah)

这是最简单的解决方案。谢谢!
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): 当基本类型位于另一个名称空间或DLL中时,XML序列化派生类型。

但是此实现可能会出现两个问题:

(1)如果DerivedBase不在Base类的命名空间中,或者甚至在依赖Base命名空间的项目中更糟,那么Base不能XMLInclude DerivedBase

(2)如果我们仅将Base类作为dll怎么办,那么Base也不能XMLInclude DerivedBase

直到现在, ...

因此,解决这两个问题的方法是使用XmlSerializer构造函数(Type,array [])

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

MSDN上的此处提供了一个详细的示例: XmlSerializer构造函数(类型,extraTypesArray [])

在我看来,对于DataContracts或Soap XML,您需要检查XmlRoot,如本SO问题中所述

SO上也有类似的答案,但未标记为一个,因为OP似乎尚未考虑过。


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));
        }
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.