强制 XML XmlDefaultValue 值的序列化

Force XML serialization of XmlDefaultValue values

使用从 XSD 文档生成的 C# class,我可以创建一个对象,并成功地将其序列化。但是,某些属性定义了 XmlDefaultValue。如果任何对象具有默认值,则在序列化对象时不会创建这些属性。

根据 documentation,这是预期的行为。但这不是我希望它表现的方式。我需要在 XML 文档中生成所有此类属性。
我已经检查了任何可以应用的代码属性,这些属性可能会强制输出它,即使它是默认值,但我找不到类似的东西。

有什么方法可以实现吗?

您可以在通过构建 XmlAttributeOverrides that specifies new XmlAttributes() { XmlDefaultValue = null } for every field or property that has DefaultValueAttribute applied, then passing this to the XmlSerializer(Type, XmlAttributeOverrides) 构造函数进行序列化时针对一组特定类型执行此操作:

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

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

但是请注意,这个 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.

但是,代码中给出的示例没有给出任何关于如何键入哈希表的建议。它也不是线程安全的。 (也许它可以追溯到 .Net 1.0?)

以下代码为具有覆盖的 xml 序列化器创建密钥方案,并制造(通过反射)所有属性和字段的 [DefaultValue] 值(如果有)被覆盖的序列化器为空,有效地取消了默认值。请注意,在创建空白 XmlAttributes() 对象时,所有属性都设置为 null。当使用此 XmlAttributes 对象覆盖时,需要保留的任何属性都需要转移到此新对象中:

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

然后你会这样称呼它:

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

例如,在以下 class 层次结构中:

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

默认 XmlSerializer 产生:

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

但自定义序列化程序会生成

<?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>

最后,请注意空值的写入由 [XmlElement( IsNullable = true )] 控制,因此空值的写入不受此序列化程序的影响。

我找到了答案: https://msdn.microsoft.com/en-us/library/system.runtime.serialization.datamemberattribute.emitdefaultvalue%28v=vs.110%29.aspx

像这样在 DataContract 中设置属性:[DataMember(EmitDefaultValue=true)]

关于 DataContract 的最后一个答案不是答案。 XSD 是自动生成的,使用 类 的人无法控制原作者使用的属性。问题是关于基于 XSD.

自动生成的 类

另一个答案也有问题,因为定义了默认值的属性也可能不允许空值(这种情况经常发生)。唯一真正的解决方案是拥有一个序列化程序,您可以在其中告诉它在序列化方面要忽略哪些属性。这一直是当前 XML 序列化程序的一个严重问题,这些序列化程序根本不允许传入哪些属性以强制序列化。

实际场景:

REST 服务在正文中接受 XML 以更新对象。 XML 有一个 XSD 由 rest 服务的作者定义。剩余服务存储的当前对象设置了非默认值。用户修改 XML 以将其更改回默认值...但是放入 REST post 主体的序列化版本跳过该值并且不包含它,因为它设置为默认值值。

真是个泥潭...无法更新值,因为不导出默认值背后的逻辑完全忽略了 XML 可用于更新对象的想法,而不仅仅是基于创建新对象XML。我不敢相信这么多年了,却没有人修改 XML 序列化程序来轻松处理这种基本情况。

如何使用 XmlDefaultValue 属性强制序列化所有 public 属性的示例:

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

输出:

<?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>
...

序列化 class 的 Disabled、HostPortParams、TranslatorObjectNumber public 属性具有默认值属性:

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

    ...