Как написать комментарий к XML-файлу при использовании XmlSerializer? - PullRequest
19 голосов
/ 12 сентября 2011

У меня есть объект 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(), но есть ли возможный атрибут или альтернативный синтаксис, который я могу реализовать, чтобы я мог продолжать использовать функциональность сериализатора?

Ответы [ 3 ]

16 голосов
/ 30 сентября 2017

Это возможно при использовании инфраструктуры по умолчанию, используя свойства, которые возвращают объект типа 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 скрипка .

12 голосов
/ 12 сентября 2011

Невозможно использовать инфраструктуру по умолчанию. Вам нужно реализовать 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.

0 голосов
/ 25 октября 2017

Возможно, опоздал на вечеринку, но у меня были проблемы, когда я пытался десериализовать, используя решение Кирилла Полищука.Наконец, я решил отредактировать 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; }
}
...