如何使用与继承祖先的命名空间匹配的明确定义的命名空间序列化 XML?

How to serialize XML with explicitly defined namespaces that matches inherited ancestor's namespace?

TLDR 版本

我正在将对象序列化为 XML 以匹配第三方提供的模式。他们的验证器要求其中一个子对象具有显式声明的名称空间,该名称空间与其祖先的名称空间相匹配。数据非常复杂,我不想为此目的推出自己的序列化程序。我如何强制 XMLSerializer class 显式呈现命名空间,即使它在技术上是多余的?

完整版

我 运行 遇到 XMLSerializer 不呈现 CoreItemsMkt 命名空间的问题。我相信这是因为属性和命名空间都与它继承的祖先命名空间完全匹配,因此序列化程序忽略了它 - 但是,提交此文件的站点验证器需要它。

例如:

<?xml version="1.0" encoding="utf-8"?>
<FSAMarketsFeed xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2">
 <FSAFeedHeader xmlns="http://www.fsa.gov.uk/XMLSchema/FSAFeedCommon-v1-2">
  [...contents omitted, this item appears once...]
 </FSAFeedHeader>
 <FSAMarketsFeedMsg>
   <CoreItemsMkt xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2"> <!--//This namespace is the issue//-->
    [...contents omitted, this item appears multiple times...]
   </CoreItemsMkt?
 </FSAMarketsFeedMsg>
 <FSAMarketsFeedMsg>
   <CoreItemsMkt xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2"> <!--//This namespace is the issue//-->
    [...contents omitted, this item appears multiple times...]
   </CoreItemsMkt?
 </FSAMarketsFeedMsg>

我正在使用这样的方法进行序列化:

        var path = GetFilePath();

        var ns = new XmlSerializerNamespaces();
        ns.Add("", "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2");

        var ser = new XmlSerializer(typeof(FSAMarketsFeed));
        var settings = new XmlWriterSettings
        { Encoding = Encoding.UTF8, Indent = true, IndentChars = "\t", NamespaceHandling = NamespaceHandling.Default };
        using (var writer = XmlWriter.Create(path, settings))
        {
            ser.Serialize(writer, GetDataToSerialize(), ns);
        }

我的根 class 定义为:

[XmlType(AnonymousType = true)]
[XmlRoot(Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2", IsNullable = false)]
public class FSAMarketsFeed
{
    public FSAMarketsFeed()
    {
        FSAMarketsFeedMsg = new FSAMarketsFeedMsg[0];
    }

    [XmlElement("FSAFeedHeader", IsNullable = true, Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAFeedCommon-v1-2")]
    public FSAFeedHeader FeedHeader { get; set; }

    [XmlElement("FSAMarketsFeedMsg")]
    public FSAMarketsFeedMsg[] FSAMarketsFeedMsg { get; set; }
}

工作提要页眉class:

[XmlType(AnonymousType = true)]
public class FSAFeedHeader
{
    [XmlElement("FeedTargetSchemaVersion", IsNullable = true)]
    public string FeedTargetSchemaVersion { get; set; }

    [XmlElement("Submitter", IsNullable = true)]
    public Submitter Submit { get; set; }

    [XmlElement("ReportDetails", IsNullable = true)]
    public ReportDetails ReportDetail { get; set; }
}

父源消息Class:

[XmlType(AnonymousType = true)]
public class FSAMarketsFeedMsg
{
    [XmlElement("CoreItemsMkt", IsNullable = true, Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2")]
    public CoreItemsMkt CoreMarket { get; set; }

    [XmlElement("Transaction", IsNullable = true)]
    public Transaction Trans { get; set; }
}

最后,无法呈现其命名空间的 CoreItemsMkt class:

[XmlType(Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2", AnonymousType = true)]
public class CoreItemsMkt
{
    //[... Children omitted ...]]
}

到目前为止尝试过:

那么,如果不手动渲染,是否有任何方法可以强制 XmlSerializer class 在 CoreItmsMkt 上渲染那些命名空间属性?

您希望在序列化指定的嵌套元素时能够强制 XmlSerializer 发出冗余的 xmlns= 属性。不幸的是,我不知道有什么 API 可以自动实现这一点。您还写了 数据非常复杂,我不想为此目的推出自己的序列化程序,所以您不想在 [= 上实现 IXmlSerializable 18=]。 (ISerializable 未被 XmlSerializer 使用,因此实施它无济于事。)因此您将想要做一些事情 "semi-manual"。为此至少有几个选项。

选项 1:序列化为临时 XDocument 然后修复属性。

使用此解决方案,您序列化到内存中的临时 XDocument,然后为每个所需的冗余 xmlns= 添加一个 XAttribute,如下所示:

// Generate the temporary XDocument
var ns = Namespaces.GetFSAMarketsFeedNamespace();
var doc = data.SerializeToXDocument(null, ns);
var root = doc.Root;

// Add redundate xmlns= attributes
var name = XName.Get("CoreItemsMkt", Namespaces.FSAMarketsFeed);
var query = doc.Descendants(name); // Could be a more complex query, possibly even an XPath query.

foreach (var element in query)
{
    if (!element.Attributes().Any(a => a.IsNamespaceDeclaration))
    {
        var prefix = element.GetPrefixOfNamespace(element.Name.Namespace);
        if (string.IsNullOrEmpty(prefix))
            element.Add(new XAttribute("xmlns", element.Name.NamespaceName));
        else
            element.Add(new XAttribute(XNamespace.Xmlns + prefix, element.Name.NamespaceName));
    }
}

// Write the XDocument to disk.

使用静态扩展 类:

public static class Namespaces
{
    public const string FSAMarketsFeed = @"http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2";
    public const string FSAFeedCommon = @"http://www.fsa.gov.uk/XMLSchema/FSAFeedCommon-v1-2";

    public static XmlSerializerNamespaces GetFSAMarketsFeedNamespace()
    {
        var ns = new XmlSerializerNamespaces();
        ns.Add("", Namespaces.FSAMarketsFeed);
        return ns;
    }
}

public static class XObjectExtensions
{
    public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
    {
        using (var reader = element.CreateReader())
        {
            serializer = serializer ?? new XmlSerializer(typeof(T));
            object result = serializer.Deserialize(reader);
            if (result is T)
                return (T)result;
        }
        return default(T);
    }

    public static XDocument SerializeToXDocument<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
    {
        var doc = new XDocument();
        using (var writer = doc.CreateWriter())
        {
            serializer = serializer ?? new XmlSerializer(obj.GetType());
            serializer.Serialize(writer, obj, ns);
        }
        return doc;
    }

    public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
    {
        var doc = obj.SerializeToXDocument(serializer, ns);
        var element = doc.Root;
        if (element != null)
            element.Remove();
        return element;
    }
}

产生 XML:

<FSAMarketsFeed xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2">
  <FSAFeedHeader xmlns="http://www.fsa.gov.uk/XMLSchema/FSAFeedCommon-v1-2">
    <FeedTargetSchemaVersion>value of FeedTargetSchemaVersion</FeedTargetSchemaVersion>
  </FSAFeedHeader>
  <FSAMarketsFeedMsg>
    <CoreItemsMkt xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2" />
  </FSAMarketsFeedMsg>
</FSAMarketsFeed>

选项 2:在其包含类型上使用 [XmlAnyElement]CoreMarket 进行嵌套序列化。

使用 [XmlAnyElement] 属性,类型可以序列化和反序列化任意子元素。您可以使用此功能对 CoreMarket 进行 嵌套序列化,并包含必要的命名空间声明。

为此,修改 FSAMarketsFeedMsg 如下:

[XmlType(AnonymousType = true)]
public class FSAMarketsFeedMsg
{
    [XmlIgnore]
    public CoreItemsMkt CoreMarket { get; set; }

    [XmlAnyElement(Name = "CoreItemsMkt", Namespace = Namespaces.FSAMarketsFeed)]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public XElement CoreMarketXml
    {
        get
        {
            return (CoreMarket == null ? null : XObjectExtensions.SerializeToXElement(CoreMarket, 
                XmlSerializerFactory.Create(typeof(CoreItemsMkt), "CoreItemsMkt", Namespaces.FSAMarketsFeed), 
                Namespaces.GetFSAMarketsFeedNamespace()));
        }
        set
        {
            CoreMarket = (value == null ? null : XObjectExtensions.Deserialize<CoreItemsMkt>(value, 
                XmlSerializerFactory.Create(typeof(CoreItemsMkt), "CoreItemsMkt", Namespaces.FSAMarketsFeed)));
        }
    }

    // Remainder of properties are left unchanged.
}

除了选项 1 的静态扩展 类 之外,您还需要以下内容以避免大量 memory leak

public static class XmlSerializerFactory
{
    static readonly Dictionary<Tuple<Type, string, string>, XmlSerializer> table;
    static readonly object padlock;

    static XmlSerializerFactory()
    {
        table = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
        padlock = new object();
    }

    public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
    {
        if (serializedType == null)
            throw new ArgumentNullException();
        if (rootName == null && rootNamespace == null)
            return new XmlSerializer(serializedType);
        lock (padlock)
        {
            var key = Tuple.Create(serializedType, rootName, rootNamespace);
            XmlSerializer serializer;
            if (!table.TryGetValue(key, out serializer))
            {
                var attr = (string.IsNullOrEmpty(rootName) ? new XmlRootAttribute() { Namespace = rootNamespace } : new XmlRootAttribute(rootName) { Namespace = rootNamespace });
                serializer = table[key] = new XmlSerializer(serializedType, attr);
            }
            return serializer;
        }
    }
}

请注意,[XmlAnyElement] 属性 将针对所有未知元素调用,因此如果您的 XML 由于某种原因具有意外元素,您可能会从 XObjectExtensions.Deserialize 因为根元素名称错误。如果可能的话,您可能希望捕获并忽略来自此方法的异常。

像您目前所做的那样序列化到磁盘。冗余 xmlns= 属性将与选项 1 中一样出现。

尝试使用自定义 XML 编写器。

public class CustomWriter : XmlTextWriter
{
    public CustomWriter(TextWriter writer) : base(writer) { }
    public CustomWriter(Stream stream, Encoding encoding) : base(stream, encoding) { }
    public CustomWriter(string filename, Encoding encoding) : base(filename, encoding) { }

    public override void WriteStartElement(string prefix, string localName, string ns)
    {
        base.WriteStartElement(prefix, localName, ns);

        if (localName == "CoreItemsMkt")
        {
            base.WriteAttributeString("xmlns",
                "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2");
            //base.WriteAttributeString("xmlns", ns);
        }
    }
}

自定义编写器强制将必需的属性添加到名称为 CoreItemsMkt 的每个元素。

用法

using (var customWriter = new CustomWriter(path, Encoding.UTF8))
{
    customWriter.Formatting = Formatting.Indented;
    customWriter.Indentation = 1;
    customWriter.IndentChar = '\t';

    ser.Serialize(customWriter, GetDataToSerialize(), ns);
}