Mvc: AddXmlSerializerFormatters with no namespace

Created on 24 Apr 2017  路  11Comments  路  Source: aspnet/Mvc

We're using AddXmlSerializerFormatters (not out of choice) and have a requirement to not have namespaces included in the serialized content. I've scoured every possible option and can't find a way of making this happen other then rewriting the XmlSerializerOutputFormatter and doing the following:

```c#
var ns = new XmlSerializerNamespaces();
ns.Add("", "");
xmlSerializer.Serialize(xmlWriter, value, ns);

instead of:

```c#
xmlSerializer.Serialize(xmlWriter, value);

Can you please advise on
a) if it's actually possible
b) how can we get some more flexibility on the xml side of things

3 - Done enhancement

Most helpful comment

We should look at adding a delegate property in the MVC options for a callback to customize the XML configuration.

All 11 comments

@herecydev I'm afraid I can't find any other way to do this with the XmlSerializer. Ultimately the MVC code just uses what's built in to the underlying .NET type.

You could probably fairly easily clone https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs and make the changes you need, though I understand that isn't a great option.

@herecydev Your approach is the best way I've found to remove the "ambient" namespaces that the serializer automatically injects. I have used a similar technique but added the root object's namespace as the default namespace to clean up the XML further:
```c#
// Try to find the root attribute so we can set the default namespace to use
var defaultNamespace = string.Empty;
var xmlRootAttributes = value.GetType().GetCustomAttributes(typeof(XmlRootAttribute), true) as XmlRootAttribute[];
if (xmlRootAttributes != null && xmlRootAttributes.Length > 0)
{
defaultNamespace = xmlRootAttributes[0].Namespace;
}

// Use this object to prevent the serilaizer from adding extra "Ambient" namespaces
var xmlSerializerNamespaces = new XmlSerializerNamespaces();
xmlSerializerNamespaces.Add(String.Empty, defaultNamespace);

xmlSerializer.Serialize(xmlWriter, value, xmlSerializerNamespaces);
```
@Eilon it would be nice if the XmlSerializerOutputFormatter by default used an empty XmlSerializerNamespaces collection so as to clean up the ambient namespaces (i.e. removing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" and xmlns:xsd="http://www.w3.org/2001/XMLSchema"). If the XmlSerializerNamespaces was read from an virtual method that took in the object being serialised, then someone wanting full control of the serialisation would have much less to override... currently I have to override the entire WriteResponseBodyAsync method. You already allow the XmlWriterSettings to be injected, which is good.

Reactivating this for consideration

We should look at adding a delegate property in the MVC options for a callback to customize the XML configuration.

@Eilon That would be a great addition...

I presume such a delegate would be able to make decisions for each object being serialised? For instance, in my code example above, the default namespace is set up based on the object being serialised's XmlRootAttribute, if it has one.

Ideally the delegate would have some way of inspecting the current context. For example, our current API defaults the XmlWriterSettings.Indent to false but allows a querystring parameter to override this on a request by request basis. I'm not sure how a delegate would be able to access such information (say from the Items collection)... though this might just be my lack of knowing how to inject anything into a delegate.

Ideally the delegate would have some way of inspecting the current context. For example, our current API defaults the XmlWriterSettings.Indent to false but allows a querystring parameter to override this on a request by request basis. I'm not sure how a delegate would be able to access such information (say from the Items collection)... though this might just be my lack of knowing how to inject anything into a delegate.

This sounds like you should just be subclassing the default formatter and overriding some things.

Regardles of how we enable this, we should take a look at these two scenarios and make sure it's reasonable to accomplish.

This actually looks like these two things are already possible.

  1. To customize the serializer for each Type - override CreateSerializer

This will be called once per type and the base class will do the caching for you.

  1. To have the Indent behavior configured by a query string parameter, you can do this by creating two instances of the formatter. The Indent formatter should come first in the collection, and should override CanWriteResult to check for your query string parameter. You can pass in a different settings object to the constructor that has Indent = true.

This will way the 'indent' formatter will have a chance to run if its criteria are satisfied, and if not it will fall back to the normal one.

@rynowak thanks for the ideas.

Firstly, I can achieve what I want by deriving from XmlSerializerOutputFormatter and overriding the WriteResponseBodyAsync method. Within there I can inspect the querystring to be able to set or clear the indent flag. I can also inspect the Type being serialized and use the overload of xmlSerializer.Serialize that allows you to supply an instance of XmlSerializerNamespaces to get rid of the "ambient" namespaces (i.e. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema") and set the default namespace. The downside of this approach is that by overriding WriteResponseBodyAsync I'm taking on responsibility for quite a bit of other code... so it could be I'll miss out on optimisations and improvements if I don't keep checking back with the current implementation of XmlSerializerOutputFormatter.

The problems I see with your approach are:

  1. As you mentioned, I'll need to create and register two instances of the formatter
  2. Overriding the CreateSerializer would give me some control over how the serializer works as I can move away from the simple constructor that only takes a Type. However, we have always been warned against this as a potential performance issue / memory leak (see XmlSerializer Documentation). Whilst moving to the constructor that takes a Type and the default namespace should be safe, advocating this approach risks somebody naively using one of the other constructors that could cause problems. I see that the current implementation of XmlSerializerOutputFormatter will cache the serializer, which should mitigate the problem, however that means my overridden class is relying on the internal workings of the base class to do that mitigation... again making it a bit brittle.
  3. Using the XmlSerializer(Type,鈥係tring) constructor in an overridden CreateSerializer will set the correct namespace as the default namespace, but it doesn't get rid of the ambient namespaces that XmlSerializer adds by default. The only way to prevent them turning up in the ouput is to directly pass an instance of XmlSerializerNamespaces into the Serialize method called within WriteResponseBodyAsync. Unfortunately, passing in an instance of XmlSerializerNamespaces clears the default namespace if it was added in the constructor of XmlSerializer... which results in the root object (if it has an XmlRootAttribute) having a namespace alias and then all elements being prepended with that alias. So an XmlSerializerNamespaces containing the correct default namespace must be passed into Serialize every time.

For me, having a simple way to provide / modify XmlWriterSettings on a request by request basis and to provide, at least once per Type being serialized, an instance of XmlSerializerNamespaces (though per request would be OK) would save me from quite a heavy weight and brittle override.

I see that the current implementation of XmlSerializerOutputFormatter will cache the serializer, which should mitigate the problem, however that means my overridden class is relying on the internal workings of the base class to do that mitigation... again making it a bit brittle.

We think of semantics caching as part of the contract. We wouldn't arbitrarily change this behavior.

The only way to prevent them turning up in the ouput is to directly pass an instance of XmlSerializerNamespaces into the Serialize method called within WriteResponseBodyAsyn

Ah OK I missed that.

It sounds like we will have to do some work to make this possible.

@kichalla can you investigate a solution to this?

@Eilon @rynowak I created a design PR now. Let me know what you think of

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dougbu picture dougbu  路  125Comments

johnnyoshika picture johnnyoshika  路  57Comments

angelsix picture angelsix  路  61Comments

NTaylorMullen picture NTaylorMullen  路  66Comments

sk29110 picture sk29110  路  33Comments