Есть ли способ сохранить атрибуты XML при сериализации части класса?

Я пытаюсь сериализовать только часть класса. Я добавил XML-атрибуты к членам класса, чтобы сгенерированные XML-теги имели правильные имена, соответствующие спецификации, независимо от того, как называются мои свойства. Это отлично работает при сериализации основного класса. Однако, если я просто хочу сериализовать часть класса, я теряю XML-атрибуты, и имена возвращаются к значениям по умолчанию. Есть ли способ сохранить атрибуты XML при сериализации только части класса?

[XmlRoot ("someConfiguration")]
public class SomeConfiguration
{
    [XmlArray("bugs")]
    [XmlArrayItem("bug")]
    public List<string> Bugs { get; set; }
}

Когда я сериализую весь класс, я получаю это (что именно так, как я и ожидал):

<someConfiguration>
  <bugs>
    <bug>Bug1</bug>
    <bug>Bug2</bug>
    <bug>Bug3</bug>
  </bugs>
</someConfiguration>

Если я попытаюсь просто сериализовать часть класса «Ошибки», я получу это (обратите внимание, что атрибуты XML, которые изменяют имена тегов, игнорируются):

<ArrayOfString>
  <string>Bug1</string>
  <string>Bug2</string>
  <string>Bug3</string>
</ArrayOfString>

Мне нужно получить это:

  <bugs>
    <bug>Bug1</bug>
    <bug>Bug2</bug>
    <bug>Bug3</bug>
  </bugs>

Как мне получить разделяемый класс для сериализации с указанными выше тегами?

Или еще лучше, есть ли способ указать имена тегов при сериализации простого List<object>. Чтобы можно было указать тег, используемый для списка, вместо него с помощью <ArrayOfobject> и указать тег, используемый для элементов массива, вместо <object>?


person Curtis    schedule 15.03.2017    source источник


Ответы (3)


есть ли способ указать имена тегов при сериализации простого списка.

В общем, в зависимости от конкретного сценария, может быть возможно заставить это работать. См. в MSDN Как указать альтернативное имя элемента для XML-поток. Пример включает переопределение сериализации определенного поля, но можно использовать тот же метод и для переопределения целых имен типов.

Но мне кажется, что это очень большая проблема. Вместо этого, почему бы просто не обработать сериализацию явно:

private static string SerializeByLinqAndToString<T>(
    List<T> data, string rootName, string elementName)
{
    XDocument document = new XDocument(
        new XElement(rootName, data.Select(s => new XElement(elementName, s))));

    return SaveXmlToString(document);
}

private static string SaveXmlToString(XDocument document)
{
    StringBuilder sb = new StringBuilder();

    using (XmlWriter xmlWriter = XmlWriter.Create(sb,
        new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true }))
    {
        document.Save(xmlWriter);
    }

    return sb.ToString();
}

Звоните так:

SomeConfiguration config = ...; // initialize as desired

string result = SerializeByLinq(config.Bugs, "bug", "bugs");

Вышеприведенное работает только со списком строк или списком типов, где содержимое элемента может быть просто результатом вызова ToString() для экземпляра типа.

Использование полнофункциональных функций сериализации в .NET может оказаться целесообразным при работе со сложными типами, но если все, что у вас есть, — это простой список строк, функция LINQ-to-XML очень удобна.

Если у вас есть более сложные типы, вы можете преобразовать каждый элемент списка в XElement для DOM и сериализовать это:

private static string SerializeByLinq<T>(
    List<T> data, string rootName, string elementName = null)
{
    XDocument document = new XDocument(
        new XElement(rootName, data.Select(t =>
            ElementFromText(SerializeObject(t), elementName)
        )));

    return SaveXmlToString(document);
}

private static XElement ElementFromText(string xml, string name = null)
{
    StringReader reader = new StringReader(xml);
    XElement result = XElement.Load(reader);

    if (!string.IsNullOrEmpty(name))
    {
        result.Name = name;
    }

    return result;
}

private static string SerializeObject<T>(T o)
{
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
    StringWriter textWriter = new StringWriter();

    using (XmlWriter writer = XmlWriter.Create(textWriter,
        new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true }))
    {
        xmlSerializer.Serialize(writer, o,
            new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty}));
    }

    return textWriter.ToString();
}

Во втором примере вы можете опустить имя элемента для дочернего элемента, и он просто будет использовать то, что уже настроено для использования типа (например, имя типа или любое другое значение, заданное для [XmlRoot]).

person Peter Duniho    schedule 15.03.2017
comment
Я попробовал это с типом, отличным от строки, и он показывает представление «ToString ()» объектов «ошибка» вместо представления XML. - person Curtis; 16.03.2017
comment
@Кертис: ну да. Ваш вопрос касался сериализации списка значений string, а не других объектов. Другие сценарии обязательно будут более сложными. В этих случаях, как я уже упоминал, вы вполне можете обнаружить, что требуется метод переопределения сериализации, о котором я упоминал. - person Peter Duniho; 16.03.2017
comment
Я попытался вызвать сериализацию для «t», который вы передаете в новый XElement, но в итоге он заменил ‹ на < и › на > и т. д. - person Curtis; 16.03.2017
comment
@Curtis: да, это ожидаемо. Содержимое XElement должно быть сохранено, и поэтому, если оно выглядит как XML, его необходимо экранировать/изменить на сущности, чтобы обеспечить его сохранение. Вам придется расширить базовый метод LINQ-to-XML для дальнейшей сериализации объекта T в объекты XElement, которые будут включены в качестве содержимого. Вышеприведенное работает только для T, где T.ToString() дает желаемое значение (например, примитивные типы, такие как string, int и т. д., и пользовательские типы, которые работают аналогично). - person Peter Duniho; 16.03.2017
comment
@Curtis: Я ценю идею вашего редактирования, но как только вы обнаружите, что вызываете string.Replace() как способ работы с XML, вы обычно движетесь в неправильном направлении. Лучшим подходом было бы сгенерировать XML (например, с помощью метода Serialize<T>(), который у вас был), затем загрузить его с помощью XDocument и, наконец, скопировать этот DOM в создаваемый вами DOM. Если у меня будет время, я постараюсь добавить что-то вроде этого в ответ выше. - person Peter Duniho; 16.03.2017
comment
@Curtis: хорошо, я добавил этот пример. Делает то, что вы делали раньше, но без риска неправильного поиска и замены строки. - person Peter Duniho; 16.03.2017

Просто бросив это туда, вы можете обернуть List‹> внутри пользовательского класса:

[XmlRoot("config")]
public class SomeConfiguration
{
    [XmlElement("bugs")]
    public BugList Bugs { get; set; }
    [XmlElement("trees")]
    public TreeList Trees { get; set; }
}

[XmlRoot("bugs")]
public class BugList 
{
    [XmlElement("bug")]
    public List<string> Items = new List<string>();
}

[XmlRoot("trees")]
public class TreeList
{
    [XmlElement("tree")]
    public List<string> Items = new List<string>();
}   

Теперь это позволит вам сериализовать отдельные списки, и они будут укоренены, как вы и ожидали.

void Main()
{
    var config = new SomeConfiguration
    {
        Bugs = new BugList { Items = { "Bug1", "Bug2" } },
        Trees = new TreeList { Items = { "Tree1", "Tree2" } }
    };

    // Your config will work as normal.
    Debug.WriteLine(ToXml(config)); // <config> <bugs>.. <trees>..</config>

    // Your collections are now root-ed properly.
    Debug.WriteLine(ToXml(config.Bugs)); // <bugs><bug>Bug1</bug><bug>Bug2</bug></bugs>
    Debug.WriteLine(ToXml(config.Trees)); // <trees><tree>Tree1</tree><tree>Tree2</tree></trees>
}

public string ToXml<T>(T obj)
{
    var ser = new XmlSerializer(typeof(T));
    var emptyNs = new XmlSerializerNamespaces();
    emptyNs.Add("","");
    using (var stream = new MemoryStream())
    {
        ser.Serialize(stream, obj, emptyNs);
        return Encoding.ASCII.GetString(stream.ToArray());
    }
}
person NPras    schedule 15.03.2017

Нашел "обходной" способ сделать это.. Вместо того, чтобы помещать атрибуты XMLArray и XMLArrayList над списком‹>:

[XmlRoot ("someConfiguration")]
public class SomeConfiguration
{
    [XmlArray("bugs")]
    [XmlArrayItem("bug")]
    public List<string> Bugs { get; set; }
}

Поместите атрибут XmlElement в список, который будет указывать тег, который будет использоваться для каждого элемента, и не иметь тега, обертывающего список. Тег вашего класса сделает это за вас.

[XmlRoot ("bugs")]
public class SomeConfiguration
{
    [XmlElement("bug")]
    public List<string> Bugs { get; set; }
}

Когда вы сериализуете вышеизложенное, вы получите:

  <bugs>
    <bug>Bug1</bug>
    <bug>Bug2</bug>
    <bug>Bug3</bug>
  </bugs>
person Curtis    schedule 15.03.2017
comment
Как приведенное выше позволяет сериализовать только список? - person Peter Duniho; 16.03.2017
comment
Ты прав. Это своего рода взлом. Но в итоге получается результат, который я ищу. На самом деле я предпочитаю ваш метод, поскольку он НАМНОГО более общий и может использоваться для правильной сериализации части класса. Мне просто нужно выяснить, как показать сериализованный ‹t› вместо t.ToString(). - person Curtis; 16.03.2017