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

本文关键字:文件 XML 注释 时向 XmlSerializer | 更新日期: 2023-09-27 18:08:24

我有一个对象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()函数,但是否有一个可能的属性或替代语法,我可以实现,以便我可以继续使用序列化器功能?

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

这可以通过使用返回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来说,序列化一个属性必须同时拥有getter和setter。因此,我给注释属性设置不做任何事情。

工作。net提琴

使用默认基础结构是不可能的。您需要为您的目的实现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

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

可能太晚了,但是当我试图使用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; }
}

用户dbc提出的解决方案看起来不错,但是似乎需要更多的手工工作来创建这样的注释,而不是使用知道如何根据XmlComment属性插入注释的XmlWriter。

参见https://archive.codeplex.com/?p=xmlcomment -似乎您可以将这样的写入器传递给XmlSerializer,从而不必实现您自己的序列化,这可能是棘手的。

我最终使用了dbc的解决方案,没有额外的代码,很好很干净。见https://dotnetfiddle.net/Bvbi0N。确保你提供了一个"设置"。访问注释元素(XmlAnyElement)。它不需要有名字。

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

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