This is a series of blogs detailing my introduction to using OData v4 with C# and ASP.NET WebApi.
The source code for the project can be found here.
Creating the Project
The Project is called Part13
Entity Relationships
So far in this series of blogs we have been dealing with single entities. In real world applications we would deal with lots of entities and have relationships between them.
Entities can be related between one and other as follows:
- One to zero/one relationship
- One to many relationship
- Many to many relationship
You need to determine which entity is used to navigate to the other entity. It needs to make sense to the end user.
For example; keeping it simple, we have two entities, Title and Royalty which lists book titles and the percentage of royalties the authors would receive for each title. The model looks like this.
public class Title { [Key] public int Id { get; set; } [Required] public string Name { get; set; } }
public class Royalty { [Key] public int Id { get; set; } public int TitleId { get; set; } public int LowRange { get; set; } public int HighRange { get; set; } public int Percentage { get; set; } }
There is a one to many relationship between Title and Royalty.
So the question now is, “which entity do we add the relationship to?”. Let’s think about the end user. It seems more intuitive to select a book and then check the royally information than to select a royalty and then see what book it relates to. So the navigation should appear within the Title entity.
public class Title
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public List<Royalty> Royalties { get; set; }
}
Since it is a one to many, we are returning a collection. If it was a one to zero or one, we would return a single instance of Royalty. In the Entity Data Model this field is referred to as a Navigation property.
Creating the Entity Data Model
We can use the EDM conventional model builder because collections are automatically picked up and relationships created.
private static IEdmModel GetEdmModel() { ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); builder.EntityType<Royalty>(); builder.EntityType<Title>(); builder.EntitySet<Title>("Titles"); return builder.GetEdmModel(); }
View the Model
With our classes and EDM created we have enough information to view them in Postman.
From Postman issue a GET request with http://localhost:40000/OData/$metadata.
We can see a Navigation Property has been added to the Title entity.
Querying Titles
I am not going to show the code for Title. It’s the same as code we have done previously.
From Postman issue a GET request with http://localhost:40000/OData/Titles.
Nothing out of the ordinary however; it makes no mention of our navigation property. This is because I have kept the Accept header set with application/json; odata.metadata=none.
Let’s change it to application/json; odata.metadata=full
That looks a bit different. We now see two sets of link to Royalties. The association link with the $ref is used when adding data with POST and PUT. I am not covering updates.
Querying Royalties
We have defined a one to many relationship which is why we need to add a title key in order to navigate to royalties.
In order to navigate to Royalties we need to add a couple of actions in our Titles controller.
// OData/Titles(Id)/Royalties [ODataRoute("Titles({Id})/Royalties")] public IQueryable<Royalty> GetRoyaltiesForTitle([FromODataUri] string Id) { return _repo.GetRoyalties(Id); } // OData/Titles(Id)/Royalties(Id) [ODataRoute("Titles({Id})/Royalties({Key})")] public Royalty GetRoyaltyForTitle([FromODataUri] string Id, [FromODataUri] string Key) { return _repo.GetRoyalty(Id, Key); }
Switch back to using None or Minimal in the Accept header or you will get an error when querying the data below. I will show you how to fix that later.
From Postman issue a GET request with http://localhost:40000/OData/Titles(1)/Royalties.
From Postman issue a GET request with http://localhost:40000/OData/Titles(1)/Royalties(2)
Containment
If we set our Accept header back to full and issue a GET request with http://localhost:40000/OData/Titles(1)/Royalties(2) we get an error.
The error occurs because we did not create an entity set for Royalties. We actually do not want one because it does not make sense to navigate as a standalone entity. So we can update our model and add the [Contained] attribute.
public class Title
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Contained]
public List<Royalty> Royalties { get; set; }
}
This is known as Containment and it allows us to define entities that should not be accessed from the top-level. When we re-issue our query it now returns the full detail.
Looking at the metadata for our entity we can see the Containstarget=”true” attribute.
In our next series of blogs we will start looking at query options.