3

I know this has been asked here and at various other places but I have not seen a simple answer. Or at least, I have not been able to find any.

In short, I have an .Net Core Web Api endpoint that accepts XML. Using (in Startup):

services.AddControllers().AddXmlSerializerFormatters();

I want to modelbind it to a class. Example:

[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
    [HttpPost]
    [Consumes("application/xml")]
    [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))]
    public async Task<ActionResult> PostPerson([FromBody] Person person)
    {
        return Ok();
    }
}

// Class/Model
[XmlRoot(ElementName = "Person")]
public class Person
{
    [XmlElement(ElementName = "Name")]
    public string Name { get; set; }

    [XmlElement(ElementName = "Id")]
    public int Id { get; set; }
}

Passing in:

<Person><Name>John</Name><Id>123</Id></Person>

works fine. However, as soon as namespaces comes into play it either fails to bind the model:

<Person xmlns="http://example.org"><Name>John</Name><Id>123</Id></Person>
<Person xmlns="http://example.org"><Name>John</Name><Id xmlns="http://example.org">123</Id></Person>

Or the model can be bound but the properties are not:

<Person><Name xmlns="http://example.org">John</Name><Id>123</Id></Person>
<Person><Name xmlns="http://example.org">John</Name><Id xmlns="http://example.org">123</Id></Person>

etc.

I understand namespaces. I do realize that I can set the namespaces in the XML attribute for the root and elements. However, I (we) have a dozens of callers and they all set their namespaces how they want. And I want to avoid to have dozens of different versions of the (in the example) Person classes (one for each caller). I would also mean that if a caller changes their namespace(s) I would have to update that callers particular version and redeploy the code.

So, how can I modelbind incoming XML to an instance of Person without taking the namespaces into account?

I've done some tests overriding/creating an input formatter use XmlTextReader and set namespaces=false:

        XmlTextReader rdr = new XmlTextReader(s);
        rdr.Namespaces = false;
        

But Microsoft recommdes to not use XmlTextReader since .Net framework 2.0 so would rather stick to .Net Core (5 in this case).

3 Answers 3

2

You can use custom InputFormatter,here is a demo:

XmlSerializerInputFormatterNamespace:

public class XmlSerializerInputFormatterNamespace : InputFormatter, IInputFormatter, IApiRequestFormatMetadataProvider

    {
        public XmlSerializerInputFormatterNamespace()
        {
            SupportedMediaTypes.Add("application/xml");
        }
        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            var xmlDoc = await XDocument.LoadAsync(context.HttpContext.Request.Body, LoadOptions.None, CancellationToken.None);
            Dictionary<string, string> d = new Dictionary<string, string>();
            foreach (var elem in xmlDoc.Descendants())
            {
                d[elem.Name.LocalName] = elem.Value;
            }
            return InputFormatterResult.Success(new Person { Id = Int32.Parse(d["Id"]), Name = d["Name"] }); 
        }
       

    }

Person:

public class Person
{
    public string Name { get; set; }

    public int Id { get; set; }
}

startup:

services.AddMvc(options =>
            {
                options.RespectBrowserAcceptHeader = true; // false by default
                options.InputFormatters.Insert(0, new XmlSerializerInputFormatterNamespace());
            }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
           .AddXmlSerializerFormatters()
          .AddXmlDataContractSerializerFormatters();

result: enter image description here

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the feedback. I didn't mention that not only do we have 10s of different 'Persons'. There is also many other xml files that comes with different namespaces (same content but the namespaces differ). So needed a more generic solution than just creating an instance of the Person in the Formatter. Took inspiration from your answer and decided to go with the XmlTextReader anyway. See my own answer below.
2

So, in order to be able to modelbind XML to a class without taking namespaces into consideration I created new InputFormatter. And I use XmlTextReader in order to ignore namespaces. Microsoft recommends to use XmlReader rather than XmlTextReader. But since XmlTextReader is there still (in .Net 6.0 Preview 3) I'll use it for now.

Simply create an inputformatter that inherits from XmlSerializerInputFormatter like so:

public class XmlNoNameSpaceInputFormatter : XmlSerializerInputFormatter
{
    private const string ContentType = "application/xml";
    public XmlNoNameSpaceInputFormatter(MvcOptions options) : base(options)
    {
        SupportedMediaTypes.Add(ContentType);
    }

    public override bool CanRead(InputFormatterContext context)
    {
        var contentType = context.HttpContext.Request.ContentType;
        return contentType.StartsWith(ContentType);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
    {
        var type = GetSerializableType(context.ModelType);
        var request = context.HttpContext.Request;

        using (var reader = new StreamReader(request.Body))
        {
            var content = await reader.ReadToEndAsync();
            Stream s = new MemoryStream(Encoding.UTF8.GetBytes(content));

            XmlTextReader rdr = new XmlTextReader(s);
            rdr.Namespaces = false;
            var serializer = new XmlSerializer(type);
            var result = serializer.Deserialize(rdr);
            return await InputFormatterResult.SuccessAsync(result);
        }
    }
}

Then add it to the inputformatters like so:

        services.AddControllers(o => 
        {
            o.InputFormatters.Add(new XmlNoNameSpaceInputFormatter(o));
        })
        .AddXmlSerializerFormatters();

Now we can modelbind Person or any other class no matter if there is namespaces or not in the incoming XML. Thanks to @yiyi-you

1 Comment

Your solution is perfect for the case that it's not able to be solved from the request message builder. In my case, another device is always putting xml message with wrong encoding= "utf-16", which causing 415 response from web api.
0

Here's an implementation that uses the built-in types as much as possible, because they have a ton of optimizations:

// inherits from the built-in implementation
internal sealed class NamespaceAgnosticXmlInputFormatter(MvcOptions options) : XmlSerializerInputFormatter(options)
{
    protected override XmlReader CreateXmlReader(Stream readStream, Encoding encoding)
        => new NamespaceAgnosticXmlReader(base.CreateXmlReader(readStream, encoding));

    // a wrapper for the built-in implementation
    private sealed class NamespaceAgnosticXmlReader(XmlReader reader) : XmlReader
    {
        private readonly XmlReader _reader = reader;

        // ignore namespace infos
        public override string NamespaceURI => string.Empty;
        public override string Prefix => string.Empty;
        public override string Name => this.LocalName;

        public override int AttributeCount => _reader.AttributeCount;
        public override string BaseURI => _reader.BaseURI;
        public override int Depth => _reader.Depth;
        public override bool EOF => _reader.EOF;
        public override bool IsEmptyElement => _reader.IsEmptyElement;
        public override string LocalName => _reader.LocalName;
        public override XmlNameTable NameTable => _reader.NameTable;
        public override XmlNodeType NodeType => _reader.NodeType;
        public override ReadState ReadState => _reader.ReadState;
        public override string Value => _reader.Value;

        public override string GetAttribute(int i) => _reader.GetAttribute(i);
        public override string? GetAttribute(string name) => _reader.GetAttribute(name);
        public override string? GetAttribute(string name, string? namespaceURI) => _reader.GetAttribute(name, namespaceURI);
        public override string? LookupNamespace(string prefix) => _reader.LookupNamespace(prefix);
        public override bool MoveToAttribute(string name) => _reader.MoveToAttribute(name);
        public override bool MoveToAttribute(string name, string? ns) => _reader.MoveToAttribute(name, ns);
        public override bool MoveToElement() => _reader.MoveToElement();
        public override bool MoveToFirstAttribute() => _reader.MoveToFirstAttribute();
        public override bool MoveToNextAttribute() => _reader.MoveToNextAttribute();
        public override bool Read() => _reader.Read();
        public override bool ReadAttributeValue() => _reader.ReadAttributeValue();
        public override void ResolveEntity() => _reader.ResolveEntity();
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.