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