开发者

How to exclude null properties when using XmlSerializer

开发者 https://www.devze.com 2022-12-08 02:53 出处:网络
I\'m serializing a class like this public MyClass { public int? a { get; set; } public int? b { get; set; }

I'm serializing a class like this

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

All of the types are nullable because I want minimal data stored when serializing an object of this type. However, when it is serialized with only "a" populated, I get the following xml

<MyClass ...>
    <a>3</a>
    <b xsi:nil="true" />
    <c xsi:nil="true" />
</MyClass>开发者_高级运维;

How do I set this up to only get xml for the non null properties? The desired output would be

<MyClass ...>
    <a>3</a>
</MyClass>

I want to exclude these null values because there will be several properties and this is getting stored in a database (yeah, thats not my call) so I want to keep the unused data minimal.


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.


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.


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


Somebody asked this question quite a long time ago, and it still seems 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, let's NOT try to prevent it from serializing those nullable value types. Instead, 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();
            }
        }
    }


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


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


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. :-)


If you can accept the overhead that this would bring, rather than serializing directly to string, write to a LINQ XDocument directly where you can post process the serialization. Using regular expressions like the other answers suggest will be very brittle.

I wrote this method to return the LINQ object, but you can always call ToString() on it.

public XElement XmlSerialize<T>(T obj)
{
    var doc = new XDocument();
    var serializer = new XmlSerializer(typeof(T));
    using (var writer = doc.CreateWriter())
        serializer.Serialize(writer, obj);
    doc.Descendants()
        .Where(x => (bool?)x.Attribute(XName.Get("nil", "http://www.w3.org/2001/XMLSchema-instance")) == true)
        .Remove();
    return doc.Root!;
}


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

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号