Reputation: 41
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).
Upvotes: 2
Views: 1555
Reputation: 41
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
Upvotes: 2
Reputation: 18159
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();
Upvotes: 2