Как написать комментарий к XML-файлу при использовании XmlSerializer?

У меня есть объект Foo, который я сериализую в поток XML.

public class Foo {
  // The application version, NOT the file version!
  public string Version {get;set;}
  public string Name {get;set;}
}

Foo foo = new Foo { Version = "1.0", Name = "Bar" };
XmlSerializer xmlSerializer = new XmlSerializer(foo.GetType());

Это работает быстро, легко и делает все, что требуется в настоящее время.

У меня проблема в том, что мне нужно вести отдельный файл документации с некоторыми незначительными замечаниями. Как и в приведенном выше примере, Name очевидно, но Version это версия приложения, а не версия файла данных, как можно было бы ожидать в этом случае. И у меня есть еще много подобных мелочей, которые я хочу уточнить с комментарием.

Я знаю, что могу сделать это, если я вручную создаю свой XML-файл, используя WriteComment() функция, но есть ли возможный атрибут или альтернативный синтаксис, который я могу реализовать, чтобы я мог продолжать использовать функциональность сериализатора?

6 ответов

Решение

Невозможно использовать инфраструктуру по умолчанию. Вам необходимо реализовать IXmlSerializable для ваших целей.

Очень простая реализация:

public class Foo : IXmlSerializable
{
    [XmlComment(Value = "The application version, NOT the file version!")]
    public string Version { get; set; }
    public string Name { get; set; }


    public void WriteXml(XmlWriter writer)
    {
        var properties = GetType().GetProperties();

        foreach (var propertyInfo in properties)
        {
            if (propertyInfo.IsDefined(typeof(XmlCommentAttribute), false))
            {
                writer.WriteComment(
                    propertyInfo.GetCustomAttributes(typeof(XmlCommentAttribute), false)
                        .Cast<XmlCommentAttribute>().Single().Value);
            }

            writer.WriteElementString(propertyInfo.Name, propertyInfo.GetValue(this, null).ToString());
        }
    }
    public XmlSchema GetSchema()
    {
        throw new NotImplementedException();
    }

    public void ReadXml(XmlReader reader)
    {
        throw new NotImplementedException();
    }
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class XmlCommentAttribute : Attribute
{
    public string Value { get; set; }
}

Выход:

<?xml version="1.0" encoding="utf-16"?>
<Foo>
  <!--The application version, NOT the file version!-->
  <Version>1.2</Version>
  <Name>A</Name>
</Foo>

Другой способ, может быть предпочтительнее: сериализовать с помощью сериализатора по умолчанию, затем выполнить постобработку, т.е. обновить XML, например, используя XDocument или же XmlDocument,

Это возможно при использовании инфраструктуры по умолчанию, используя свойства, которые возвращают объект типа XmlComment и пометив эти свойства с [XmlAnyElement("SomeUniquePropertyName")],

Т.е. если вы добавите свойство в Foo как это:

public class Foo
{
    [XmlAnyElement("VersionComment")]
    public XmlComment VersionComment { get { return new XmlDocument().CreateComment("The application version, NOT the file version!"); } set { } }

    public string Version { get; set; }
    public string Name { get; set; }
}

Будет сгенерирован следующий XML:

<Foo>
  <!--The application version, NOT the file version!-->
  <Version>1.0</Version>
  <Name>Bar</Name>
</Foo>

Тем не менее, вопрос требует больше, чем это, а именно какой-то способ поиска комментария в системе документации. Следующее достигается с помощью методов расширения для поиска документации на основе имени свойства отраженного комментария:

public class Foo
{
    [XmlAnyElement("VersionXmlComment")]
    public XmlComment VersionXmlComment { get { return GetType().GetXmlComment(); } set { } }

    [XmlComment("The application version, NOT the file version!")]
    public string Version { get; set; }

    [XmlAnyElement("NameXmlComment")]
    public XmlComment NameXmlComment { get { return GetType().GetXmlComment(); } set { } }

    [XmlComment("The application name, NOT the file name!")]
    public string Name { get; set; }
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class XmlCommentAttribute : Attribute
{
    public XmlCommentAttribute(string value)
    {
        this.Value = value;
    }

    public string Value { get; set; }
}

public static class XmlCommentExtensions
{
    const string XmlCommentPropertyPostfix = "XmlComment";

    static XmlCommentAttribute GetXmlCommentAttribute(this Type type, string memberName)
    {
        var member = type.GetProperty(memberName);
        if (member == null)
            return null;
        var attr = member.GetCustomAttribute<XmlCommentAttribute>();
        return attr;
    }

    public static XmlComment GetXmlComment(this Type type, [CallerMemberName] string memberName = "")
    {
        var attr = GetXmlCommentAttribute(type, memberName);
        if (attr == null)
        {
            if (memberName.EndsWith(XmlCommentPropertyPostfix))
                attr = GetXmlCommentAttribute(type, memberName.Substring(0, memberName.Length - XmlCommentPropertyPostfix.Length));
        }
        if (attr == null || string.IsNullOrEmpty(attr.Value))
            return null;
        return new XmlDocument().CreateComment(attr.Value);
    }
}

Для которого генерируется следующий XML:

<Foo>
  <!--The application version, NOT the file version!-->
  <Version>1.0</Version>
  <!--The application name, NOT the file name!-->
  <Name>Bar</Name>
</Foo>

Заметки:

  • Метод расширения XmlCommentExtensions.GetXmlCommentAttribute(this Type type, string memberName) предполагает, что свойство комментария будет названо xxxXmlComment где xxx это "реальная" собственность. Если это так, он может автоматически определить имя недвижимости, пометив входящие memberName приписать с CallerMemberNameAttribute, Это можно изменить вручную, передав реальное имя.

  • Как только тип и имя члена известны, метод расширения ищет соответствующий комментарий, ища [XmlComment] Атрибут применяется к свойству. Это может быть заменено кэшированным поиском в отдельный файл документации.

  • Пока еще нужно добавить xxxXmlComment свойства для каждого свойства, которые могут быть прокомментированы, это, вероятно, будет менее обременительным, чем реализация IXmlSerializable непосредственно, что довольно сложно, может привести к ошибкам при десериализации и может потребовать вложенной сериализации сложных дочерних свойств.

  • Чтобы гарантировать, что каждый комментарий предшествует связанному с ним элементу, см. Контроль порядка сериализации в C#.

  • За XmlSerializer чтобы сериализовать свойство, оно должно иметь как геттер, так и сеттер. Таким образом я дал в комментарии свойства сеттеров, которые ничего не делают.

Рабочая .Net скрипка.

Добавьте комментарий в конце xml после сериализации (магия - очистить xmlWriter).

byte[] buffer;

XmlSerializer serializer = new XmlSerializer(result.GetType());

var settings = new XmlWriterSettings() { Encoding = Encoding.UTF8 };

using (MemoryStream memoryStream = new MemoryStream())
{
    using (XmlWriter xmlWriter = XmlWriter.Create(memoryStream, settings))
    {
        serializer.Serialize(xmlWriter, result);

        xmlWriter.WriteComment("test");

        xmlWriter.Flush();

        buffer = memoryStream.ToArray();
    }
}

для вложенного xml я изменил метод таким образом (для меня у меня было простое свойство в виде строки (можно сделать его более сложным в логике)

      public void WriteXml(XmlWriter writer)
    {
        var properties = GetType().GetProperties();

        foreach (var propertyInfo in properties)
        {
            if (propertyInfo.IsDefined(typeof(XmlCommentAttribute), false))
            {
                writer.WriteComment(
                    propertyInfo.GetCustomAttributes(typeof(XmlCommentAttribute), false)
                        .Cast<XmlCommentAttribute>().Single().Value);
            }

                if (propertyInfo.GetValue(this, null).GetType().ToString() != "System.String")
                {
                    XmlSerializer xmlSerializer = new XmlSerializer(propertyInfo.GetValue(this, null).GetType());
                    xmlSerializer.Serialize(writer, propertyInfo.GetValue(this, null));
                }
                else
                {
                    writer.WriteElementString(propertyInfo.Name, propertyInfo.GetValue(this, null).ToString());

                }
            }
    }

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

public static void WriteXml(object objectToSerialize, string path)
{
    try
    {
        using (var w = new XmlTextWriter(path, null))
        {
            w.Formatting = Formatting.Indented;
            var serializer = new XmlSerializer(objectToSerialize.GetType());
            serializer.Serialize(w, objectToSerialize);
        }

        WriteComments(objectToSerialize, path);
    }
    catch (Exception e)
    {
        throw new Exception($"Could not save xml to path {path}. Details: {e}");
    }
}

public static T ReadXml<T>(string path) where T:class, new()
{
    if (!File.Exists(path))
        return null;
    try
    {
        using (TextReader r = new StreamReader(path))
        {
            var deserializer = new XmlSerializer(typeof(T));
            var structure = (T)deserializer.Deserialize(r);
            return structure;
        }
    }
    catch (Exception e)
    {
        throw new Exception($"Could not open and read file from path {path}. Details: {e}");
    }
}

private static void WriteComments(object objectToSerialize, string path)
{
    try
    {
        var propertyComments = GetPropertiesAndComments(objectToSerialize);
        if (!propertyComments.Any()) return;

        var doc = new XmlDocument();
        doc.Load(path);

        var parent = doc.SelectSingleNode(objectToSerialize.GetType().Name);
        if (parent == null) return;

        var childNodes = parent.ChildNodes.Cast<XmlNode>().Where(n => propertyComments.ContainsKey(n.Name));
        foreach (var child in childNodes)
        {
            parent.InsertBefore(doc.CreateComment(propertyComments[child.Name]), child);
        }

        doc.Save(path);
    }
    catch (Exception)
    {
        // ignored
    }
}

private static Dictionary<string, string> GetPropertiesAndComments(object objectToSerialize)
{
    var propertyComments = objectToSerialize.GetType().GetProperties()
        .Where(p => p.GetCustomAttributes(typeof(XmlCommentAttribute), false).Any())
        .Select(v => new
        {
            v.Name,
            ((XmlCommentAttribute) v.GetCustomAttributes(typeof(XmlCommentAttribute), false)[0]).Value
        })
        .ToDictionary(t => t.Name, t => t.Value);
    return propertyComments;
}

[AttributeUsage(AttributeTargets.Property)]
public class XmlCommentAttribute : Attribute
{
    public string Value { get; set; }
}

Предлагаемое решение по пользователю DBC выглядит хорошо, однако, кажется, нужно больше ручной работы, чтобы создать такие комментарии, чем при использовании XmlWriter, который знает, как вставлять комментарии, основанные на XmlComment атрибутов.

См. https://archive.codeplex.com/?p=xmlcomment - кажется, вы можете передать такой писатель в XmlSerializer и, таким образом, вам не придется реализовывать собственную сериализацию, что может быть сложно.

Я все же в конечном итоге использовал решение dbc, красивое и чистое, без лишнего кода. См. https://dotnetfiddle.net/Bvbi0N. Убедитесь, что вы предоставили средство доступа "set" для элемента комментария (XmlAnyElement). Кстати, у него не обязательно должно быть имя.

Обновление: лучше всегда передавать уникальное имя, иначе используйте [XmlAnyElement("someCommentElement")] вместо [XmlAnyElement]. Я использовал один и тот же класс с WCF, и он задыхался от тех XmlAnyElements, которым не было предоставлено имя, хотя у меня были [XmlIgnore, SoapIgnore, IgnoreDataMember] на всех из них.

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