DEV Community

loading...

Relationships with JsonApiDotNetCore

Petar Radošević
Dad of five, software engineer with pen and paper. Architecture and APIs at Degreed.
・4 min read

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        { }
    }
}
Enter fullscreen mode Exit fullscreen mode

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": []
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)