OData V4 client adding child entity

2019-09-08 03:56发布

问题:

I have a parent (Order) and child (OrderDetail) where Order already exists in the database and OrderDetail also exists in the database.
All I really want to do is add another OrderDetail record bound to the Order.

I have been down several paths and I'm not even sure what is the correct path.
Let's make some assumptions that navigations between these are already working.
I can $expand=OrderDetails fine and I can also Orders(1)/OrderDetails fine and do the reverse from OrderDetails.

Based on this Updating the Data Service, all I need to do is call AddRelatedObject and then Add the object to the OrderDetails collection.

// Add the new item with a link to the related Order
context.AddRelatedObject(order, "OrderDetails", newOrderDetail);
// Add the new order detail to the collection
order.Order_Details.Add(newOrderDetail);
newOrderDetail.Order = order;

Seems simple enough.
Yet when I execute context.SaveChanges(SaveChangesOptions.ReplaceOnUpdate) it will throw an error.

{"error":{"code":"","message":"No HTTP resource was found that matches the request URI 'http://localhost/Test/odata/Orders(1)/OrderDetails'.","innererror":{"message":"No routing convention was found to select an action for the OData path with template '~/entityset/key/navigation'.","type":"","stacktrace":""}}}

But if I navigate to the URL listed, it shows data.
Time for Fiddler.
In Fiddler, I can see this is a POST to the URL and not a GET.
Which it should be a POST but not to the URL listed.
The POST should have been to /odata/OrderDetails

Round 2

// Add the new item with a link to the related Order
context.AttachTo("OrderDetails", newOrderDetail);
// Add a link between Order and the new OrderDetail
context.AddLink(order, "OrderDetails", newOrderDetail);
// Add the new order detail to the collection
order.Order_Details.Add(newOrderDetail);
newOrderDetail.Order = order;

Still a POST with an error but the URL is slightly different and the json posted only has "/odata/OrderDetail(0)" it also now has "$ref".

{"error":{"code":"","message":"No HTTP resource was found that matches the request URI 'http://localhost/Test/odata/Orders(1)/OrderDetails/$ref'.","innererror":{"message":"No routing convention was found to select an action for the OData path with template '~/entityset/key/navigation/$ref'.","type":"","stacktrace":""}}}

Well a quick web search led me to this article Entity Relations in OData v4 Using ASP.NET Web API 2.2
This article says I need to add a "CreateRef" in the Orders controller.
I did create a "CreateRef" in the Orders controller and sure enough it gets called BUT the article assumes the OrderDetail exists in the database.
It is not posting the json OrderDetail object.

Round 3

// Add the new item with a link to the related Order
context.AttachTo("OrderDetails", newOrderDetail);
// Attach a link between Order and the new OrderDetail
context.AttachLink(order, "OrderDetails", newOrderDetail);
// Add the new order detail to the collection
order.Order_Details.Add(newOrderDetail);
newOrderDetail.Order = order;

Well this seems much better.
No error but it did not fully work.
It sent a PUT to the /odata/OrderDetails(0) and it did send the json OrderDetail object BUT this should have been a POST not a PUT.

I feel I am so close yet I can't seem to figure out how to get it to work properly.

Any ideas?

回答1:

I had the same problem and found the solution today.

Take a look at http://aspnetwebstack.codeplex.com/discussions/457028 :

There is no inbuilt convention to handle POST requests to ~/entityset(key)/navigation. You have to build one yourself. Check out this a sample code for that.

You need first to create a EntitySetRoutingConvention :

public class CreateNavigationPropertyRoutingConvention : EntitySetRoutingConvention
{
    public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
    {
        if (odataPath.PathTemplate == "~/entityset/key/navigation" && controllerContext.Request.Method == HttpMethod.Post)
        {
            IEdmNavigationProperty navigationProperty = (odataPath.Segments[2] as NavigationPathSegment).NavigationProperty;
            controllerContext.RouteData.Values["key"] = (odataPath.Segments[1] as KeyValuePathSegment).Value; // set the key for model binding.
            return "PostTo" + navigationProperty.Name;
        }

        return null;
    }
}

Then, you have to register it in WebApiConfig.Register:

var routingConventions = ODataRoutingConventions.CreateDefault();
routingConventions.Insert(0, new CreateNavigationPropertyRoutingConvention());
server.Configuration.Routes.MapODataRoute("odata", "", GetEdmModel(), new DefaultODataPathHandler(), routingConventions);

Please note this sample is for oData v3 but can easily be converted to v4.

Then, you'll just have to add your parent object to the context and use AddRelatedObject for all the children. Your request will be send to this void in your ParentController:

public HttpResponseMessage PostToOrders([FromODataUri] int key, Order order)
    {
        // create order.
        return Request.CreateResponse(HttpStatusCode.Created, order);
    }


回答2:

After trial and error I found something that worked.

// create a new order detail
OrderDetail newOrderDetail = new OrderDetail();
// set the orderID on the new order detail
newOrderDetail.OrderID = order.ID;
// add the order back as a link on the order detail
newOrderDetail.Order = order;
// add the order detail to the order detail collection on the order
order.OrderDetails.Add(newOrderDetail);
// add the order detail to the context
context.AddToOrderDetail(newOrderDetail);
// now update context for the order
context.UpdateObject(order);
// now save
context.SaveChanges();