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

83

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

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

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

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

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

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

Elad
источник

Ответы:

116

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

При попытке преобразовать <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>

This is obviously more cumbersome for users of the class though avoids much boiler plate.

A happy medium may be merging the XmlAnything idea into the 'backing' property of the first technique. In this way most of the grunt work is done for you but consumers of the class suffer no impact beyond confusion with introspection.

ShuggyCoUk
источник
I tried to implement your approach witch wrapping properties but unfortunately have a problem :( Could you have a look at this post, please: stackoverflow.com/questions/7584922/…
SOReader
Are there any artical introducing FooSerialized property?
Gqqnbig
42

The solution to this is using reflection with the DataContractSerializer. You don't even have to mark your class with [DataContract] or [DataMember]. It will serialize any object, regardless of whether it has interface type properties (including dictionaries) into xml. Here is a simple extension method that will serialize any object into XML even if it has interfaces (note you could tweak this to run recursively as well).

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

what the LINQ expression does is it enumerates each property, returns each property that is an interface, gets the value of that property (the underlying object), gets the type of that concrete object puts it into an array, and adds that to the serializer's list of known types.

Now the serializer knows how about the types it is serializing so it can do its job.

Despertar
источник
Very elegant and easy solution to the problem. Thanks!
Ghlouw
2
This does not appear to work for a generic IList of and interface. e.g. IList<IMyInterface>. The concreate value for IMyInterface needs to be added to the KnownTypes however, instead the IList<IMyInterface> will be added.
galford13x
6
@galford13x I tried to make this example as simple as possible while still demonstrating the point. Adding in ever single case, like recursion or interface types makes it less clear to read and takes away from the main point. Please feel free to add any additional checks to pull the needed known types. To be honest I don't think there is anything you cant get using reflection. This for example will get the type of the generic parameter, stackoverflow.com/questions/557340/…
Despertar
I understand, I only mentioned this since the question asked for interface serialization. I figured I'd let others know the error would be expected without modification to prevent head banging on their part. I did appreciate your code, however, as I added the [KnownType()] attribute and your code lead me to the result.
galford13x
1
Is there a way to ommit the namesapce when serializing? I tried to use xmlwriterSettings using an xmlwriter instead, I use the overload where I can pass the addtional types, but it's not working...
Legends
9

If you know your interface implementors up-front there's a fairly simple hack you can use to get your interface type to serialize without writing any parsing code:

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

The resulting xml should look something along the lines of

 <interface><ofTypeKnownImplementor01><!-- etc... -->
hannasm
источник
1
Very useful, thanks. In most situations I know the classes that implement the interface. This answer should be higher up imo.
Jonah
This is the easiest solution. Thank you!
mKay
8

You can use ExtendedXmlSerializer. This serializer support serialization of interface property without any tricks.

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

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

var xml = serializer.Serialize(obj);

Your xml will look like:

<?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 support .net 4.5 and .net Core.

Wojtpl2
источник
3

Replacing the IModelObject interface with an abstract or concrete type and use inheritance with XMLInclude is possible, but seems like an ugly workaround.

If it is possible to use an abstract base I would recommend that route. It will still be cleaner than using hand-rolled serialization. The only trouble I see with the abstract base is that your still going to need the concrete type? At least that is how I've used it in the past, something like:

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;
}
csharptest.net
источник
2

Unfortunately there's no simple answer, as the serializer doesn't know what to serialize for an interface. I found a more complete explaination on how to workaround this on MSDN

MattH
источник
1

Unfortuantely for me, I had a case where the class to be serialized had properties that had interfaces as properties as well, so I needed to recursively process each property. Also, some of the interface properties were marked as [XmlIgnore], so I wanted to skip over those. I took ideas that I found on this thread and added some things to it to make it recursive. Only the deserialization code is shown here:

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;
}
acordner
источник
1

I have found a simpler solution (you don't need the DataContractSerializer), thanks to this blog here: XML serializing derived types when base type is in another namespace or DLL

But 2 problems can rise in this implementation:

(1) What if DerivedBase is not in the namespace of class Base, or even worse in a project that depends on Base namespace, so Base cannot XMLInclude DerivedBase

(2) What if we only have class Base as a dll ,so again Base cannot XMLInclude DerivedBase

Till now, ...

So the solution to the 2 problems is by using XmlSerializer Constructor (Type, array[]) :

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

A detailed example is provided here on MSDN: XmlSerializer Constructor (Type, extraTypesArray[])

It seems to me that for DataContracts or Soap XMLs, you need to check the XmlRoot as mentioned here in this SO question.

A similar answer is here on SO but it isn't marked as one, as it not the OP seems to have considered it already.

B Charles H
источник
0

in my project, I have a
List<IFormatStyle> FormatStyleTemplates;
containing different Types.

I then use the solution 'XmlAnything' from above, to serialize this list of different types. The generated xml is beautiful.

    [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));
        }
    }
Detlef Kroll
источник