使用 XmlSerializer 时如何向 XML 文件写入注释?

Posted

技术标签:

【中文标题】使用 XmlSerializer 时如何向 XML 文件写入注释?【英文标题】:How to write a comment to an XML file when using the XmlSerializer? 【发布时间】:2021-09-07 18:20:11 【问题描述】:

我有一个对象 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 是应用程序版本,而不是在这种情况下可以预期的数据文件版本。我还有更多类似的小事想通过评论来澄清。

我知道如果我使用 WriteComment() 函数手动创建我的 XML 文件,我可以做到这一点,但是我可以实现一个可能的属性或替代语法,以便我可以继续使用序列化程序功能吗?

【问题讨论】:

远相关:Insert comment into XML after xml tag 我有很多地方想插入评论,所以我不想创建一个循环遍历所有元素、检查名称并插入适当的评论。我也更喜欢将它们添加到元素所在的位置,而不是文件顶部的主要注释块。 @tyfius:使用 XmlSerializer(或任何其他序列化器)无法做到这一点,因为编译器不保留源代码 cmets,编译后根本没有地方可以找到序列化器。跨度> 【参考方案1】:

这可以通过使用返回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 是“真实”属性。如果是这样,它可以通过用CallerMemberNameAttribute 标记传入的memberName 属性来自动确定真实属性名称。这可以通过传入真实姓名手动覆盖。

一旦知道类型和成员名称,扩展方法就会通过搜索应用于属性的[XmlComment] 属性来查找相关注释。这可以替换为缓存查找到单独的文档文件。

1234563可能需要复杂子属性的嵌套序列化。

要确保每条评论都位于其关联元素之前,请参阅Controlling order of serialization in C#。

XmlSerializer 要序列化一个属性,它必须同时具有 getter 和 setter。因此,我给出了什么都不做的评论属性设置器。

工作.Net fiddle。

【讨论】:

不错的解决方案,但我无法控制评论出现在 xml 文件中的哪个位置。在我的示例中,我导出了 5 个元素。在第一个元素之前声明了注释,但在 xml 文件中它显示在第四个元素之后。 @GeorgW。 - 你能提供一个minimal reproducible example吗?这是一个导出了 5 个元素的小提琴,显示了正确放置的 cmets:dotnetfiddle.net/nauWoo 这个解决方案简直太棒了!无需更改序列化逻辑。它在正确的地方对我有用。非常感谢! 使用第一个/简单的解决方案,我发现我必须明确指定Order= 才能让评论出现在元素上方。 [XmlAnyElement(Name = "XmlComment", Order = 0)]。我只想在文件顶部附近添加一条评论。【参考方案2】:

无法使用默认基础架构。你需要为你的目的实现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,例如使用XDocumentXmlDocument

【讨论】:

即使实现了IXmlSerializable也不能序列化cmets,cmets只存在于源码中,编译后不保存。 @Igor,OP 可以将 cmets 放入自定义属性中并使用反射读取它们。 是的,当然,这是不言而喻的,但最初的问题是关于 cmets,不是吗? And I have many more similar little things I want to clarify with a comment.我只是想澄清一下,没有办法序列化 cmets。 @Kirill Polishchuk,你能澄清一下吗?所以基本上我想做像[XmlComment("Application version, NOT file version")]public string Version get;set; or [XmlComment("Distance in meters")]public double Distance get;set; 这样的事情,然后像&lt;Distance&gt;5000.0&lt;/Distance&gt;&lt;!-- Distance in meters --&gt;&lt;Version&gt;1.0.0&lt;/Version&gt;&lt;!-- Application version, NOT file version --&gt; 这样打印出来。任何例子都值得赞赏。 您好 kirill,我知道我来晚了,但我想知道您是否还可以指出 ReadXml() 的可能实现,它允许我阅读 XML 中的注释。 【参考方案3】:

序列化后在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();
    

【讨论】:

【参考方案4】:

可能迟到了,但是当我尝试使用 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; 

【讨论】:

【参考方案5】:

用户 dbc 提出的 solution 看起来不错,但与使用知道如何基于 XmlComment 属性插入 cmets 的 XmlWriter 相比,创建此类 cmets 似乎需要更多的手动工作。

请参阅https://archive.codeplex.com/?p=xmlcomment - 似乎您可以将这样的编写器传递给 XmlSerializer,因此不必实现自己的序列化,这可能很棘手。

我自己最终还是使用了 dbc 的解决方案,它干净整洁,没有额外的代码。见https://dotnetfiddle.net/Bvbi0N。确保为注释元素(XmlAnyElement)提供“set”访问器。顺便说一句,它不需要有名字。

更新:最好始终传递一个唯一的名称,也就是使用 [XmlAnyElement("someCommentElement")] 而不是 [XmlAnyElement]。正在使用与 WCF 相同的类,并且它对那些没有提供名称的 XmlAnyElements 感到窒息,即使我有 [XmlIgnore, SoapIgnore, IgnoreDataMember]。

【讨论】:

以上是关于使用 XmlSerializer 时如何向 XML 文件写入注释?的主要内容,如果未能解决你的问题,请参考以下文章

如何通过对 XmlSerializer.Serialize 的调用创建 XmlNode?

XMLSerializer 不反序列化 XML

XmlSerializer和复杂前缀

导入 org.apache.xml.serialize.XMLSerializer 无法解析

XmlSerializer 在文件中加载“作为”反序列化期间 - 当 XML 包含希伯来语时

如何在XML序列化时隐藏可为null的的字段