This is a bug in Web API OData, nightly build. I believe this is a bug in `ODataEntityTypeSerializer`, but it is also likely a design flaw in EntitySetController.
In OData, it's valid to have a url like http://localhost/EntitySet(10)?$expand=Property1/SubProperty,Property2 , where 10 is an ID in EntitySet. I haven't verified that this is valid in the odata spec, but it's supported by WCF Data Services server and client. More importantly, such URLs are built by the WCF data services client if you create a query with `Expand` and with `where ID == 10`.
In `EntitySetController`, the default implementation of the Get(TKey) action calls `GetEntityByKey(key)`, then returns the entity or `null` if not found - so there's clearly no query support for executing the expand.
If I add the [Queryable] attribute:
``` C#
[Queryable(AllowedQueryOptions = AllowedQueryOptions.All, MaxExpansionDepth = 15)]
public virtual HttpResponseMessage Get([FromODataUri] TKey key)
{
TEntity entity = GetEntityByKey(key);
return EntitySetControllerHelpers.GetByKeyResponse(Request, entity);
}
```
and invoke such a query, all the link collections are empty (not surprising), and all the navigation properties/links throw an exception during serialization, like:
``` xml
<m:type>System.InvalidOperationException</m:type>
<m:stacktrace></m:stacktrace>
<m:internalexception>
<m:message>The EDM instance of type '[Property1Type Nullable=True]' is missing the property 'Title'.</m:message>
<m:type>System.InvalidOperationException</m:type>
<m:stacktrace> at System.Web.Http.OData.EntityInstanceContext.GetPropertyValue(String propertyName)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.CreateStructuralProperty(IEdmStructuralProperty structuralProperty, EntityInstanceContext entityInstanceContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.CreateStructuralPropertyBag(IEnumerable`1 structuralProperties, EntityInstanceContext entityInstanceContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteEntry(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteObjectInline(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteExpandedNavigationProperty(KeyValuePair`2 navigationPropertyToExpand, EntityInstanceContext entityInstanceContext, ODataWriter writer)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteExpandedNavigationProperties(IDictionary`2 navigationPropertiesToExpand, EntityInstanceContext entityInstanceContext, ODataWriter writer)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteEntry(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteObjectInline(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteObject(Object graph, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
--- 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.Runtime.CompilerServices.TaskAwaiter.GetResult()
at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__10.MoveNext()</m:stacktrace>
</m:internalexception>
```
Note that this exception occurs from serializing a null relationship object, and even though the object is null, the error message says the null object's property is missing. So the error is misleading, and it shouldn't happen.
Further, I can't add support for such queries in `Get(TKey)`, because I can't process `ODataQueryOptions` in the normal way. For more info on that, see https://aspnetwebstack.codeplex.com/workitem/1073
.
To be clear, I'd like to implement something like:
``` C#
public virtual HttpResponseMessage Get([FromODataUri] TKey key, ODataQueryOptions<TEntity> queryOptions)
{
queryOptions.Validate(QueryValidationSettings);
IQueryable<TEntity> query = GetBaseQueryable().Where(e => e.ID == key);
IQueryable queryOptionsApplied = queryOptions.ApplyTo(query);
TEntity entity = queryOptionsApplied.Cast<TEntity>().FirstOrDefault();
return EntitySetControllerHelpers.GetByKeyResponse(Request, entity);
}
```
- but I can't due to the bug in 1073.
Comments: Personally I think the design needs review, the odata implementation and EntitySetController/ AsyncEntitySetController needs to support composite key entities out of the box. Just my two pennies worth.
In OData, it's valid to have a url like http://localhost/EntitySet(10)?$expand=Property1/SubProperty,Property2 , where 10 is an ID in EntitySet. I haven't verified that this is valid in the odata spec, but it's supported by WCF Data Services server and client. More importantly, such URLs are built by the WCF data services client if you create a query with `Expand` and with `where ID == 10`.
In `EntitySetController`, the default implementation of the Get(TKey) action calls `GetEntityByKey(key)`, then returns the entity or `null` if not found - so there's clearly no query support for executing the expand.
If I add the [Queryable] attribute:
``` C#
[Queryable(AllowedQueryOptions = AllowedQueryOptions.All, MaxExpansionDepth = 15)]
public virtual HttpResponseMessage Get([FromODataUri] TKey key)
{
TEntity entity = GetEntityByKey(key);
return EntitySetControllerHelpers.GetByKeyResponse(Request, entity);
}
```
and invoke such a query, all the link collections are empty (not surprising), and all the navigation properties/links throw an exception during serialization, like:
``` xml
<m:type>System.InvalidOperationException</m:type>
<m:stacktrace></m:stacktrace>
<m:internalexception>
<m:message>The EDM instance of type '[Property1Type Nullable=True]' is missing the property 'Title'.</m:message>
<m:type>System.InvalidOperationException</m:type>
<m:stacktrace> at System.Web.Http.OData.EntityInstanceContext.GetPropertyValue(String propertyName)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.CreateStructuralProperty(IEdmStructuralProperty structuralProperty, EntityInstanceContext entityInstanceContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.CreateStructuralPropertyBag(IEnumerable`1 structuralProperties, EntityInstanceContext entityInstanceContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteEntry(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteObjectInline(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteExpandedNavigationProperty(KeyValuePair`2 navigationPropertyToExpand, EntityInstanceContext entityInstanceContext, ODataWriter writer)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteExpandedNavigationProperties(IDictionary`2 navigationPropertiesToExpand, EntityInstanceContext entityInstanceContext, ODataWriter writer)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteEntry(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteObjectInline(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteObject(Object graph, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
--- 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.Runtime.CompilerServices.TaskAwaiter.GetResult()
at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__10.MoveNext()</m:stacktrace>
</m:internalexception>
```
Note that this exception occurs from serializing a null relationship object, and even though the object is null, the error message says the null object's property is missing. So the error is misleading, and it shouldn't happen.
Further, I can't add support for such queries in `Get(TKey)`, because I can't process `ODataQueryOptions` in the normal way. For more info on that, see https://aspnetwebstack.codeplex.com/workitem/1073
.
To be clear, I'd like to implement something like:
``` C#
public virtual HttpResponseMessage Get([FromODataUri] TKey key, ODataQueryOptions<TEntity> queryOptions)
{
queryOptions.Validate(QueryValidationSettings);
IQueryable<TEntity> query = GetBaseQueryable().Where(e => e.ID == key);
IQueryable queryOptionsApplied = queryOptions.ApplyTo(query);
TEntity entity = queryOptionsApplied.Cast<TEntity>().FirstOrDefault();
return EntitySetControllerHelpers.GetByKeyResponse(Request, entity);
}
```
- but I can't due to the bug in 1073.
Comments: Personally I think the design needs review, the odata implementation and EntitySetController/ AsyncEntitySetController needs to support composite key entities out of the box. Just my two pennies worth.