Подтвердить что ты не робот

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

4b9b3361

Ответ 1

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

Ответ 2

Это возможно с использованием инфраструктуры по умолчанию, используя свойства, которые возвращают объект типа 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) предполагает, что свойство comment будет называться xxxXmlComment, где xxx является "реальным" свойством. Если это так, он может автоматически определить имя реального свойства, пометив входящий атрибут memberName CallerMemberNameAttribute. Это можно переопределить вручную, передав настоящее имя.

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

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

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

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

Работа . Чистая скрипта.

Ответ 3

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