Thursday, December 4, 2014

ASP.Net OData V4 with inherited key: A primitive value was specified; however, a value of the non-primitive type '' was expected

At the time of writing this, there is absolutely no help on the net on this error I keep on getting. By trial and error, I've been able not to explain the message, but at least find the cause.

In my case, I have two classes:

class BaseClass
{
  public int ID {get; set;}
}
class DerivedClass : BaseClass {  public string A { get; set; }
}

And in the modelBuilder configuration (which may not be necessary here but in my case, the builder would treat some other property as the primary key instead of this one and caused other cryptic errors):

builder.EntitySet<DerivedClass>("DerivedClasses").EntityType.HasKey(c => c.ID);

Now in my DerivesClassesController ODataController, if I try to access the derived class like so:

http://someaddress/api/DerivedClasses

I get this OData V4 formatted error (in french) :
{ "error":{ "code":"","message":"An error has occurred.","innererror":{ "message":"The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; odata.metadata=minimal'.","type":"System.InvalidOperationException","stacktrace":"","internalexception":{ "message":"Une valeur primitive a \u00e9t\u00e9 sp\u00e9cifi\u00e9e alors qu'une valeur de type non primitif '' \u00e9tait attendue.","type":"Microsoft.OData.Core.ODataException","stacktrace":" \u00e0 Microsoft.OData.Core.ValidationUtils.ValidateIsExpectedPrimitiveType(Object value, IEdmPrimitiveTypeReference valuePrimitiveTypeReference, IEdmTypeReference expectedTypeReference, Boolean bypassValidation)\r\n \u00e0 .............................
(Here I used to suggest defining a "new int ID" in the DerivedClass which worked but was kind of dirty and defeated the whole base class purpose. Here's a better way:)

The trick is to map the base class' ID property:

builder.EntityType<BaseClass>().HasKey(c => c.ID);

And do not remap the key on each derived class or you'll end up with a cryptic "The sequence contains more than one element" error.

The problem with this, however, is that the builder will try to map every entity that is inherited from the base class which you might or might not want. As of this writing, there is no way of preventing this automatic discovery.

So another trick is to explicitly ignore all derived types and only after that, remap wanted entities. Doing this by hand would be problematic wouldn't it. So here's a nice reflective loop that'll do the ignore part in a couple of lines. Place this just after the base class key definition so you end up with:

builder.EntitySet<DerivedClass>("DerivedClasses").EntityType.HasKey(c => c.ID);

var types = AppDomain.CurrentDomain.GetAssemblies()
 .SelectMany(a => a.GetTypes())
 .Where(t => t.IsSubclassOf(typeof(AbstractModel)));

foreach (var type in types)
 builder.Ignore(types.ToArray());

Then proceed on the derived types mappings as usual.

2 comments:

Gabriel said...

THANK YOU SO MUCH I spent hours on this, such a cryptic error for a major framework

Jerther said...

Glad I could help :)