26

I have an object Foo which I serialize to an XML stream.

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());

This works fast, easy and does everything currently required.

The problem I'm having is that I need to maintain a separate documentation file with some minor remarks. As in the above example, Name is obvious, but Version is the application version and not the data file version as one could expect in this case. And I have many more similar little things I want to clarify with a comment.

I know I can do this if I manually create my XML file using the WriteComment() function, but is there a possible attribute or alternative syntax I can implement so that I can keep using the serializer functionality?

dbc
  • 104,963
  • 20
  • 228
  • 340
Jensen
  • 3,498
  • 2
  • 26
  • 43
  • Distantly related: [Insert comment into XML after xml tag](http://stackoverflow.com/questions/2086326/c-xml-insert-comment-into-xml-after-xml-tag/2086466#2086466) – dtb Sep 12 '11 at 09:47
  • I'm having a lot of places where I want to insert a comment so I'd prefer not create a loop which iterates over all the elements, checks the name and inserts the appropriate comment. I also prefer to add them where the element is located, rather than a major comment block at the top of the file. – Jensen Sep 12 '11 at 09:52
  • @tyfius: You can't do that using XmlSerializer (or any other serializer) because the compliler doesn't preserve source code comments, there's simply no place after compilation where serializer can find them. – Igor Korkhov Sep 12 '11 at 10:02

6 Answers6

29

This is possible using the default infrastructure by making use of properties that return an object of type XmlComment and marking those properties with [XmlAnyElement("SomeUniquePropertyName")].

I.e. if you add a property to Foo like this:

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; }
}

The following XML will be generated:

<Foo>
  <!--The application version, NOT the file version!-->
  <Version>1.0</Version>
  <Name>Bar</Name>
</Foo>

However, the question is asking for more than this, namely some way to look up the comment in a documentation system. The following accomplishes this by using extension methods to look up the documentation based on the reflected comment property name:

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);
    }
}

For which the following XML is generated:

<Foo>
  <!--The application version, NOT the file version!-->
  <Version>1.0</Version>
  <!--The application name, NOT the file name!-->
  <Name>Bar</Name>
</Foo>

Notes:

  • The extension method XmlCommentExtensions.GetXmlCommentAttribute(this Type type, string memberName) assumes that the comment property will be named xxxXmlComment where xxx is the "real" property. If so, it can automatically determine the real property name by marking the incoming memberName attribute with CallerMemberNameAttribute. This can be overridden manually by passing in the real name.

  • Once the type and member name are known, the extension method looks up the relevant comment by searching for an [XmlComment] attribute applied to the property. This could be replaced with a cached lookup into a separate documentation file.

  • While it is still necessary to add the xxxXmlComment properties for each property that might be commented, this is likely to be less burdensome than implementing IXmlSerializable directly which is quite tricky, can lead to bugs in deserialization, and can require nested serialization of complex child properties.

  • To ensure that each comment precedes its associated element, see Controlling order of serialization in C#.

  • For XmlSerializer to serialize a property it must have both a getter and setter. Thus I gave the comment properties setters that do nothing.

Working .Net fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 3
    Nice solution, but I can't control at which point the comment appears in xml file. In my example I have 5 elements exported. Before first element the comment is declared but in xml file it shows up after fourth element. – Georg W. Jul 06 '18 at 09:32
  • @GeorgW. - Can you provide a [mcve] for that? Here's a fiddle with 5 elements exported that shows the comments being placed correctly: https://dotnetfiddle.net/nauWoo – dbc Jul 17 '18 at 04:43
  • This solution simply is AWESOME! No need to alter the serialization logic. It works for me at exactly the right places. Thanks a ton! – Nicolas Oct 10 '18 at 10:11
  • 1
    Using the 1st/simple solution, I found I had to specify `Order=` explicitly to get the comment to appear above the element. `[XmlAnyElement(Name = "XmlComment", Order = 0)]`. I just wanted a single comment near the top of the file. – tig Mar 30 '20 at 18:25
  • Hi, just wanted to comment that the code will only work on c# 5 and lower versions will miss some member options – Y.D Aug 17 '22 at 05:47
14

Isn't possible using default infrastructure. You need to implement IXmlSerializable for your purposes.

Very simple implementation:

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; }
}

Output:

<?xml version="1.0" encoding="utf-16"?>
<Foo>
  <!--The application version, NOT the file version!-->
  <Version>1.2</Version>
  <Name>A</Name>
</Foo>

Another way, maybe preferable: serialize with default serializer, then perform post-processing, i.e. update XML, e.g. using XDocument or XmlDocument.

Kirill Polishchuk
  • 54,804
  • 11
  • 122
  • 125
  • You can't serialize comments even if you implement IXmlSerializable, comments exist in source code only, they are not preserved after compilation. – Igor Korkhov Sep 12 '11 at 10:07
  • 2
    @Igor, OP can put comments inside custom attributes and read them using reflection. – Kirill Polishchuk Sep 12 '11 at 10:12
  • Yes, sure, it goes without saying, but the original question was about comments, wasn't it? `And I have many more similar little things I want to clarify with a comment.` I just wanted to clarify that there was no way to serialize comments. – Igor Korkhov Sep 12 '11 at 10:16
  • @Kirill Polishchuk, can you clarify that? So basically I want to do something like `[XmlComment("Application version, NOT file version")]public string Version {get;set;} or [XmlComment("Distance in meters")]public double Distance {get;set;}` and have it end printed out like `5000.01.0.0`. Any examples are appreciated. – Jensen Sep 12 '11 at 14:39
  • @tyfius, Exactly. You class `Foo` needs to implement `IXmlSerializable`, e.g.: `public class Foo : IXmlSerializable`. In method `WriteXml` you need to get attribute `XmlCommentAttribute` from your property, extract its value and write to XmlWriter `writer.WriteComment(attribute.Value)`. – Kirill Polishchuk Sep 12 '11 at 14:46
  • 1
    Hello kirill,I know I am a bit late, but I was wondering if you could also point me on a possible implementation of ReadXml() that will allow me to read the Comments in an XML. – Vova Feb 18 '13 at 11:26
  • @Vova I doubt that reading the values back to the attributes is possibly, because they are compile-time static (afair) –  Oct 22 '14 at 11:53
  • Hi, is there a way to use this method for nested xml? if you use this method for nested xml its just writing the name of the object as value and not doing drill down to the other objects in the object – Y.D Aug 18 '22 at 19:52
2

Add comment at the end of xml after serialization (magic is to flush 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();
    }
}
  • 1
    This is a very nice approach. You can also add the comment to the top of the file by putting the WriteComment call before Serialize. – Kflexior Feb 21 '23 at 08:35
0

Probably late to the party but I had problems when I was trying to deserialize using Kirill Polishchuk solution. Finally I decided to edit the XML after serializing it and the solution looks like:

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; }
}
Gabriel_ES
  • 13
  • 2
0

Proposed solution by user dbc looks fine, however it seems to need more manual work to create such comments than using an XmlWriter that knows how to insert comments based on XmlComment attributes.

See https://archive.codeplex.com/?p=xmlcomment - it seems you can pass such a writer to XmlSerializer and thus not have to implement your own serialization which could be tricky.

I did myself end up using dbc's solution though, nice and clean with no extra code. See https://dotnetfiddle.net/Bvbi0N. Make sure you provide a "set" accessor for the comment element (the XmlAnyElement). It doesn't need to have a name btw.

Update: better pass a unique name always, aka use [XmlAnyElement("someCommentElement")] instead of [XmlAnyElement]. Was using the same class with WCF and it was choking upon those XmlAnyElements that didn't have a name provided, even though I had [XmlIgnore, SoapIgnore, IgnoreDataMember] at all of them.

George Birbilis
  • 2,782
  • 2
  • 33
  • 35
0

for nested xml, I changed the method this way(for me i was having simple property as string(its possible to make it more complex in the logic)

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());

                }
            }
    }
Y.D
  • 95
  • 8