In our previous post we setup a basic JSON:API compliant API with the 4.0 release of the JsonApiDotNetCore framework. You can find the code we wrote under the part-1
branch on Github.
In this post I would like to introduce some of the more powerful capabilities of the framework and to do so, let's talk about relationships.
Relationships
We are going to use the canonical todo example and create the ability to assign a todo to a single person. Lets start by creating a new TodoItem.cs
file and add the following to it:
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
namespace MyApi
{
public class TodoItem : Identifiable
{
[Attr]
public string Todo { get; set; }
[Attr]
public uint Priority { get; set; }
[HasOne]
public Person Owner { get; set; }
}
}
In the code above, we state that a TodoItem
has a single Person
as its owner. Let's also add the reverse relationship, where we state that a Person
can have many todo's. Open up your Person.cs
and add the following:
using System.Collections.Generic;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
namespace MyApi
{
public class Person : Identifiable
{
[Attr]
public string Name { get; set; }
[HasMany]
public ICollection<TodoItem> TodoItems { get; set; }
}
}
We're also going to add our new TodoItem
to our DbContext
to release the power of Entity Framework Core:
using Microsoft.EntityFrameworkCore;
namespace MyApi
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<Person> People { get; set; }
public DbSet<TodoItem> TodoItems { get; set; }
}
}
To expose the new endpoint, let's create a controller, TodoItemsController.cs
with the following boilerplate:
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Services;
using Microsoft.Extensions.Logging;
namespace MyApi.Controllers
{
public class TodoItemsController : JsonApiController<TodoItem>
{
public TodoItemsController(
IJsonApiOptions options,
ILoggerFactory loggerFactory,
IResourceService<TodoItem> resourceService)
: base(options, loggerFactory, resourceService)
{ }
}
}
Before we run our API, make sure to delete the my-api.db
file in our project folder for it to pick up the new schema.
We can now run our API and go to https://localhost:5001/todoItems and we will see the following:
{
"links": {
"self": "https://localhost:5001/todoItems",
"first": "https://localhost:5001/todoItems"
},
"data": []
}
It works, but it's a bit boring. Let's add some more data in our Startup.cs
file:
if (!context.People.Any())
{
context.People.Add(new Person
{
Name = "John Doe",
TodoItems = new List<TodoItem>
{
new TodoItem { Todo = "Make pizza", Priority = 1},
new TodoItem { Todo = "Clean room", Priority = 2}
}
});
context.SaveChanges();
}
Another query will show us the following, if we go to /people
we will see:
{
"links": {
"self": "https://localhost:5001/people",
"first": "https://localhost:5001/people"
},
"data": [
{
"type": "people",
"id": "1",
"attributes": {
"name": "John Doe"
},
"relationships": {
"todoItems": {
"links": {
"self": "https://localhost:5001/people/1/relationships/todoItems",
"related": "https://localhost:5001/people/1/todoItems"
}
}
},
"links": {
"self": "https://localhost:5001/people/1"
}
}
]
}
And if we query /todoItems
we see:
{
"links": {
"self": "https://localhost:5001/todoItems",
"first": "https://localhost:5001/todoItems"
},
"data": [
{
"type": "todoItems",
"id": "1",
"attributes": {
"todo": "Make pizza",
"priority": 1
},
"relationships": {
"owner": {
"links": {
"self": "https://localhost:5001/todoItems/1/relationships/owner",
"related": "https://localhost:5001/todoItems/1/owner"
}
}
},
"links": {
"self": "https://localhost:5001/todoItems/1"
}
},
{
"type": "todoItems",
"id": "2",
"attributes": {
"todo": "Clean room",
"priority": 2
},
"relationships": {
"owner": {
"links": {
"self": "https://localhost:5001/todoItems/2/relationships/owner",
"related": "https://localhost:5001/todoItems/2/owner"
}
}
},
"links": {
"self": "https://localhost:5001/todoItems/2"
}
}
]
}
Ok, if you thought this was cool, wait till you see the querying and filtering capabilities.
Including Relationships
To get all todo-items for a single person, the JSON:API spec defines a relationships endpoint, which would be /people/1/relationships/todoItems
:
{
"links": {
"self": "https://localhost:5001/people/1/relationships/todoItems",
"related": "https://localhost:5001/people/1/todoItems",
"first": "https://localhost:5001/people/1/relationships/todoItems"
},
"data": [
{
"type": "todoItems",
"id": "1"
}
]
}
This shows you all the todo-items IDs for that specific person. To get all individual todo's would cost many queries.
Amazingly, JSON:API also enables you to get all that data in a single call, by doing:
GET /people/1?include=todoItems
{
"links": {
"self": "https://localhost:5001/people/1?include=todoItems"
},
"data": {
"type": "people",
"id": "1",
"attributes": {
"name": "John Doe"
},
"relationships": {
"todoItems": {
"links": {
"self": "https://localhost:5001/people/1/relationships/todoItems",
"related": "https://localhost:5001/people/1/todoItems"
},
"data": [
{
"type": "todoItems",
"id": "1"
},
{
"type": "todoItems",
"id": "2"
}
]
}
},
"links": {
"self": "https://localhost:5001/people/1"
}
},
"included": [
{
"type": "todoItems",
"id": "1",
"attributes": {
"todo": "Make pizza",
"priority": 1
},
"relationships": {
"owner": {
"links": {
"self": "https://localhost:5001/todoItems/1/relationships/owner",
"related": "https://localhost:5001/todoItems/1/owner"
}
}
},
"links": {
"self": "https://localhost:5001/todoItems/1"
}
},
{
"type": "todoItems",
"id": "2",
"attributes": {
"todo": "Clean room",
"priority": 2
},
"relationships": {
"owner": {
"links": {
"self": "https://localhost:5001/todoItems/2/relationships/owner",
"related": "https://localhost:5001/todoItems/2/owner"
}
}
},
"links": {
"self": "https://localhost:5001/todoItems/2"
}
}
]
}
OK, that was easy. But what if that's just too big of a payload and you do not want to waste any bandwidth? That's where "sparse fieldsets" come in.
You just want to show a list of the todo's? Let's reduce the payload:
GET /people/1?include=todoItems&fields[todoItems]=todo
{
...
"included": [
{
"type": "todoItems",
"id": "1",
"attributes": {
"todo": "Make pizza"
},
"links": {
"self": "https://localhost:5001/todoItems/1"
}
},
{
"type": "todoItems",
"id": "2",
"attributes": {
"todo": "Clean room"
},
"links": {
"self": "https://localhost:5001/todoItems/2"
}
}
]
}
Pretty powerful stuff! But there is even more. In the next post, I want to show you some of the powerful filter capabilities that are also added in the 4.0 release.
Ow, and BTW, you can find all the code above in the part-2
branch on Github.
Top comments (0)