4

I'm getting an error on my REST API when I serialize my return objects that use generic types into XML. This error does not reproduce when using JSON.

Here's a simplified example that demonstrates my problem:

using System;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using System.Web.Http;

namespace Areas.API
{
    public abstract class AnimalController<TProperties> : ApiController
        where TProperties : new()
    {
        protected abstract TProperties makeNew( Int32 Id );

        [Route( "{id}" )]
        public AnimalReturn<TProperties> Get( Int32 id )
        {
            TProperties p = makeNew( id );
            return new AnimalReturn<TProperties>()
            {
                Properties = p,
                Request = Request,
                StatusCode = HttpStatusCode.OK
            };
        }
    }


    public class AnimalReturn<TProperties> : AnimalResult
    {
        public TProperties Properties;
    }

    public class AnimalResult : IHttpActionResult
    {
        [IgnoreDataMember]
        public HttpStatusCode StatusCode = HttpStatusCode.OK;

        [IgnoreDataMember]
        public HttpRequestMessage Request = null;

        public Task<HttpResponseMessage> ExecuteAsync( System.Threading.CancellationToken cancellationToken )
        {
            HttpResponseMessage response = null;
            if( null != Request )
            {
                response = Request.CreateResponse( StatusCode, this );
            }
            return Task.FromResult( response );
        } // ExecuteAsync()
    }

    [RoutePrefix( "api/v3/Fish" )]
    public class FishController : AnimalController<FishController.FishProperties>
    {
        public class FishProperties
        {
            public bool IsShark;
        }

        protected override FishProperties makeNew( Int32 Id )
        {
            return new FishProperties()
            {
                IsShark = true
            };
        }
    }

    [RoutePrefix( "api/v3/Dog" )]
    public class DogController : AnimalController<DogController.DogProperties>
    {
        public class DogProperties
        {
            public string Breed;
            public Int32 TagNo;
        }

        protected override DogProperties makeNew( Int32 Id )
        {
            return new DogProperties()
            {
                Breed = "Labrador",
                TagNo = 12345
            };
        }

    }
}

When I run this API call:

localhost/api/v3/Dog/1

with the header

Accept: text/xml

I get this error:

<Error>
    <Message>An error has occurred.</Message>
    <ExceptionMessage>The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'.</ExceptionMessage>
    <ExceptionType>System.InvalidOperationException</ExceptionType>
    <StackTrace />
    <InnerException>
        <Message>An error has occurred.</Message>
        <ExceptionMessage>Type 'Areas.API.AnimalReturn`1[[Areas.API.DogController+DogProperties, NbtWebApp, Version=2012.2.4.1, Culture=neutral, PublicKeyToken=null]]' with data contract name 'AnimalReturnOfDogController.DogPropertiesS2PP9ThI:http://schemas.datacontract.org/2004/07/Areas.API' is not expected. Consider using a DataContractResolver or add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.</ExceptionMessage>
        <ExceptionType>System.Runtime.Serialization.SerializationException</ExceptionType>
        <StackTrace>   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeAndVerifyType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, Boolean verifyKnownType, RuntimeTypeHandle declaredTypeHandle, Type declaredType)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithXsiTypeAtTopLevel(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle originalDeclaredTypeHandle, Type graphType)
   at System.Runtime.Serialization.DataContractSerializer.InternalWriteObjectContent(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
   at System.Runtime.Serialization.DataContractSerializer.InternalWriteObject(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
   at System.Runtime.Serialization.XmlObjectSerializer.WriteObjectHandleExceptions(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
   at System.Runtime.Serialization.DataContractSerializer.WriteObject(XmlWriter writer, Object graph)
   at System.Net.Http.Formatting.XmlMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content)
   at System.Net.Http.Formatting.XmlMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.WebHost.HttpControllerHandler.&lt;WriteBufferedResponseContentAsync&gt;d__1b.MoveNext()</StackTrace>
    </InnerException>
</Error>

However, when I remove the inheritance (note AnimalReturn2), the problem goes away:

using System;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using System.Web.Http;

namespace Areas.API
{
    public abstract class AnimalController<TProperties> : ApiController
        where TProperties : new()
    {
        protected abstract TProperties makeNew( Int32 Id );

        [Route( "{id}" )]
        public AnimalReturn2<TProperties> Get( Int32 id )
        {
            TProperties p = makeNew( id );
            return new AnimalReturn2<TProperties>()
            {
                Properties = p,
                Request = Request,
                StatusCode = HttpStatusCode.OK
            };
        }
    }

    public class AnimalReturn2<TProperties> : IHttpActionResult
    {
        public TProperties Properties;

        [IgnoreDataMember]
        public HttpStatusCode StatusCode = HttpStatusCode.OK;

        [IgnoreDataMember]
        public HttpRequestMessage Request = null;

        public Task<HttpResponseMessage> ExecuteAsync( System.Threading.CancellationToken cancellationToken )
        {
            HttpResponseMessage response = null;
            if( null != Request )
            {
                response = Request.CreateResponse( StatusCode, this );
            }
            return Task.FromResult( response );
        } // ExecuteAsync()
    }

    [RoutePrefix( "api/v3/Fish" )]
    public class FishController : AnimalController<FishController.FishProperties>
    {
        public class FishProperties
        {
            public bool IsShark;
        }

        protected override FishProperties makeNew( Int32 Id )
        {
            return new FishProperties()
            {
                IsShark = true
            };
        }
    }

    [RoutePrefix( "api/v3/Dog" )]
    public class DogController : AnimalController<DogController.DogProperties>
    {
        public class DogProperties
        {
            public string Breed;
            public Int32 TagNo;
        }

        protected override DogProperties makeNew( Int32 Id )
        {
            return new DogProperties()
            {
                Breed = "Labrador",
                TagNo = 12345
            };
        }

    }
}

Now when I run the same request, I get:

<AnimalReturn2OfDogController.DogPropertiesS2PP9ThI 
    xmlns:i="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns="http://schemas.datacontract.org/2004/07/Areas.API">
    <Properties>
        <Breed>Labrador</Breed>
        <TagNo>12345</TagNo>
    </Properties>
</AnimalReturn2OfDogController.DogPropertiesS2PP9ThI>

The question is: Is there a way to solve the XML serialization problem while still using inheritance?

I have tried using KnownTypes (as suggested here: Unexpected Type - Serialization Exception and here: "Type not expected", using DataContractSerializer - but it's just a simple class, no funny stuff?) in various places and various combinations, though I still suspect there's an easy answer there somewhere.

I have tried disabling proxy classes as well, but that didn't appear to help.

Thanks.

Community
  • 1
  • 1
ssalter
  • 55
  • 7
  • *I have tried using KnownTypes in various places and various combinations* -- where/how did you try using them? – dbc Sep 09 '15 at 18:16
  • I tried adding all of these to AnimalReturn: [KnownType("AnimalResult")],[KnownType("DogController")], [KnownType("DogController.DogProperties")]. I'm sure I tried other things while fiddling with it, but I can't remember all the combinations. – ssalter Sep 10 '15 at 13:25
  • make that KnownType(typeof(AnimalResult)), etc. – ssalter Sep 10 '15 at 18:03

3 Answers3

1

The serialization is being done inside the base class method AnimalResult.ExecuteAsync(), which is calling HttpRequestMessageExtensions.CreateResponse<T> Method (HttpRequestMessage, HttpStatusCode, T):

            response = Request.CreateResponse( StatusCode, this );

Since this is statically typed as an AnimalResult, this type becomes the generic parameter T passed to the extension method. Thus, try adding the types derived from it as KnownType attributes to AnimalResult:

[KnownType(typeof(AnimalReturn<DogController.DogProperties>))]
[KnownType(typeof(AnimalReturn<FishController.FishProperties>))]
public class AnimalResult : IHttpActionResult
{
}

You should also be able to get this to work by using MakeGenericMethod() to call HttpRequestMessageExtensions.CreateResponse<T> with this.GetType() as the type argument. See How to invoke static generic class methods if the generic type parameters are unknown until runtime?

Update

Here is an example of how you could invoke a static generic method in runtime, using the actual type of the AnimalResult being returned, and thereby avoid needing to define all possible derived classes of AnimalResult as known types:

public class AnimalResult : IHttpActionResult
{
    [IgnoreDataMember]
    public HttpStatusCode StatusCode = HttpStatusCode.OK;

    [IgnoreDataMember]
    public HttpRequestMessage Request = null;

    private static HttpResponseMessage StaticCreateResponse<TAnimalResult>(TAnimalResult animalResult) where TAnimalResult : AnimalResult
    {
        if (animalResult == null)
            throw new ArgumentNullException(); // Should have been checked outside.
        Debug.Assert(typeof(TAnimalResult) == animalResult.GetType());
        return animalResult.Request.CreateResponse(animalResult.StatusCode, animalResult);
    }

    HttpResponseMessage CreateResponse()
    {
        if (Request == null)
            return null;
        else
        {
            var method = typeof(AnimalResult).GetMethod("StaticCreateResponse", BindingFlags.NonPublic | BindingFlags.Static);
            var genericMethod = method.MakeGenericMethod(new[] { GetType() });
            return (HttpResponseMessage)genericMethod.Invoke(null, new object[] { this });
        }
    }

    public Task<HttpResponseMessage> ExecuteAsync( CancellationToken cancellationToken )
    {
        var response = CreateResponse();
        return Task.FromResult( response );
    } // ExecuteAsync()
}
dbc
  • 104,963
  • 20
  • 228
  • 340
  • This does solve the problem. Unfortunately, my real code has ~40 derived types, and could get more in the future, and this doesn't seem like a good solution. I'm intrigued with your idea of passing in the type to CreateResponse, but I wasn't able to get it to work. – ssalter Sep 10 '15 at 19:02
  • @ssalter - what did you do to try to pass the type into `CreateResponse`? Can you update your question to show what you did, and what happened? – dbc Sep 10 '15 at 19:13
  • I couldn't find a way to get CreateResponse out of reflection. I ran into this problem: http://stackoverflow.com/questions/28977541/cant-find-getrequestcontext-in-webapi – ssalter Sep 10 '15 at 21:07
  • @ssalter - no idea what's going on with that question, so I updated my answer to make `AnimalResult` dynamically invoke a private static method inside itself. – dbc Sep 10 '15 at 22:21
  • Interesting. I'll mark yours as the solution, though I think what I posted also solves the problem acceptably. The only downside here would be the cost of reflection. – ssalter Sep 11 '15 at 13:51
1

While @dbc's answer does also solve the problem, having to manage the proliferation of KnownType() statements for new classes, with only a runtime error to signal the problem, wasn't going to work for my application.

I did find another way, which does both provide the Type to the class and enforces it at compile time instead of runtime. In my example above, it would look like this:

    public class AnimalReturn<TProperties> : AnimalResult<AnimalReturn<TProperties>>
    {
        public TProperties Properties;
    }

    public class AnimalResult<T> : IHttpActionResult
        where T : class
    {
        [IgnoreDataMember]
        public HttpStatusCode StatusCode = HttpStatusCode.OK;

        [IgnoreDataMember]
        public HttpRequestMessage Request = null;

        public Task<HttpResponseMessage> ExecuteAsync( System.Threading.CancellationToken cancellationToken )
        {
            HttpResponseMessage response = null;
            if( null != Request )
            {
                response = Request.CreateResponse( StatusCode, this as T );
            }
            return Task.FromResult( response );
        } // ExecuteAsync()
    }

So you pass the type you are attempting to return as a generic type into AnimalResult, and then cast it as that type when providing it as a parameter to CreateResponse.

ssalter
  • 55
  • 7
0

Use xml serializer everything will work properly.To change serializer use- config.Formatters.XmlFormatter.UseXmlSerializer = true; in Register method of a class WebApiConfig.

Kumar Waghmode
  • 509
  • 2
  • 18