12. Resolver chains
5m

Overview

We can for a playlist's tracks, but only through the playlist(id: ID) root , not through featuredPlaylists. What's going on?

In this lesson, we will:

  • Learn about chains
  • Learn about the parent of a

Examining the data source response

Let's examine the response from our GET /browse/featured-playlists endpoint. It looks like we do have access to a tracks property under playlist.items.tracks.

"tracks": {
"href": "string",
"total": 0
}

Note: Alternatively, you can find the same information by following the trail of types and properties in the SpotifyService class.

Instead of a list of track objects (similar to the list that the earlier GET /playlists/{playlist_id} endpoint returns), we get a single object with two properties: total, the total number of tracks available, and href, a URL for the endpoint where we can retrieve the full list of track objects.

This is a common pattern in REST APIs. Imagine if the response did include the full list of track objects. That would make for a very large response, to have a list of playlists and a list of tracks for each playlist.

Instead, we'll need to make one more additional call to the REST API. In this case, the GET /playlists/{playlist_id}/tracks endpoint.

The next question becomes: where in our code will we make that call?

Examining the query

Let's take a step back and look at the we want to implement:

query GetFeaturedPlaylists {
featuredPlaylists {
id
name
description
tracks {
id
name
explicit
uri
}
}
}

From this , we're resolving the featuredPlaylists using the Query.FeaturedPlaylists function:

Query.cs
public async Task<List<Playlist>> FeaturedPlaylists(SpotifyService spotifyService)
{
var response = await spotifyService.GetFeaturedPlaylistsAsync();
return response.Playlists.Items.Select(item => new Playlist(item)).ToList();
}

When we convert the response into Playlist objects, we're using the Playlist constructor that takes in a PlaylistSimplified type.

Playlist.cs
public Playlist(PlaylistSimplified obj)
{
Id = obj.Id;
Name = obj.Name;
Description = obj.Description;
}

In this case, we're not doing anything to initialize the playlist's tracks (compared to the constructor that takes in a SpotifyWeb.Playlist object), which is why we're not getting any data in return!

Remember, we need to make an extra call to the GET /playlists/{playlist_id}/tracks endpoint to get the list of tracks. And we're back to our original question: where in our code will we make that call?

We could add it in this constructor. This is where we left off in our function statements.

Playlist.cs
public Playlist(PlaylistSimplified obj)
{
Id = obj.Id;
Name = obj.Name;
Description = obj.Description;
// should we make a call to spotifyService.GetPlaylistsTracksAsync() here?
}

But that would mean that whenever we for featuredPlaylists, we would always make an additional network call to the REST API, even when the didn't ask for tracks!

So instead, we're going to make use of the chain.

Following the resolver chain

A resolver chain is the order in which functions are called when resolving a particular . It can contain a sequential path as well as parallel branches.

Let's take an example from our project. This GetPlaylist retrieves the name of a playlist.

query GetPlaylist($playlistId: ID!) {
playlist(id: $playlistId) {
name
}
}

When resolving this , the will first call the Query.Playlist() function, then the Playlist.Name() function, which returns a string type and ends the chain.

Resolver chain in a diagram

Each in this chain passes their return value to the next function down, using the resolver's parent .

Remember, a has access to a number of parameters. So far, we've used (the SpotifyService) and (like the playlist id ). parent is another such parameter!

In this example, the Playlist.Name() function would have access to the Playlist object that Query.Playlist() returned.

Let's look at another .

query GetPlaylistTracks($playlistId: ID!) {
playlist(id: $playlistId) {
name
tracks {
uri
}
}
}

This time, we've added more and asked for each playlist's list of tracks, specifically their uri values.

Our chain grows, adding a parallel branch.

Resolver chain in a diagram

Note that since Playlist.Tracks returns a list of potentially multiple tracks, this might run more than once to retrieve each track's URI.

Following the trail of the , Playlist.Tracks() would have access to Playlist as the parent, Track.Uri() would have access to the Track object as the parent.

If our didn't include the tracks (like the first example we showed), then the Playlist.Tracks() function would never be called!

Refactoring Playlist.Tracks

Now that we know what a chain is, we can use it to determine the best place to insert the additional REST API call for a playlist's tracks.

Remember, we were debating including it in the constructor section, where it would be called every single time, even when the doesn't include it:

Playlist.cs
public Playlist(PlaylistSimplified obj)
{
Id = obj.Id;
Name = obj.Name;
Description = obj.Description;
// should we make a call to spotifyService.GetPlaylistsTracksAsync() here?
// probably not!
}

Instead, we'll refactor the Playlist.Tracks property into a function with a body. Right now, it's a simplistic get; property .

First, let's add a private for tracks, prefixing it with an underscore (_) to follow common convention. We'll make this nullable.

Playlist.cs
private List<Track>? _tracks;

Then, we'll update the Playlist(SpotifyWeb.Playlist obj) constructor to set the value for this private instead.

Playlist.cs
- Tracks = obj.Tracks.Items.Select(item => new Track(item.Track)).ToList();
+ _tracks = obj.Tracks.Items.Select(item => new Track(item.Track)).ToList();

Next, we'll transform the Tracks property to a function with a body instead of get; set; methods.

Playlist.cs
- public List<Track> Tracks { get; set; }
+ public List<Track> Tracks()
+ {
+
+ }

For now, we'll return what's in the private _tracks .

Playlist.cs
return _tracks;

Our GetPlaylistDetails should still be working with these changes. Take a moment to save our changes and confirm!

Now we have a perfect place to make our additional HTTP call.

Instead of returning the _tracks private immediately, we'll check to see if it exists. If it does, return it, but if it doesn't, we'll make the HTTP call.

Playlist.cs
if (_tracks != null) {
return _tracks;
} else {
// TODO: HTTP call
}

Since this is a regular function, we'll have access to the SpotifyService class in the function parameters. We'll also update the function to be asynchronous and return Task<List<Track>>.

Playlist.cs
public async Task<List<Track>> Tracks(SpotifyService spotifyService)

Next, inside the else block, let's call the service's GetPlaylistsTracksAsync method and await the results.

var response = await spotifyService.GetPlaylistsTracksAsync();

The GetPlaylistsTracksAsync method needs one : the ID of the playlist. How do we get that value?

Well, this method belongs to the Playlist class, so we have access to this.Id.

Playlist.cs
var response = await spotifyService.GetPlaylistsTracksAsync(this.Id);

Another way to access the playlist's ID is through the parent parameter in the function, using the [Parent] attribute. This attribute uses dependency injection to inject the value of the parent into the .

Playlist.cs
public async Task<List<Track>> Tracks(
SpotifyService spotifyService,
[Parent] Playlist parent
)

In the body of the , we would then replace this.Id with parent.Id.

Playlist.cs
// var response = await spotifyService.GetPlaylistsTracksAsync(this.Id); // same as below
var response = await spotifyService.GetPlaylistsTracksAsync(parent.Id);

Both approaches are valid! We'll stick with the first one, using this.Id.

Let's finish up our function. After calling GetPlaylistsTracksAsync, we'll dig into the response's Items property, map through the collection and create a Track object from each item. This should look familiar, we already did the same thing in the Playlist (SpotifyWeb.Playlist obj) constructor! Don't forget to use ToList() at the end to match the type this is expecting to return.

Playlist.cs
return response.Items.Select(item => new Track(item.Track)).ToList();

Explorer time: round 2!

Server running with the latest changes? Great! Now when we jump back over to Sandbox and run the for featuredPlaylists and its list of tracks, we get what we asked for!

query GetFeaturedPlaylists {
featuredPlaylists {
id
name
description
tracks {
id
name
explicit
uri
}
}
}

👏👏👏

Comparing with the REST approach

Time to put on our product app developer hat again! Let's compare what this feature would have looked like if we had used REST instead of .

If we had used REST, the app logic would have included:

  • Making the HTTP GET call to the /browse/featured-playlists endpoint
  • Making an extra HTTP GET call for each playlist in the response to GET /playlists/{playlist_id}/tracks. Waiting for all of those to resolve, depending on the number of playlists, could take a while. Plus, this introduces the common N+1 problem.
  • Retrieving just the id, name and explicit and uri properties, discarding all the rest of the response. There's so much more to the response that wasn't used! Again, if the client app had slow network speeds or not much data, that big response comes with a cost.

With , we have our short and sweet, clean, readable coming from the client, coming back in exactly the shape they specified, no more, no less!

All the logic of extracting the data, making extra HTTP calls, and filtering for which are needed are all done on the side. We still have the N+1 problem, but it's on the server-side (where response and request speeds are more consistent and generally faster) instead of the client-side (where network speeds are and inconsistent).

Note: We can address the N+1 problem on the side using DataLoaders. Check out the Hot Chocolate documentation for how to implement them.

Key takeaways

  • A chain is the order in which resolver functions are called when resolving a particular . It can contain a sequential path as well as parallel branches.
  • Each in this chain passes their return value to the next function down, using the resolver's parent .

Up next

Feeling confident with queries? It's time to explore the other side of : .

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.