1

TLDR version

I am serializing objects into XML to match a schema provided by a third party. Their validator requires one of the child objects to have a namespace explicitly declared which matches it's ancestor's namespace . The data is complex enough that I don't want to roll my own serializer for this purpose. How can I force the XMLSerializer class to explicitly render a namespace even though it is technically redundant?

Full version

I am running into an issue where the CoreItemsMkt namespace is not rendered by the XMLSerializer. I believe that this is because both the attribute and the namespaces exactly match the ancestor's namespace that it is inheriting from, therefore the serializer omits it - however, the site validator that this file gets submitted to requires it.

For example:

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

I'm serializing with a method like this:

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

My root class is defined as:

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

The working feed header 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; }
}

The parent Feed Message 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; }
}

Finally, the CoreItemsMkt class which is failing to render its namespace:

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

Tried so far:

  • Using XMmlType(AnonymousType = true) to try to break the inheritance chain
  • Explicitly setting xmlns as an XmlAttributeAttribute w/ a hard coded value.
  • Setting and removing XmlType(Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2") on CoreItemsMkt
  • Adding and removing XmlElement(Namespace = "the value") on the FSAMArketsFeedMsg's property.
  • Implementing ISerializable on CoreItmsMkt (Couldn't quite figure out how to get that to work though.)
  • Stack overflow searches - I've found 1 similar question that was answered with "This is unsupported, change your output namespace." Unfortunately, that answer doesn't work for me.

So, without hand rendering this, is there any way to force the XmlSerializer class to render those namespace attributes on CoreItmsMkt?

J. Patton
  • 27
  • 10
  • Just to confirm, by *a schema explicitly declared which matches it's ancestor's schema* you mean *a **namespace** explicitly declared which matches it's ancestor's **namespace***? – dbc Jul 15 '16 at 02:59
  • @dbc... that is correct. Sorry, was going back and forth between this and a SQL project. Will edit to correct in a moment. – J. Patton Jul 15 '16 at 13:39

2 Answers2

1

Try to use custom XML writer.

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

The custom writer forcibly adds the required attribute to every element with the CoreItemsMkt name.

Usage

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

    ser.Serialize(customWriter, GetDataToSerialize(), ns);
}
Alexander Petrov
  • 13,457
  • 2
  • 20
  • 49
  • While I can see this getting awkward when more customisability is needed, this answer solved my issue neatly. Thank you. – J. Patton Jul 15 '16 at 13:35
0

You would like to be able to force XmlSerializer to emit redundant xmlns= attributes when serializing specified nested elements. Unfortunately, I don't know of any API to make this happen automatically. You also wrote The data is complex enough that I don't want to roll my own serializer for this purpose so you don't want to have to implement IXmlSerializable on FSAMarketsFeedMsg. (ISerializable is not used by XmlSerializer so implementing it will not help.) Thus you're going to want to do something "semi-manual". There are at least a couple of options for this.

Option 1: Serialize to a temporary XDocument then fix the attributes.

With this solution, you serialize to a temporary XDocument in memory, then add an XAttribute for each desired redundant xmlns=, as follows:

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

Using the static extension classes:

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

Which produces the 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>

Option 2: Do a nested serialization of CoreMarket using [XmlAnyElement] on its containing type.

Using an [XmlAnyElement] property, a type can serialize and deserialize any arbitrary child element. You can use this functionality to do a nested serialization of CoreMarket with the necessary namespace declarations included.

To do this, modify FSAMarketsFeedMsg as follows:

[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.
}

In addition to the static extension classes from Option 1, you will need the following to avoid a substantial 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;
        }
    }
}

Note that the [XmlAnyElement] property will be called for all unknown elements, so if your XML for some reason has unexpected elements, you may get an exception thrown from XObjectExtensions.Deserialize because the root element name is wrong. You may want to catch and ignore exceptions from this method if that is a possibility.

Serialize to disk as you are currently doing. The redundant xmlns= attributes will be present as in Option 1.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • @db, that is a quality answer, and I think that the majority of people who run into the same issue should use one of these two options. Unfortunately for my case, Option 1 started to hit OutOfMemory exceptions (I'm handling gigs of data) and option 2 broke a separate piece of code that was out of scope for the question (I also have an excel file renderer running off of the same classes.). Still, I learned form this post, and suspect that I will be using it at a later date. Thank you. – J. Patton Jul 15 '16 at 13:38
  • @J.Patton - I'd be interested to know how the excel file renderer broke. Did some other unrelated serializer try to serialize the `CoreMarketXml` property? – dbc Jul 15 '16 at 22:38
  • it resulted in an excel file that was the right file size, but unopenable (With the "This file may be corrupted or incomplete" error message) when opening in excel. It's something I rolled myself years ago, so it's not a terribly smart excel file renderer. In my case, it was simpler to just use the other solution than to go back and fix it. – J. Patton Jul 18 '16 at 13:44