How do I post an entity to an OData endpoint while at the same time associating it with other existing entities in the body?
Consider the following class structure (sample):
class Invoice
{
public int Id { get; set; }
public Person Issuer { get; set; }
public Person Recipient { get; set; }
// other properties
}
class Person
{
public int Id { get; set; }
// other properties
}
Both Invoice
and Person
are entities in my domain (thus the Id
property). Imagine that both are exposed in their own entitysets, so:
GET http://host/odata/People(1)
returns Person
with Id = 1
GET http://host/odata/Invoices(2)?$expand='Issuer, Recipient'
returns Invoice
with Id = 2
and both Issuer
and Recipient
expanded in the payload
Now consider the following requirement:
I want to create a new invoice in the system that will be associated to an existing issuer and recipient
How do I "tell" the OData framework that I want to associate a given navigation property to an existing entity? How would my controller action be notified that this is the intention?
Ideally, I'd like a POST body to look like this:
POST http://host/odata/Invoices
{ "Issuer": "/odata/People(1)", "Recipient": "/odata/People(2)", "Property1": "someValue", "Property2": "100", ... }
Once the server receives this payload, it should:
Person
for the Issuer
property. If it does not exist, a bad request should be returned.Person
for the Recipient
property. If it does not exist, a bad request should be returned.Invoice
instance and assign the Issuer
and Recipient
from above, then save it to the database.I know OData has support for configuring the relationships after-the-fact with special PUT/POST URLs using the entity/relation/$ref
syntax. With such syntax, I'd be able to do something like this:
POST http://host/odata/Invoices
{ "Property1": "someValue", "Property2": "100" }
PUT http://host/odata/Invoices(x)/Issuer/$ref
{"@odata.id":"http://host/odata/People(1)"}
PUT http://host/odata/Invoices(x)/Recipient/$ref
{"@odata.id":"http://host/odata/People(2)"}
However, I want to be able to perform this all in a single POST operation that should atomically create the instance.
I tried a few ideas to see what the server would accept, and this seemed to go through:
{
"Issuer": { "@odata.id": "/odata/People(1)" },
"Recipient": { "@odata.id": "/odata/People(2)" },
"Property1": "someValue",
"Property2": "100",
...
}
But I have no idea how I'd be able to read/parse the IDs from this (like how it is done in a dedicated Ref
method), or even if this is supported in the OData standard.
For now, I'll resort to just passing the ID
property in the model and in the server assuming this will always mean an existing relationship, but that's far from ideal as it is not general-purpose enough and would make my API inflexible.
The simplest solution is to expose the ForeignKey
properties in your Model directly. Even the Models used in the MS Doc Entity Relations in OData v4 Using ASP.NET Web API 2.2 explaining $ref
expose the FKs.
class Invoice
{
public int Id { get; set; }
public int Issuer_Person_Id { get; set; }
[ForeignKey(nameof(Issuer_Person_Id)]
public Person Issuer { get; set; }
public int Recipient_Person_Id { get; set; }
[ForeignKey(nameof(Recipient_Person_Id)]
public Person Recipient { get; set; }
// other properties
}
This doesn't make your API inflexible, rather it makes your API MORE flexible. This also grants you greater control over the DataBase Implementation of your Model, whilst still being database engine agnostic.
In environments where Lazy Loading is enabled, including FKs has some added performance benefits if you need to check the existence of a related entity without requiring it to be loaded into memory.
NOTE: By including the FKs in the Model the
$ref
syntax and batching can still be used, but we now have access to the more practical FK Id values that can be easily validated in the server side code, just as it is easier to send the values.
Now in the PATCH
or POST
we can simply use the Ids directly to link the Person
records.
The same level of information/understanding is required at both the client and server sides to achieve this, so it is still general purpose, the
$metadata
document fully describes which FK fields link the related entities but a good naming convention as demonstrated here can help
{
"Issuer_Person_Id": 1,
"Recipient_Person_Id": 2,
"Property1": "someValue",
"Property2": "100",
...
}
Be Careful:
One of the reasons that many Model designers choose NOT to exposeForeignKey
properties is that ambiguity exists when or if you try to send or process both the ForeignKey and the related Entity.
For aPATCH
there is no confusion, the v4.0 specification tells use specifically to ignore the related entity and that it shouldn't be sent at all.11.4.3 Update an Entity
If an update specifies both a binding to a single-valued navigation property and a dependent property that is tied to a key property of the principal entity according to the same navigation property, then the dependent property is ignored and the relationship is updated according to the value specified in the binding.For a
POST
however if the related entity is provided in the request as well as the FK, the related entity is assumed to be a deep insert and the FK is ignored.11.4.2.2 Create Related Entities when Creating an Entity
Each included related entity is processed observing the rules for creating an entity as if it was posted against the original target URL extended with the navigation path to this related entity.With FKs enabled My advice therefore is to take steps on the client side to make sure you don't try to send both the FK and the related entities in requests back to the API.
I agree that the @odata.id in the post as you have suggested is a logical conclusion, however it raises other potential implementation issues which is why the protocol provides the concept of direct CRUD operations against the $ref
endpoint that represents the ForeignKey reference.
OData V4.0 was specifically declarative and designed such that operations against a single resource should only affect that resource. This is why we cannot PATCH
related properties in a single query, as with this referencing issue, there are too many potential implementation variations and interpretations of how it might work that they kept the specification concise and constrained in the way that it is.
Basically a consensus between interested parties could not be reached on the protocol specifics and guidance on how to handle deep updates before the specification was drafted. The ASP.Net FX and Core implementations (as of this post) are only OData 4.0 Minimal Conformance Level OOTB. There is a lot you need to do to increase the level of conformance.
Batching is the preferred mechanism to perform operations that "might" affect multiple resources in a single transacted query, however it is a more involved solution than if you just expose the FKs!
Whilst it's nice that we can use complicated syntax and batching to achieve this in other ways, there are many other practical benefits to exposing the FK Ids in the Model and making them accessible to the client side, not just in the server logic, IMO these can be really big benefits in the right scenarios:
Optimised data retrieval in Grids
If many rows have a link to the same record in another table you only need to download the linked value from the common table once. For some types of data it will be more efficient to download all the possible lookup values from the common tables and then in your presentation layer you can join in the results based on the Ids. These lookup values may only need to be downloaded once in the whole session in some use cases.
ComboBox relationship assignments
There is a time and a place, but by including the FKs in your Model it is very simple to bind to ComboBox
or DropDownList
implemenations in the presentation layer to change or assign the related entities, the implementation is almost identical to the grid presentation, bind the control the FK, and show the related entities in the drop down list.
OData v4.01 Minimal Conformance level SHOULD support deep updates
But the current version of ODataLib (v8) used by the .Net 5 runtime does not support this feature OOTB and is still only minimally compliant to v4.0, albeit with some of the more advanced features than before.
11.4.3.1 Update Related Entities When Updating an Entity
Payloads with an OData-Version header with a value of 4.01 or greater MAY include nested entities and entity references that specify the full set of to be related entities, or a nested delta payload representing the related entities that have been added, removed, or changed. Such a request is referred to as a “deep update”. If the nested collection is represented identical to an expanded navigation property, then the set of nested entities and entity references specified in a successful update request represents the full set of entities to be related according to that relationship and MUST NOT include added links, deleted links, or deleted entities.
The json payload implementation is similar to your suggestion:
{
"@type":"#container.Invoice",
"Issuer": { "@id": "People(1)" },
"Recipient": { "@id": "People(2)" },
"Property1": "someValue",
"Property2": "100",
...
}
There is now also a nested delta representation to Add, Remove or Update links as well as nested values in a single request, but these advanced mechanisms are not yet implemented in the ODataLib runtimes.