3

Using a C# class generated from an XSD document, I can create an object, and serialize it successfully. However, some attributes have an XmlDefaultValue defined. If any objects have the default value, then those attributes do not get created when the object is serialized.

This is expected behavior according to the documentation. But this is not how I want it to behave. I need to have all such attributes generated in the XML document.
I've checked for any code attributes that can be applied that might force it to be outputted, even if it is the default value, but I couldn't find anything like that.

Is there any way to make that work?

slugster
  • 49,403
  • 14
  • 95
  • 145
Wedge
  • 887
  • 9
  • 12
  • Related: [How to tell XmlSerializer to serialize properties with `[DefautValue(…)]` always?](https://stackoverflow.com/q/15357589/3744182). – dbc May 23 '18 at 20:21

4 Answers4

2

You can do this for a specific set of types when serializing by constructing an XmlAttributeOverrides that specifies new XmlAttributes() { XmlDefaultValue = null } for every field or property that has DefaultValueAttribute applied, then passing this to the XmlSerializer(Type, XmlAttributeOverrides) constructor:

    var overrides = new XmlAttributeOverrides();
    var attrs = new XmlAttributes() { XmlDefaultValue = null };

    overrides.Add(typeToSerialize, propertyNameWithDefaultToIgnore, attrs);
    var serializer = new XmlSerializer(typeToSerialize, overrides);

Note, however, this important warning from the documentation:

Dynamically Generated Assemblies

To increase performance, the XML serialization infrastructure dynamically generates assemblies to serialize and deserialize specified types. The infrastructure finds and reuses those assemblies. This behavior occurs only when using the following constructors:

XmlSerializer.XmlSerializer(Type)

XmlSerializer.XmlSerializer(Type, String)

If you use any of the other constructors, multiple versions of the same assembly are generated and never unloaded, which results in a memory leak and poor performance. The easiest solution is to use one of the previously mentioned two constructors. Otherwise, you must cache the assemblies in a Hashtable, as shown in the following example.

However, the example given in the code doesn't give any suggestion of how to key the hashtable. It also isn't thread-safe. (Perhaps it dates from .Net 1.0?)

The following code creates a key scheme for xml serializers with overrides, and manufactures (via reflection) serializers for which the [DefaultValue] values (if any) of all properties and fields are overridden to be null, effectively cancelling the default value. Note, when creating a blank XmlAttributes() object all attributes are set to null. When overriding with this XmlAttributes object any attributes that are desired to stay need to be transferred into this new object:

public abstract class XmlSerializerKey
{
    static class XmlSerializerHashTable
    {
        static Dictionary<object, XmlSerializer> dict;

        static XmlSerializerHashTable()
        {
            dict = new Dictionary<object, XmlSerializer>();
        }

        public static XmlSerializer GetSerializer(XmlSerializerKey key)
        {
            lock (dict)
            {
                XmlSerializer value;
                if (!dict.TryGetValue(key, out value))
                    dict[key] = value = key.CreateSerializer();
                return value;
            }
        }
    }

    readonly Type serializedType;

    protected XmlSerializerKey(Type serializedType)
    {
        this.serializedType = serializedType;
    }

    public Type SerializedType { get { return serializedType; } }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(this, obj))
            return true;
        else if (ReferenceEquals(null, obj))
            return false;
        if (GetType() != obj.GetType())
            return false;
        XmlSerializerKey other = (XmlSerializerKey)obj;
        if (other.serializedType != serializedType)
            return false;
        return true;
    }

    public override int GetHashCode()
    {
        int code = 0;
        if (serializedType != null)
            code ^= serializedType.GetHashCode();
        return code;
    }

    public override string ToString()
    {
        return string.Format(base.ToString() + ": for type: " + serializedType.ToString());
    }

    public XmlSerializer GetSerializer()
    {
        return XmlSerializerHashTable.GetSerializer(this);
    }

    protected abstract XmlSerializer CreateSerializer();
}

public abstract class XmlserializerWithExtraTypesKey : XmlSerializerKey
{
    static IEqualityComparer<HashSet<Type>> comparer;

    readonly HashSet<Type> extraTypes = new HashSet<Type>();

    static XmlserializerWithExtraTypesKey()
    {
        comparer = HashSet<Type>.CreateSetComparer();
    }

    protected XmlserializerWithExtraTypesKey(Type serializedType, IEnumerable<Type> extraTypes)
        : base(serializedType)
    {
        if (extraTypes != null)
            foreach (var type in extraTypes)
                this.extraTypes.Add(type);
    }

    public Type[] ExtraTypes { get { return extraTypes.ToArray(); } }

    public override bool Equals(object obj)
    {
        if (!base.Equals(obj))
            return false;
        XmlserializerWithExtraTypesKey other = (XmlserializerWithExtraTypesKey)obj;
        return comparer.Equals(this.extraTypes, other.extraTypes);
    }

    public override int GetHashCode()
    {
        int code = base.GetHashCode();
        if (extraTypes != null)
            code ^= comparer.GetHashCode(extraTypes);
        return code;
    }
}

public sealed class XmlSerializerIgnoringDefaultValuesKey : XmlserializerWithExtraTypesKey
{
    readonly XmlAttributeOverrides overrides;

    private XmlSerializerIgnoringDefaultValuesKey(Type serializerType, IEnumerable<Type> ignoreDefaultTypes, XmlAttributeOverrides overrides)
        : base(serializerType, ignoreDefaultTypes)
    {
        this.overrides = overrides;
    }

    public static XmlSerializerIgnoringDefaultValuesKey Create(Type serializerType, IEnumerable<Type> ignoreDefaultTypes, bool recurse)
    {
        XmlAttributeOverrides overrides;
        Type [] typesWithOverrides;

        CreateOverrideAttributes(ignoreDefaultTypes, recurse, out overrides, out typesWithOverrides);
        return new XmlSerializerIgnoringDefaultValuesKey(serializerType, typesWithOverrides, overrides);
    }

    protected override XmlSerializer CreateSerializer()
    {
        var types = ExtraTypes;
        if (types == null || types.Length < 1)
            return new XmlSerializer(SerializedType);
        return new XmlSerializer(SerializedType, overrides);
    }

    static void CreateOverrideAttributes(IEnumerable<Type> types, bool recurse, out XmlAttributeOverrides overrides, out Type[] typesWithOverrides)
    {
        HashSet<Type> visited = new HashSet<Type>();
        HashSet<Type> withOverrides = new HashSet<Type>();
        overrides = new XmlAttributeOverrides();

        foreach (var type in types)
        {
            CreateOverrideAttributes(type, recurse, overrides, visited, withOverrides);
        }

        typesWithOverrides = withOverrides.ToArray();
    }

    static void CreateOverrideAttributes(Type type, bool recurse, XmlAttributeOverrides overrides, HashSet<Type> visited, HashSet<Type> withOverrides)
    {
        if (type == null || type == typeof(object) || type.IsPrimitive || type == typeof(string) || visited.Contains(type))
            return;
        var attrs = new XmlAttributes() { XmlDefaultValue = null };
        foreach (var property in type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public))
            if (overrides[type, property.Name] == null) // Check to see if overrides for this base type were already set.
                if (property.GetCustomAttributes<DefaultValueAttribute>(true).Any())
                {
                    withOverrides.Add(type);
                    overrides.Add(type, property.Name, attrs);
                }
        foreach (var field in type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public))
            if (overrides[type, field.Name] == null) // Check to see if overrides for this base type were already set.
                if (field.GetCustomAttributes<DefaultValueAttribute>(true).Any())
                {
                    withOverrides.Add(type);
                    overrides.Add(type, field.Name, attrs);
                }
        visited.Add(type);
        if (recurse)
        {
            var baseType = type.BaseType;
            if (baseType != type)
                CreateOverrideAttributes(baseType, recurse, overrides, visited, withOverrides);
        }
    }
}

And then you would call it like:

var serializer = XmlSerializerIgnoringDefaultValuesKey.Create(typeof(ClassToSerialize), new[] { typeof(ClassToSerialize), typeof(AdditionalClass1), typeof(AdditionalClass2), ... }, true).GetSerializer();

For example, in the following class hierarchy:

public class BaseClass
{
    public BaseClass() { Index = 1; }
    [DefaultValue(1)]
    public int Index { get; set; }
}

public class MidClass : BaseClass
{
    public MidClass() : base() { MidDouble = 1.0; }
    [DefaultValue(1.0)]
    public double MidDouble { get; set; }
}

public class DerivedClass : MidClass
{
    public DerivedClass() : base() { DerivedString = string.Empty; }
    [DefaultValue("")]
    public string DerivedString { get; set; }
}

public class VeryDerivedClass : DerivedClass
{
    public VeryDerivedClass() : base() { this.VeryDerivedIndex = -1; }
    [DefaultValue(-1)]
    public int VeryDerivedIndex { get; set; }
}

The default XmlSerializer produces:

<VeryDerivedClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />

But the custom serializer produces

<?xml version="1.0" encoding="utf-16"?>
<VeryDerivedClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Index>1</Index>
    <MidDouble>1</MidDouble>
    <DerivedString />
    <VeryDerivedIndex>-1</VeryDerivedIndex>
</VeryDerivedClass>

Finally, note that writing of null values is controlled by [XmlElement( IsNullable = true )] so writing of nulls is not affected by this serializer.

dbc
  • 104,963
  • 20
  • 228
  • 340
2

The last answer regarding DataContract is NOT the answer. The XSD is generated automatically and the person consuming the classes is not in control of the attributes used by the original author. The question was about auto-generated classes based on an XSD.

The other answer is problematic too because properties that have defaults defined also may not allow null values (this happens often). The only real solution is to have a serializer where you can tell it what properties to ignore with respect to serialization. This has been and always be a serious problem with current XML serializers that simply don't allow one to pass in what properties to force being serialized.

Actual scenario:

A REST service accepts XML in the body to update an object. The XML has an XSD defined by the author of the rest service. The current object stored by the rest service has a non-default value set. The users modifies the XML to change it back to the default... but the serialized version put in the body of the REST post skips the value and doesn't include it because its set to a default value.

What a quagmire... can't update the value because the logic behind not exporting default values completely ignores the idea that XML can be used to update an object, not just create new ones based on the XML. I can't believe its been this many years and nobody modified XML serializers to handle this basic scenario with ease.

Trevor
  • 21
  • 2
  • No, I answered my own question. The DataContract attribute is exactly the correct answer I was originally looking for. The DataContract was generated from an XSD, but it's not a continuous build, just a one-time generation, so there's no issue with custom modifying the class after it's been built for the first time. – Wedge May 22 '17 at 18:16
  • *This has been and always be a serious problem with current XML serializers that simply don't allow one to pass in what properties to force being serialized.* -- `XmlSerializer` does support conditional serialization, see [ShouldSerialize*() vs *Specified Conditional Serialization Pattern](https://stackoverflow.com/q/37838640/3744182). – dbc Mar 07 '20 at 20:02
0

Example how to force serialize all public properties with XmlDefaultValue attribute:

[Test]
public void GenerateXMLWrapTest()
{
  var xmlWrap = new XmlWrap();

  using (var sw = new StringWriter())
  {
    var overrides = new XmlAttributeOverrides();
    var attrs = new XmlAttributes { XmlDefaultValue = null };

    var type = typeof(XmlWrap);

    foreach (var propertyInfo in type.GetProperties())
    {
      if (propertyInfo.CanRead && propertyInfo.CanWrite && propertyInfo.GetCustomAttributes(true).Any(o => o is DefaultValueAttribute))
      {
        var propertyNameWithDefaultToIgnore = propertyInfo.Name;
        overrides.Add(type, propertyNameWithDefaultToIgnore, attrs);
      }
    }

    var serializer = new XmlSerializer(type, overrides);

    serializer.Serialize(sw, xmlWrap);
    sw.Flush();
    var xmlString = sw.ToString();
    Console.WriteLine(xmlString);
  }
}

Output:

<?xml version="1.0" encoding="utf-16"?>
<ConIdTranslator xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:devices-description-1.0">
  <Disabled>false</Disabled>
  <HostPortParams>COM1 baud=115200 parity=None data=8 stop=One</HostPortParams>
  <TranslatorObjectNumber>9000</TranslatorObjectNumber>
...

Where Disabled, HostPortParams, TranslatorObjectNumber public properties of serialized class has default value attribute:

  [Serializable]
  [XmlRoot("ConIdTranslator", Namespace = "urn:devices-description-1.0", IsNullable = false)]
  public class ConIdTranslatorXmlWrap : HardwareEntityXmlWrap
  {
    #region Fields

    [EditorBrowsable(EditorBrowsableState.Never)]
    [XmlIgnore]
    private string hostPortParams = "COM1 baud=115200 parity=None data=8 stop=One";

    [EditorBrowsable(EditorBrowsableState.Never)]
    [XmlIgnore]
    private bool disabled = false;

    ...

    #endregion

    #region Properties

    [XmlElement]
    [DefaultValue(false)]
    public bool Disabled
    {
      get => this.disabled;
      set
      {
        this.disabled = value;
        this.OnPropertyChanged("Disabled");
      }
    }

    [XmlElement]
    [DefaultValue("COM1 baud=115200 parity=None data=8 stop=One")]
    public string HostPortParams
    {
      get => this.hostPortParams;
      set
      {
        this.hostPortParams = value;
        this.OnPropertyChanged("HostPortParams");
      }
    }

    ...
vinogradniy
  • 126
  • 2
  • 4
-1

I found the answer: https://msdn.microsoft.com/en-us/library/system.runtime.serialization.datamemberattribute.emitdefaultvalue%28v=vs.110%29.aspx

Set the attribute in the DataContract like this: [DataMember(EmitDefaultValue=true)]

Wedge
  • 887
  • 9
  • 12