1

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();
        }
0

3 Answers 3

3

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.

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

7 Comments

Darrel -- You are right that Accept should be used. Using Accept: application/xml, unfortunately, produces the same results. I've modified the question to reflect your correction, though I suppose the question still stands.
@Don01001100 You most likely have some issue in your domain object that prevents the XML serializer from being able to render it. Install/enable the WebAPI tracing, that should tell you why it is not working.
I didn't see anything that was an obvious problem to me. Can you take a look? I added the output to my question.
@Don01001100 Do you have cycles in your MyDomainObject? I.e. It points to an object that points back to it? the XmlSerializer will choke on that I believe.
@Don01001100 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.
|
3

@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.

Comments

1

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");
    }
}

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.