Post XML to asp.net core 2.0 web api

Viewed 5929

I'm trying to post XML to asp.net core 2:

$.ajax({
    type: "POST",
    url: 'api/Test',
    data: "<test>hello<test>",
    contentType: "application/xml",
    success: function (response) { alert(response); },
});

How should I write the action, so it accepts the xml as parameter?

  • IActionResult Post([FromBody]string xml) -> xml is null
  • IActionResult Post([FromBody]XElement xml) -> xml is null
  • IActionResult Post(XElement xml) -> InvalidOperationException: Could not create an instance of type 'System.Xml.Linq.XElement'. Model bound complex types must not be abstract or value types and must have a parameterless constructor.
  • IActionResult Post(string xml) -> xml is null

in the Startup.ConfigureServices:

services.AddMvc()
    .AddXmlSerializerFormatters();

How to write the controller so that it accepts XML as a parameter? I know I can read it from HttpContext.Request, but I want it to be parameter

3 Answers

I've ended up creating custom InputFormatter, which was pretty straitforward, but if there is better alternative, you are very welcome to write an answer!

public class XDocumentInputFormatter : InputFormatter, IInputFormatter, IApiRequestFormatMetadataProvider
{
    public XDocumentInputFormatter()
    {
        SupportedMediaTypes.Add("application/xml");
    }

    protected override bool CanReadType(Type type)
    {
        if (type.IsAssignableFrom(typeof(XDocument))) return true;
        return base.CanReadType(type);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
    {
        var xmlDoc = await XDocument.LoadAsync(context.HttpContext.Request.Body, LoadOptions.None, CancellationToken.None);
        return InputFormatterResult.Success(xmlDoc);
    }
}

Register the XDocumentInputFormatter in startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options => options.InputFormatters.Insert(0, new XDocumentInputFormatter()));
}

Just a change for the answer given by Liero, you should use a StreamReader, so you can support multiple encodings. Tested my solution with UTF-8, UTF-16 and ASCI declaration header.

Change the Method from XDocumentInputFormatter:

public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
    var xmlDoc = await XDocument.LoadAsync(context.HttpContext.Request.Body, LoadOptions.None, CancellationToken.None);
    return InputFormatterResult.Success(xmlDoc);
}

To below

public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) {
        // Use StreamReader to convert any encoding to UTF-16 (default C# and sql Server).
        using (var streamReader = new StreamReader(context.HttpContext.Request.Body)) {
            var xmlDoc = await XDocument.LoadAsync(streamReader, LoadOptions.None, CancellationToken.None);
            return InputFormatterResult.Success(xmlDoc);
        }
    }

These solutions works, but have one disadvantage in .NET Core 3 - they causes exception (inside calling XDocument.LoadAsync):

System.InvalidOperationException: Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.

Here is my modified solution with FileBufferingReadStream (inspired by code from Microsoft.AspNetCore.Mvc.Formatters.XmlSerializerInputFormatter)

public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
    Check.NotNull(context, nameof(context));

    var xmlDoc = await LoadXmlFromRequestAsync(context.HttpContext);
    return InputFormatterResult.Success(xmlDoc);
}

private static async Task<XDocument> LoadXmlFromRequestAsync(HttpContext httpContext)
{
    Check.NotNull(httpContext, nameof(httpContext));

    //Code from Microsoft.AspNetCore.Mvc.Formatters.XmlSerializerInputFormatter to use FileBufferingReadStream to avoid synchronous read issue:
    //https://github.com/dotnet/aspnetcore/issues/18723 - Synchronous call inside XDocument.LoadAsync causes --> System.InvalidOperationException: Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.
    int memoryThreshold = 30720;
    long contentLength = httpContext.Request.ContentLength.GetValueOrDefault();
    if (contentLength > 0 && contentLength < memoryThreshold)
    {
        memoryThreshold = (int)contentLength;
    }

    var readStream = new FileBufferingReadStream(httpContext.Request.Body, memoryThreshold);
    httpContext.Response.RegisterForDispose(readStream);
    await readStream.DrainAsync(CancellationToken.None);
    readStream.Seek(0, SeekOrigin.Begin);

    try
    {
        using (var streamReader = new StreamReader(readStream))
        {
            var xmlDoc = await XDocument.LoadAsync(streamReader, LoadOptions.None, CancellationToken.None);
            return xmlDoc;
        }
    }
    finally
    {
        await readStream.DisposeAsync();
    }
}
Related