C# XML сериализация, коллекция и корневой элемент

Мое приложение сериализует объекты в потоках. Вот образец того, что мне нужно:

<links>
  <link href="/users" rel="users" />
  <link href="/features" rel="features" />
</links>

В этом случае объект является коллекцией объекта "ссылки".

----------- Первая версия

Сначала я использовал DataContractSerializer, однако вы не можете сериализовать элементы как атрибуты ( источник).

Вот объект:

[DataContract(Name="link")]
public class LinkV1
{
    [DataMember(Name="href")]
    public string Url { get; set; }

    [DataMember(Name="rel")]
    public string Relationship { get; set; }
}

И вот результат:

<ArrayOflink xmlns:i="...." xmlns="...">
  <link>
    <href>/users</href>
    <rel>users</rel>
  </link>
  <link>
    <href>/features</href>
    <rel>features</rel>
  </link>
</ArrayOflink>

----------- Вторая версия

Хорошо, не тихо, что я хочу, поэтому я попробовал классический XmlSerializer, но... ооооо, вы не можете указать имя корневого элемента & элементов коллекции, если корневой элемент является коллекцией...

Вот код:

[XmlRoot("link")]
public class LinkV2
{
    [XmlAttribute("href")]
    public string Url { get; set; }

    [XmlAttribute("rel")]
    public string Relationship { get; set; }
}

Вот результат:

<ArrayOfLinkV2>
  <LinkV2 href="/users" rel="users" />
  <LinkV2 href="/features" rel="features" />
  <LinkV2 href="/features/user/{keyUser}" rel="featuresByUser" />
</ArrayOfLinkV2>

----------- Третья версия

используя XmlSerializer + корневой элемент:

[XmlRoot("trick")]
public class TotallyUselessClass
{
    [XmlArray("links"), XmlArrayItem("link")]
    public List<LinkV2> Links { get; set; }
}

И его результат:

 <trick>
  <links>
    <link href="/users" rel="users" />
    <link href="/features" rel="features" />
    <link href="/features/user/{keyUser}" rel="featuresByUser" />
  </links>
</trick>

Хорошо, но я не хочу этот корневой узел!! Я хочу, чтобы моя коллекция была корневым узлом.

Вот ограничения:

  • код сериализации является общим, он работает с любым сериализуемым
  • обратная операция (десериализация) тоже должна работать
  • Я не хочу пересчитывать результат (я сериализуюсь прямо в потоке вывода)

Каковы мои решения сейчас:

  1. Кодирование моего собственного XmlSerializer
  2. Уловка XmlSerializer, когда он работает с коллекцией (я пытался найти XmlRootElement и настроить его для создания своего собственного XmlRootAttribute, но это вызывает проблемы при десериализации + имя элемента по-прежнему сохраняет имя класса)

Любая идея?

Что действительно беспокоит меня в этом вопросе, так это то, что я хочу, чтобы это было действительно очень просто...

3 ответа

Решение

Хорошо, вот мое окончательное решение (надеюсь, оно кому-нибудь поможет), которое может сериализовать простой массив, List<>, HashSet<>, ...

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

1) Используйте 'XmlType' на сериализуемом объекте

[XmlType("link")]
public class LinkFinalVersion
{
    [XmlAttribute("href")]
    public string Url { get; set; }

    [XmlAttribute("rel")]
    public string Relationship { get; set; }
}

2) Создайте код метода "smart-root-detector for for collection", который будет возвращать атрибут XmlRootAttribute

private XmlRootAttribute XmlRootForCollection(Type type)
{
    XmlRootAttribute result = null;

    Type typeInner = null;
    if(type.IsGenericType)
    {
        var typeGeneric = type.GetGenericArguments()[0];
        var typeCollection = typeof (ICollection<>).MakeGenericType(typeGeneric);
        if(typeCollection.IsAssignableFrom(type))
            typeInner = typeGeneric;
    }
    else if(typeof (ICollection).IsAssignableFrom(type)
        && type.HasElementType)
    {
        typeInner = type.GetElementType();
    }

    // yeepeeh ! if we are working with a collection
    if(typeInner != null)
    {
        var attributes = typeInner.GetCustomAttributes(typeof (XmlTypeAttribute), true);
        if((attributes != null)
            && (attributes.Length > 0))
        {
            var typeName = (attributes[0] as XmlTypeAttribute).TypeName + 's';
            result = new XmlRootAttribute(typeName);
        }
    }
    return result;
}

3) Вставьте этот XmlRootAttribute в сериализатор

// hack : get the XmlRootAttribute if the item is a collection
var root = XmlRootForCollection(type);
// create the serializer
var serializer = new XmlSerializer(type, root);

Я же говорил, это было сложно;)


Чтобы улучшить это, вы можете:

A) Создайте XmlTypeInCollectionAttribute, чтобы указать пользовательское имя корня (если базовое множественное число не соответствует вашим потребностям)

[XmlType("link")]
[XmlTypeInCollection("links")]
public class LinkFinalVersion
{
}

Б) Если возможно, кешируйте свой XmlSerializer (например, в статическом словаре).

В моем тестировании создание экземпляра XmlSerializer без XmlRootAttributes занимает 3 мс. Если вы укажете XmlRootAttribute, это займет около 80 мс (просто чтобы иметь имя собственного корневого узла!)

XmlSerializer должен уметь делать то, что вам нужно, но это сильно зависит от начальной структуры и настроек. Я использую его в своем собственном коде для генерации удивительно похожих вещей.

public class Links<Link> : BaseArrayClass<Link> //use whatever base collection extension you actually need here
{
    //...stuff...//
}

public class Link
{
    [XmlAttribute("href")]
    public string Url { get; set; }

    [XmlAttribute("rel")]
    public string Relationship { get; set; }
}

теперь, сериализация Links класс должен генерировать именно то, что вы ищете.

Проблема с XmlSerializer заключается в том, что когда вы даете ему дженерики, он отвечает дженериками. Список реализует Array где-то там, и сериализованный результат почти всегда будет ArrayOf<X>, Чтобы обойти это, вы можете назвать свойство или класс root. Закрытие того, что вам нужно, это, вероятно, вторая версия из ваших примеров. Я предполагаю, что вы пытались выполнить прямую сериализацию объекта List Links. Это не сработает, потому что вы не указали корневой узел. Теперь аналогичный подход можно найти здесь. В этом они указывают XmlRootAttribute при объявлении сериализатора. ваш будет выглядеть так:

XmlSerializer xs = new XmlSerializer(typeof(List<Link>), new XmlRootAttribute("Links"));

Ну вот...

 class Program
{
    static void Main(string[] args)
    {

        Links ls = new Links();
        ls.Link.Add(new Link() { Name = "Mike", Url = "www.xml.com" });
        ls.Link.Add(new Link() { Name = "Jim", Url = "www.xml.com" });
        ls.Link.Add(new Link() { Name = "Peter", Url = "www.xml.com" });

        XmlSerializer xmlSerializer = new XmlSerializer(typeof(Links));

        StringWriter stringWriter = new StringWriter();

        xmlSerializer.Serialize(stringWriter, ls);

        string serializedXML = stringWriter.ToString();

        Console.WriteLine(serializedXML);

        Console.ReadLine();
    }
}

[XmlRoot("Links")]
public class Links
{
    public Links()
    {
        Link = new List<Link>();
    }

    [XmlElement]
    public List<Link> Link { get; set; }
}

[XmlType("Link")]
public class Link
{
    [XmlAttribute("Name")]
    public string Name { get; set; }


    [XmlAttribute("Href")]
    public string Url { get; set; }

}
Другие вопросы по тегам