ASP.NET Web API cannot return XML if the returned

2019-08-15 06:23发布

I'm using ASP.NET Web API's ApiController to expose business logic as a Web service. I'm testing both XML and JSON, since we have demand for both, and I've been using Fiddler to test. I've narrowed it down to this: having an IList<T> property forces JSON for some reason, but changing the property to List<T> allows either JSON or XML. Unfortunately, I need these to use IList<T>, so how can I make XML out of objects with IList<T> properties?

If I use the following HTML headers to GET http://localhost:4946/Api/MyBizLog/GetDomainObject?id=foo:

Authorization: basic ***************
Accept: application/xml
Host: localhost:4946

I get back JSON if an exception is thrown. If I change Content-Type to Content-Type: application/xml, I get XML if an exception is thrown. If an exception is not thrown, however, I always get JSON.

The method I'm calling has a signature like public virtual MyDomainObject GetDomainObject(String id).

How do I get it to return the content type I ask for on success as well as failure?

I have the following WebApiConfig:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "AlternativeApi",
            routeTemplate: "api/{controller}/{action}",
            defaults: new { }
        );

        config.Formatters.XmlFormatter.UseXmlSerializer = true;
    }
}

More Information

I installed WebAPI tracing per @Darren Miller's suggestion, and I get the following:

I put a breakpoint on the first line of the action. I then sent the GET from Fiddler. The output showed the following when execution stopped at the breakpoint:

iisexpress.exe Information: 0 : Request, Method=GET, Url=http://localhost:4946/Api/MyBizLog/GetDomainObject?id=foo, Message='http://localhost:4946/Api/MyBizLog/GetDomainObject?id=foo'
iisexpress.exe Information: 0 : Message='MyBizLog', Operation=DefaultHttpControllerSelector.SelectController
iisexpress.exe Information: 0 : Message='WebApp.Api.MyBizLogController', Operation=DefaultHttpControllerActivator.Create
iisexpress.exe Information: 0 : Message='WebApp.Api.MyBizLogController', Operation=HttpControllerDescriptor.CreateController
iisexpress.exe Information: 0 : Message='Selected action 'GetDomainObject(String id)'', Operation=ApiControllerActionSelector.SelectAction
iisexpress.exe Information: 0 : Message='Parameter 'id' bound to the value 'foo'', Operation=ModelBinderParameterBinding.ExecuteBindingAsync
iisexpress.exe Information: 0 : Message='Model state is valid. Values: id=foo', Operation=HttpActionBinding.ExecuteBindingAsync
iisexpress.exe Information: 0 : Operation=TransactionalApiFilterAttribute.ActionExecuting

I then let execution continue, and I got the following:

iisexpress.exe Information: 0 : Message='Action returned 'DomainObjects.MyDomainObject'', Operation=ReflectedHttpActionDescriptor.ExecuteAsync
iisexpress.exe Information: 0 : Message='Will use same 'JsonMediaTypeFormatter' formatter', Operation=JsonMediaTypeFormatter.GetPerRequestFormatterInstance
iisexpress.exe Information: 0 : Message='Selected formatter='JsonMediaTypeFormatter', content-type='application/json; charset=utf-8'', Operation=DefaultContentNegotiator.Negotiate
iisexpress.exe Information: 0 : Operation=ApiControllerActionInvoker.InvokeActionAsync, Status=200 (OK)
iisexpress.exe Information: 0 : Operation=TransactionalApiFilterAttribute.ActionExecuted, Status=200 (OK)
iisexpress.exe Information: 0 : Operation=MyBizLogController.ExecuteAsync, Status=200 (OK)
iisexpress.exe Information: 0 : Response, Status=200 (OK), Method=GET, Url=http://localhost:4946/Api/MyBizLog/GetDomainObject?id=foo, Message='Content-type='application/json; charset=utf-8', content-length=unknown'
iisexpress.exe Information: 0 : Operation=JsonMediaTypeFormatter.WriteToStreamAsync
iisexpress.exe Information: 0 : Operation=MyBizLogController.Dispose

I do have an ActionFilterAttribute that reads the basic authentication and tells the business logic layer who the current user is, but skipping that doesn't change the results.

Yet More Information

So I've narrowed it down to IList and List. If I #define WORKS, I get XML. If I #define DOESNT_WORK, I get JSON. This is actually code that actually runs.

        public class Bar
        {
        }

        public class Foo
        {
#if WORKS
            public virtual List<Bar> Bars { get; set; }
#elif DOESNT_WORK
            public virtual IList<Bar> Bars { get; set; }
#endif
        }

        [HttpPost]
        [HttpGet]
        public Foo Test()
        {
            return new Foo();
        }

3条回答
来,给爷笑一个
2楼-- · 2019-08-15 06:44

Just like @Patrick mentioned, @Darrel's answer is correct. I am not suggesting a different answer here, this is only a solution as whole, just in case anyone else stumbles here:

Controller:

[HttpPost]
[Route("myRoute")]
[ResponseType(typeof(MyCustomModel))]
/* Note: If your response type is of type IEnumerable, i.e. IEnumerable<MyCustomModel>, then Example in Swagger will look like this: 

<?xml version="1.0"?>
<Inline Model>
    <AttributeIdProperty>string</AttributeIdProperty>
    <PropertyForElement>string</PropertyForElement>
</Inline Model>

The real output will be correct representation, however:

<ArrayOfMyCustomModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<MyCustomModel AttributeIdProperty="Value for this attribute property">
    <PropertyForElement>Value for this element property</PropertyForElement>
</MyCustomModel>
</ArrayOfContentMetadata>
*/
public virtual IHttpActionResult MyMethod([FromBody]MyCustomModel myCustomModel)
{
    if (myCustomModel== null) throw new Exception("Invalid input", HttpStatusCode.BadRequest);
    return Ok(_myBusiness.MyMethod(myCustomModel);
}

Model:

public class MyCustomModel : IXmlSerializable
{
    [JsonProperty("attributeIdProperty ")]
    [XmlAttribute("AttributeIdProperty")]
    public string AttributeIdProperty { get; set; }

    [JsonProperty("propertyForElement ")]
    [XmlElement("PropertyForElement ")]
    public string PropertyForElement { get; set; }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "MyCustomModel")
        {
            AttributeIdProperty = reader["AttributeIdProperty"];
            PropertyForElement = reader["PropertyForElement"];
            reader.Read();
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("AttributeIdProperty", AttributeIdProperty);
        writer.WriteElementString("PropertyForElement", PropertyForElement);
    }
}

Web API Config:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
        config.Filters.Add(new GlobalExceptionFilter());
        //below line is what's most important for this xml serialization
        config.Formatters.XmlFormatter.UseXmlSerializer = true;
    }
}

Swagger Config:

public class SwaggerConfig
{
    public static void Register()
    {

        var swaggerHeader = new SwaggerHeader();

        ///...

        GlobalConfiguration.Configuration
            .EnableSwagger(c =>
            {
                swaggerHeader.Apply(c);
            });

Swagger Header:

public class SwaggerHeader : IOperationFilter
{
    public string Description { get; set; }
    public string Key { get; set; }
    public string Name { get; set; }

    public void Apply(SwaggerDocsConfig c)
    {
        c.ApiKey(Key).Name(Name).Description(Description).In("header");
        c.OperationFilter(() => this);
    }

    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
        if (!operation.produces.Contains("application/xml")) operation.produces.Add("application/xml");
        if (!operation.produces.Contains("text/xml")) operation.produces.Add("text/xml");
    }
}
查看更多
乱世女痞
3楼-- · 2019-08-15 07:03

@Darrel Miller has the answer:

From what I understand, XmlSerializer can't handle interfaces. Either change your property to List<>, implement IXmlSerializable on your class, or use DataContractSerializer. Or even better, don't try and return domain objects across the wire.

查看更多
Ridiculous、
4楼-- · 2019-08-15 07:04

That's because you are using the wrong header. Content-Type is used to describe the payload you are transferring. In the case of a GET there is no payload, therefore there is no need for Content-Type or Content-Length. You should be setting the Accept header to indicate your preference of the media type that will be returned.

查看更多
登录 后发表回答