OData Entity Creation with Navigation Properties

Topics: ASP.NET Web API
Sep 16, 2013 at 10:24 AM
If I have a model as such:
public class Customer
{
  public int Id { get; set; };
  public ICollection<Order> Orders { get; set; };
}

public class Order
{
  public int Id { get; set; };
  public string Name { get; set; }
  
  [Required]
  public Customer Customer { get; set; }
}
Is there any way I can create a new Order object with a single OData POST request, where Customer is an existing object?

With Web API 2 Release Candidate I have attempted to submit the following POST but on the Web API EntitySetController, the bound Entity passed to CreateEntity virtual method is null when I submit this (I have also expanded the __metadata object to include id = uri and the type with no success):
{
  "Name":"MyTestOrder",
  "Customer":
   {
      "__metadata":
      { 
         "uri":"http://localhost/odata/Customers(10)"
      }
   }
}
I also tried using WCF Data Services Client with following code:
var customer = ctx.Customers.Where(c => c.Id == 10).SingleOrDefault();
var newOrder = new Order { Name = "TestOrder", Customer = customer };
ctx.AddToOrders(newOrder);
ctx.AddLink(customer, "Orders", newOrder);
ctx.SetLink(newOrder, "Customer", customer);
ctx.SaveChanges();
This generates the following JSON in a single POST request:
{
  "odata.type":"MyModel.Order",
  "Customer@odata.bind":"http://localhost/odata/Customers(10)",
  "Id":0,
  "Name":"Test Order"
}
But on the Web API side, again the model binding passes null into the CreateEntity(TEntity entity) method on the EntitySetController.

I realize I could expose the Foreign Key property on the Order entity, but this kind of goes against the principles of HATEOAS.

So I'd like to know if I can do it by including a navigation link in the POST request.
Sep 16, 2013 at 9:21 PM
We don't support having deserializing links to related objects in requests. There are two ways to do this in a single request.

1) OData batching - by doing
POST /service/$batch HTTP/1.1 
Host: host 
Content-Type: multipart/mixed; boundary=batch_36522ad7-fc75-4b56-8c71-56071383e77b 

--batch_36522ad7-fc75-4b56-8c71-56071383e77b 
Content-Type: multipart/mixed; boundary=changeset_77162fcd-b8da-41ac-a9f8-9357efbbd621 
Content-Length: ###       

--changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621) 
Content-Type: application/http 
Content-Transfer-Encoding: binary 
Content-ID: 1 

POST /service/Orders HTTP/1.1 
Host: host  
Content-Type: application/json 
Content-Length: ### 

<Order json> 

--changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621) 
Content-Type: application/http 
Content-Transfer-Encoding: binary 

POST $1/$links/Customer HTTP/1.1 
Host: host 
Content-Type: application/json;
Content-Length: ### 

<customer ID link> 

--changeset(77162fcd-b8da-41ac-a9f8-9357efbbd621)-- 
--batch(36522ad7-fc75-4b56-8c71-56071383e77b)--
2) Post the Order to ~/Customers(id)/Orders
Sep 16, 2013 at 9:27 PM
Great thanks!
Sep 18, 2013 at 9:27 PM
raghuramn wrote:
We don't support having deserializing links to related objects in requests. There are two ways to do this in a single request.


2) Post the Order to ~/Customers(id)/Orders
For now this seems the simpler option.

Is there an out of the box convention for processing a POST request to ~/Customers(id)/Orders?

I know there is one for GET ~/Customers(id)/Orders but I'm not if one exists for POST.

I'm guessing that I need to create an OData Action?
Sep 18, 2013 at 9:35 PM
You guessed it right. There is no inbuilt convention to handle POST requests to ~/entityset(key)/navigation. You have to build one yourself. Check out this sample code for that.

You don't need an OData action to handle this. Just a custom routing convention as shown in the sample code above.
Sep 19, 2013 at 1:39 AM
Great thanks, that works and helps me understand a little more how OData routing works in Web API.

Sorry with all the questions, but because I'm returning an HttpResponseMessage, I'm assuming I need to take care of Location header and also parse the Prefer header in the Request (i.e. for "no-content" or "return-content" etc)?

Are there any public helpers in the OData Web API stack that I can call to assist with this?
Sep 19, 2013 at 3:39 AM
You can do something like this instead of returning HttpResponseMessage with the latest bits.
        public IHttpActionResult Post(Customer c)
        {
            // add the customer.

            return Created(c);
        }
Created() takes care of the location header and the prefer header. You can similarly use Updated() for PUT/PATCH requests.
Sep 19, 2013 at 6:49 AM
Edited Sep 19, 2013 at 6:50 AM
Hi

Thanks again. I found the Location header returned is the location of the base entity you are adding a child object to - e.g. http://localhost/odata/Customers(1)

I used the following to create a more meaningful Location:
[HttpPost]
public IHttpActionResult AddToOrders([FromODataUri] int key, Order order
{
    var createdOrder = myService.Add(order);

    var location = String.Format("{0}/{1}({2})/{3}({4})", Request.RequestUri, "Customers", key, "Orders", createdOrder.Id);

    return Created(location, createdOrder)
}
May 5, 2014 at 12:45 AM
In looking at the NavigationRoutingConvention class (which appears to be part of the default OData routing conventions), the custom routing convention should no longer be needed and POST requests to ~/entityset(key)/navigation are now being handled out of the box by using the "PostTo" method name prefix.