Search code examples
c#serializationjson.netasp.net-web-api2odata

OData V4 failing to properly serialize list of POCO(?)s containing a System.Object property


I'm writing an OData V4 service with Web API 2 using the currently available OData NuGet packages.

I have an Entity Set of class Foo like so:

class Foo {
    string SomePropertyUnrelatedToThePost {get; set;}
    ...
    IList<Bar> TheImportantPropertyList {get; set;}
}

Bar doesn't have too much going on:

class Bar {
    string Name {get; set;}
    int? Group {get; set;}
    object Value {get; set;}
}

In use, Bar#Value is never assigned anything other than basic values, but some are primitives and some are not: bool, byte, char, short, int, long, string, Decimal, DateTime...

I am registering the Foo set as the docs instruct, using an ODataConventionModelBuilder like so:

...
builder.EntitySet<Foo>("Foos"); 

and registering my Bar as a complex type with builder.ComplexType<Bar>(); does not seem to change the outcome here.

The problem is that when I return a Foo object in my ODataController, the JSON response does not include Bar#Value.

{
  ...
  "SomePropertyUnrelatedToThePost": "Foo was here",
  ...
  "TheImportantPropertyList": [
     {
        "Name": "TheAnswer",
        "Group": null
     },
     {
        "Name": "TheQuestion",
        "Group": null
     }
   ]
}

Adding to my confusion is the fact that I can manually serialize a Foo in my controller method like so:

var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings;//.CreateJsonSerializer();
var s = JsonSerializer.Create(settings);
...
var json = Encoding.Default.GetString(...);

to produce a properly serialized result:

{
  "SomePropertyUnrelatedToThePost": "Foo was here",
  ...
  "TheImportantPropertyList": [
     {
        "Name": "TheAnswer",
        "Value": 42,
        "Group": null
     },
     {
        "Name": "TheQuestion",
        "Value": "What is the airspeed velocity of an unladen swallow?",
        "Group": null
     }
   ]
}

Am I configuring OData incorrectly? Do I have some other core misunderstanding? As I wrote this question it occurred to me that if I changed my model to include the System.Type of the assigned Value property, I could write a custom serializer, but it seems like it shouldn't have to come to that.

Edit: When I'm manually serializing my Foo, I'm not using the default OData serializer, I'm using a new Newtonsoft JsonSerializer. The default OData serializer and deserializers simply do not like properties of type Object.


Solution

  • I got this going. This post helped. Being new to OData, it took a while to get through the documentation, as most of it is out of date.

    In my WebApiConfig.cs, I used the new method of injecting an ODataSerializerProvider into OData:

    config.MapODataServiceRoute("odata", "api/v1", b =>
                   b.AddService(ServiceLifetime.Singleton, sp => builder.GetEdmModel())
                    .AddService<ODataSerializerProvider>(ServiceLifetime.Singleton, sp => new MySerializerProvider(sp)));
    

    MySerializerProvider:

    internal sealed class MySerializerProvider : DefaultODataSerializerProvider
    {
        private MySerializer _mySerializer;
    
        public MySerializerProvider(IServiceProvider sp) : base(sp)
        {
            _mySerializer = new MySerializer(this);
        }
    
        public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
        {
            var fullName = edmType.FullName();
    
            if (fullName == "Namespace.Bar")
                return _mySerializer;            
            else
                return base.GetEdmTypeSerializer(edmType);            
        }        
    }
    

    In my custom serializer, I noted OData will not automatically convert a DateTime to a DateTimeOffset. MySerializer:

    internal sealed class MySerializer : ODataResourceSerializer
    {
      public MySerializer(ODataSerializerProvider sp) : base(sp) { }
    
      public override ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
      {
          ODataResource resource = base.CreateResource(selectExpandNode, resourceContext);
    
          if (resource != null && resourceContext.ResourceInstance is Bar b)
              resource = BarToResource(b);
    
          return resource;
      }
    
      private ODataResource BarToResource(Bar b)
      {
          var odr = new ODataResource
          {
              Properties = new List<ODataProperty>
              {
                  new ODataProperty
                  {
                      Name = "Name",
                      Value = b.Name
                  },
                  new ODataProperty
                  {
                      Name = "Value",
                      Value = b.Value is DateTime dt ? new DateTimeOffset(dt) : b.Value
                  },
                  new ODataProperty
                  {
                      Name = "Group",
                      Value = b.Group
                  },
              }
          };
    
          return odr;
      }
    }
    

    I realize this is a pretty specific question and answer but I hope someone finds it useful.