Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning XDocument from Controller (dotnet coreclr)

I have a XDocument inside a controller that I want to server as xml and json (depending on the Accept header of the request).

I am using dotnet core:

In my startup.cs/ConfigureServices I have this:

services.AddMvc().AddXmlDataContractSerializerFormatters();

My controller essentially is this:

public async Task<IActionResult> getData(int id)
{
    XDocument xmlDoc = db.getData(id);
    return Ok(xmlDoc);
}

When making a request with Accept: application/json, I get my data properly formatted as JSON. When making a request with Accept: application/xml, I still get a JSON response (same as with application/json).

I have also tried with:

services.AddMvc().AddXmlSerializerFormatters();

but that was even worse as even normal objects were served as JSON (XmlDataContractSerializer could handle normal objects, but not XDocument).

When I add [Produces("application/xml")]to the controller (using AddXmlSerializerFormatters), I get a Http 406 error when serving XDocument, but I do get a XML output when returning normal objects.

Do I have to convert the XDocument to objects to output XML from a controller? Is there an easy way to convert XDocuments to objects?

like image 953
galmok Avatar asked Jan 06 '23 17:01

galmok


2 Answers

I was able to reproduce the described problem and after reading some source code in the ASP.NET Core GitHub repository (https://github.com/aspnet/Mvc/tree/dev/src/Microsoft.AspNetCore.Mvc.Formatters.Xml) there is a missing feature in the Xml formatters project. While the JSON formatter handles XDocument values amazingly well, the xml formatter tries to serialize the XDocument instance although not all objects are serializable. Enabling the XmlSerializerOutputFormatter to pass through the XmlData (simply by writing the string representation on the stream) would solve the root cause.

Therefore a quick and rather simple/naive workaround is to return a plain ContentResult (if content negotiation is not a strict requirement), like

return new ContentResult
            {
                Content = xmlDoc.ToString(),
                ContentType = "text/xml",
                StatusCode = 200
            };

instead of

  return Ok(xmlDoc);

In order to solve the root cause I suggest a feature request in the https://github.com/aspnet/Mvc Repo.

like image 70
Ralf Bönning Avatar answered Jan 15 '23 03:01

Ralf Bönning


I solved the issue using the source code for the XmlDataContractSerializerOutputFormatter and substituted WriteResponseBodyAsync with this (5 lines including comment added):

    public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (selectedEncoding == null)
        {
            throw new ArgumentNullException(nameof(selectedEncoding));
        }

        var writerSettings = WriterSettings.Clone();
        writerSettings.Encoding = selectedEncoding;

        // Wrap the object only if there is a wrapping type.
        var value = context.Object;
        var wrappingType = GetSerializableType(context.ObjectType);
        if (wrappingType != null && wrappingType != context.ObjectType)
        {
            var wrapperProvider = WrapperProviderFactories.GetWrapperProvider(new WrapperProviderContext(
                declaredType: context.ObjectType,
                isSerialization: true));

            value = wrapperProvider.Wrap(value);
        }

        var dataContractSerializer = GetCachedSerializer(wrappingType);

        using (var textWriter = context.WriterFactory(context.HttpContext.Response.Body, writerSettings.Encoding))
        {
            using (var xmlWriter = CreateXmlWriter(textWriter, writerSettings))
            {
                // If XDocument, use its own serializer as DataContractSerializer cannot handle XDocuments.
                if (value is XDocument)
                {
                    ((XDocument)value).WriteTo(xmlWriter);
                }
                else
                    dataContractSerializer.WriteObject(xmlWriter, value);
            }

            // Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
            // buffers. This is better than just letting dispose handle it (which would result in a synchronous 
            // write).
            await textWriter.FlushAsync();
        }
    }

I am not completely satisfied with this solution, but it does allow the honoring of the Accept header and produces either JSON or XML when given an XDocument. If the XDocument is inside on object, it will not be caught. That would mean rewriting DataContractSerializer, something I'd rather not do.

Strange thing is that on Microsofts own documentation DataContractSerializer should be able to handle XDocument:

https://msdn.microsoft.com/en-us/library/ms731923(v=vs.110).aspx

like image 31
galmok Avatar answered Jan 15 '23 05:01

galmok