使用XmlSerializer时如何向XML文件添加注释?

27

我有一个对象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文件,我可以做到这一点,但是否存在可能的属性或替代语法,以便我可以继续使用序列化器功能?


相对无关:在 XML 标签后插入注释 - dtb
我有很多地方想插入注释,所以我不想创建一个循环来遍历所有元素,检查名称并插入适当的注释。我也更喜欢将它们添加到元素所在的位置,而不是在文件顶部添加一个主要的注释块。 - Jensen
@tyfius:你不能使用XmlSerializer(或任何其他序列化器)来做到这一点,因为编译器不会保留源代码注释,在编译后没有地方可以让序列化器找到它们。 - Igor Korkhov
生成XML文档时,开发人员可以直接从源代码中使用vs builder完成。有关详细信息,请参阅以下链接: https://learn.microsoft.com/pl-pl/dotnet/core/project-sdk/msbuild-props#documentationfile - undefined
6个回答

31

使用默认基础设施是可以实现的,通过使用返回类型为 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]属性查找相关注释。这可以替换为缓存查找到单独的文档文件中。

  • 虽然仍然需要为可能被注释的每个属性添加xxxXmlComment属性,但这很可能比直接实现IXmlSerializable要少负担得多,后者非常棘手,可能导致反序列化错误,并且可能需要复杂子属性的嵌套序列化。

  • 要确保每个注释在其关联元素之前,请参见控制C#中序列化的顺序

  • 对于XmlSerializer序列化属性,它必须同时具有getter和setter。因此,我给了注释属性一个什么都不做的setter。

正在使用 .Net fiddle 工作。


3
不错的解决方案,但我无法控制注释在xml文件中出现的位置。在我的示例中,我导出了5个元素,在第一个元素之前声明了注释,但在xml文件中它出现在第四个元素之后。 - Georg W.
@GeorgW. - 你能提供一个 [mcve] 吗?这里有一个包含5个元素的fiddle,展示了正确放置注释的情况:https://dotnetfiddle.net/nauWoo - dbc
这个解决方案真的太棒了!不需要改变序列化逻辑。它在我需要的地方完美地工作。非常感谢! - Nicolas
1
使用第一种/简单解决方案后,我发现我必须明确指定Order =才能使注释出现在元素上方。 [XmlAnyElement(Name =“ XmlComment”,Order = 0)]。我只想在文件顶部附近有一个注释。 - tig
嗨,只是想评论一下,这段代码只能在C# 5及以下版本上运行,更高版本会缺少一些成员选项。 - Y.D

14

使用默认基础架构是不可能的。为了达到您的目的,您需要实现 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,也无法序列化注释,因为注释仅存在于源代码中,在编译后不会被保留。 - Igor Korkhov
2
@Igor,OP可以在自定义属性中放置注释,并使用反射读取它们。 - Kirill Polishchuk
是的,当然,这是不言而喻的,但最初的问题是关于注释的,不是吗?我还有很多类似的小事情想通过注释澄清。 我只是想澄清没有办法序列化注释。 - Igor Korkhov
1
你好kirill,我知道我有点晚了,但我在想你是否能够指导我一下可能的ReadXml()实现方式,使我能够读取XML中的注释。 - Vova
你好,有没有办法用这种方法处理嵌套的XML?如果你用这种方法处理嵌套的XML,它只会将对象的名称作为值写入,而不会深入到对象中的其他对象。 - Y.D
显示剩余3条评论

2
在序列化后的XML结尾添加注释(关键是刷新XML编写器)。
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();
    }
}

1
这是一个非常好的方法。您还可以通过在序列化之前放置WriteComment调用来将注释添加到文件顶部。 - Kflexior

0

对于嵌套的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());

                }
            }
    }

0

可能有点晚了,但是我在尝试使用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; }
}

0

用户dbc提出的solution方案似乎不错,但是与使用一个XmlWriter相比,它似乎需要更多手动工作来创建这样的注释,该XmlWriter可以根据XmlComment属性插入注释。

请参见https://archive.codeplex.com/?p=xmlcomment - 看起来您可以将此类编写器传递给XmlSerializer,因此无需实现自己的序列化,这可能会很棘手。

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

更新:最好始终传递一个唯一的名称,即使用[XmlAnyElement(“someCommentElement”)]而不是[XmlAnyElement]。我正在使用相同的类与WCF,并且它在那些没有提供名称的XmlAnyElements上出现问题,即使我在它们所有地方都有[XmlIgnore、SoapIgnore、IgnoreDataMember]。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接