Recently I ran into a strange error when working on an ODataController. On several occasions (especially on CREATE or UPDATE operations) we do not want to return the actual underlying entity of the controller, but instead would like to return an entity that can be used for synchronisation such as a Job
.
So instead of doing something like this and return the actual entity of the controller …
public class ApprovalsController : ODataController { [Authorize] public async Task<IHttpActionResult> Put([FromODataUri] long key, Approval approval) { Contract.Requires(0 < key, "|400|"); Contract.Requires(null != approval, "|400|"); var entity = await db.Approvals.FindAsync(key); Contract.Assert(null != entity, "|404|"); // do something outstanding with the approval ... var result = ApprovalManager.ProcessSync(approval); Contract.Assert(result, "|410|"); // ... and return the processed approval return Ok(approval); } }
… we would like to return something like that (a job used to track the status of the entity):
public class ApprovalsController : ODataController { [Authorize] public async Task<IHttpActionResult> Put([FromODataUri] long key, Approval approval) { Contract.Requires(0 < key, "|400|"); Contract.Requires(null != approval, "|400|"); var entity = await db.Approvals.FindAsync(key); Contract.Assert(null != entity, "|404|"); // do something marvelous with the approval ... var job = ApprovalManager.ProcessAsync(approval); Contract.Assert(null != job, "|410|"); // ... and return a job that can be used to track the status of the operation return ResponseMessage(ODataControllerHelper.ResponseAccepted<Job>(this, job)); } }
In the above example Job
is a standard OData (EntityFramework based) entity that has a NavigationLink
called EntityKind
that is also exposed via an OData controller. The Approval
entity (also exposed via an OData controller) does not have the EntityKind
property on its model.
When testing the example where we return the Job
we get an error on the Web.Api pipeline stating the following:
message=No NavigationLink factory was found for the navigation property 'EntityKind' from entity type 'biz.dfch.CS.Appclusive.Core.OdataServices.Core.Job' on entity set 'Approvals'. Try calling HasNavigationPropertyLink on the EntitySetConfiguration. Parameter name: navigationProperty
The error message is actually true – Approval
does not have an EntityKind
property (as stated above), but this is actually not wanted. However, the OData framework somehow expects that on the entity, even though we specified the returned type via ResponseMessage(ResponseAccepted)
.
I tried several approaches (like defining manual NavigationPropertyLink
s on the ApprovalsController
) but I have not found a way to circumvent this error (except of course by defining the EntityKind
on Approval
).
The only way I managed to actually return a type that is different from the controller’s type is to create an additional model (called JobResponse
in my example) that has no NavigationPropertyLink
s at all and to return that to the client. For that model I also had to define a JobResponsesController
otherwise I would receive another OData error stating that only defined types could be returned. I put all actions on that controller to throw new NotImplementedException();
as it is not used (and not supposed to be used) at all directly, but only to return tracking information to the caller.
public class JobResponse { [Key] public long Id { get; set; } public string Message { get; set; } } public class ApprovalsController : ODataController { [Authorize] public async Task<IHttpActionResult> Put([FromODataUri] long key, Approval approval) { Contract.Requires(0 < key, "|400|"); Contract.Requires(null != approval, "|400|"); var entity = await db.Approvals.FindAsync(key); Contract.Assert(null != entity, "|404|"); // do something marvelous with the approval ... var job = ApprovalManager.ProcessAsync(approval); Contract.Assert(null != job, "|410|"); // ... and return a job that can be used to track the status of the operation var jobResponse = CreateJobResponseFromJob(job); Contract.Assert(null != jobResponse, "|410|"); var response = this.ResponseMessage(ODataControllerHelper.ResponseAccepted<JobResponse>(controller, jobResponse)); } }
I do not know if this is a bug or by design – at least a major nuisance to me.