How to exclude null properties when using XmlSerializer

|▌冷眼眸甩不掉的悲伤 提交于 2019-12-02 21:53:27

I suppose you could create an XmlWriter that filters out all elements with an xsi:nil attribute, and passes all other calls to the underlying true writer.

Allen Rice

You ignore specific elements with specification

public MyClass
{
    public int? a { get; set; }

    [System.Xml.Serialization.XmlIgnore]
    public bool aSpecified { get { return this.a != null; } }

    public int? b { get; set; }
    [System.Xml.Serialization.XmlIgnore]
    public bool bSpecified { get { return this.b != null; } }

    public int? c { get; set; }
    [System.Xml.Serialization.XmlIgnore]
    public bool cSpecified { get { return this.c != null; } }
}

The {field}Specified properties will tell the serializer if it should serialize the corresponding fields or not by returning true/false.

Better late than never...

I found a way (maybe only available with the latest framework I don't know) to do this. I was using DataMember attribute for a WCF webservice contract and I marked my object like this:

[DataMember(EmitDefaultValue = false)]
public decimal? RentPrice { get; set; }

Yet Another Solution: regex to the rescue, use \s+<\w+ xsi:nil="true" \/> to remove all null properties from a string containing XML. I agree, not the most elegant solution, and only works if you only have to serialize. But that was all I needed today, and I don't wanted to add {Foo}Specified properties for all the properties that are nullable.

public string ToXml()
{
    string result;

    var serializer = new XmlSerializer(this.GetType());

    using (var writer = new StringWriter())
    {
        serializer.Serialize(writer, this);
        result = writer.ToString();
    }

    serializer = null;

    // Replace all nullable fields, other solution would be to use add PropSpecified property for all properties that are not strings
    result = Regex.Replace(result, "\\s+<\\w+ xsi:nil=\"true\" \\/>", string.Empty);

    return result;
}

1) Extension

 public static string Serialize<T>(this T value) {
        if (value == null) {
            return string.Empty;
        }
        try {
            var xmlserializer = new XmlSerializer(typeof(T));
            var stringWriter = new Utf8StringWriter();
            using (var writer = XmlWriter.Create(stringWriter)) {
                xmlserializer.Serialize(writer, value);
                return stringWriter.ToString();
            }
        } catch (Exception ex) {
            throw new Exception("An error occurred", ex);
        }
    }

1a) Utf8StringWriter

public class Utf8StringWriter : StringWriter {
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}

2) Create XElement

XElement xml = XElement.Parse(objectToSerialization.Serialize());

3) Remove Nil's

xml.Descendants().Where(x => x.Value.IsNullOrEmpty() && x.Attributes().Where(y => y.Name.LocalName == "nil" && y.Value == "true").Count() > 0).Remove();

4) Save to file

xml.Save(xmlFilePath);

This question's been asked quite a long time ago but still is VERY relevant even in 2017. None of the proposed answers here weren't satisfactory to me so here's a simple solution I came up with:

Using regular expressions is the key. Since we haven't much control over the XmlSerializer's behavior, so let's NOT try to prevent it from serializing those nullable value types. Instead, just take the serialized output and replace the unwanted elements with an empty string using Regex. The pattern used (in C#) is:

<\w+\s+\w+:nil="true"(\s+xmlns:\w+="http://www.w3.org/2001/XMLSchema-instance")?\s*/>

Here's an example:

using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Serialization;

namespace MyNamespace
{
    /// <summary>
    /// Provides extension methods for XML-related operations.
    /// </summary>
    public static class XmlSerializerExtension
    {
        /// <summary>
        /// Serializes the specified object and returns the XML document as a string.
        /// </summary>
        /// <param name="obj">The object to serialize.</param>
        /// <param name="namespaces">The <see cref="XmlSerializerNamespaces"/> referenced by the object.</param>
        /// <returns>An XML string that represents the serialized object.</returns>
        public static string Serialize(this object obj, XmlSerializerNamespaces namespaces = null)
        {
            var xser = new XmlSerializer(obj.GetType());
            var sb = new StringBuilder();

            using (var sw = new StringWriter(sb))
            {
                using (var xtw = new XmlTextWriter(sw))
                {
                    if (namespaces == null)
                        xser.Serialize(xtw, obj);
                    else
                        xser.Serialize(xtw, obj, namespaces);
                }
            }

            return sb.ToString().StripNullableEmptyXmlElements();
        }

        /// <summary>
        /// Removes all empty XML elements that are marked with the nil="true" attribute.
        /// </summary>
        /// <param name="input">The input for which to replace the content.    </param>
        /// <param name="compactOutput">true to make the output more compact, if indentation was used; otherwise, false.</param>
        /// <returns>A cleansed string.</returns>
        public static string StripNullableEmptyXmlElements(this string input, bool compactOutput = false)
        {
            const RegexOptions OPTIONS =
            RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline;

            var result = Regex.Replace(
                input,
                @"<\w+\s+\w+:nil=""true""(\s+xmlns:\w+=""http://www.w3.org/2001/XMLSchema-instance"")?\s*/>",
                string.Empty,
                OPTIONS
            );

            if (compactOutput)
            {
                var sb = new StringBuilder();

                using (var sr = new StringReader(result))
                {
                    string ln;

                    while ((ln = sr.ReadLine()) != null)
                    {
                        if (!string.IsNullOrWhiteSpace(ln))
                        {
                            sb.AppendLine(ln);
                        }
                    }
                }

                result = sb.ToString();
            }

            return result;
        }
    }
}

I hope this helps.

If you make the class you want to serialise implement IXmlSerializable, you can use the following writer. Note, you will need to implement a reader, but thats not too hard.

    public void WriteXml(XmlWriter writer)
    {
        foreach (var p in GetType().GetProperties())
        {
            if (p.GetCustomAttributes(typeof(XmlIgnoreAttribute), false).Any())
                continue;

            var value = p.GetValue(this, null);

            if (value != null)
            {
                writer.WriteStartElement(p.Name);
                writer.WriteValue(value);
                writer.WriteEndElement();
            }
        }
    }

The simplest way of writing code like this where the exact output is important is to:

  1. Write an XML Schema describing your exact desired format.
  2. Convert your schema to a class using xsd.exe.
  3. Convert your class back to a schema (using xsd.exe again) and check it against your original schema to make sure that the serializer correctly reproduced every behaviour you want.

Tweak and repeat until you have working code.

If you are not sure of the exact data types to use initially, start with step 3 instead of step 1, then tweak.

IIRC, for your example you will almost certainly end up with Specified properties as you have already described, but having them generated for you sure beats writing them by hand. :-)

Mark the element with [XmlElement("elementName", IsNullable = false)] null values will be omitted.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!