Overview
We're still missing something: a playlist's tracks — the songs we actually want to listen to!
In this lesson, we will:
- Create the
Track
type - Write the resolver function for a playlist's tracks
The Track
object
Take a few moments to study the mockup above and start thinking of what pieces of data we'll need, and what types they might be. We'll ignore the artist information for now to keep things simpler.
You might also want to check out the GET /playlists/{playlist_id}/tracks
endpoint for a better idea of what the API returns and what they've named certain properties.
Take your time!
When you're ready, compare it with what we came up with:
- The track's name is a
string.
- The "E" label denotes if the track is explicit. We can make that a
bool
type and let the client use the logic to display an "E" label or not. - The duration of the track. The mockup shows the formatting to be in minutes and seconds with a colon in between, so maybe we might need to make this a
string
type, or maybe the client team wants to control their own formatting and we should return it as adouble
type with the duration in milliseconds. The REST endpoint returns the latter, so let's stick with that for now. - Though it's not shown on the mockup, it's helpful to have an identifier for an object, so we'll make sure to return the track's ID as well.
- There's an option to copy a link to the track to share with others so they can open it on Spotify as well. So we'll probably need to return that link as a
string
. In the REST API, they've named thisuri
. - We'll make all of these fields non-nullable since the REST API does the same.
Your design might have looked a little different from ours, but with these pieces in mind, let's go ahead and create our Track
class! Remember, we're keeping all our GraphQL types organized together under the Types
folder.
You should have everything you need to know to try writing this one out yourself! If you need a reference, feel free to use the one below:
namespace Odyssey.MusicMatcher;[GraphQLDescription("A single audio file, usually a song.")]public class Track{[ID][GraphQLDescription("The ID for the track.")]public string Id { get; }[GraphQLDescription("The name of the track.")]public string Name { get; set; }[GraphQLDescription("The track length in milliseconds.")]public double DurationMs { get; set; }[GraphQLDescription("Whether or not the track has explicit lyrics (true = yes it does; false = no it does not OR unknown)")]public bool Explicit { get; set; }[GraphQLDescription("The URI for the track, usually a Spotify link.")]public string Uri { get; set; }public Track(string id, string name, string uri){Id = id;Name = name;Uri = uri;}}
Tip: When choosing field names in your schema, try to be descriptive but concise. For example, we could have chosen duration
as one of the field names. However, durationMs
is a bit more descriptive on the format of the field returning in milliseconds. We could have also chosen durationInMilliseconds
for further clarity. If we wanted to also support returning a formatted string, we could add a new field called durationString
. Using GraphQL descriptions also helps with clarity. Learn more about schema naming conventions in the Apollo documentation.
Don't forget to add descriptions to the schema using the [GraphQLDescription]
attribute.
Connecting tracks to a playlist
With our Track
class all set up, we can now add the long awaited tracks
field to Playlist
.
[GraphQLDescription("The playlist's tracks.")]public List<Track> Tracks { get; set; }
Let's think about this particular resolver function. So far, the Playlist
class contains simple property resolvers (for Id
, Name
and Description
). Remember, behind the scenes, Hot Chocolate converts properties with get
accessors to resolvers.
Can we do the same for our Tracks
resolver? Well, let's examine where our data is coming from. For this next section, we highly recommend using your code editor's features to Cmd/Ctrl + click on a particular type to navigate to its type navigation!
The details for a particular playlist are coming from the Query.Playlist
resolver function, which uses the SpotifyService
data source method GetPlaylistAsync
.
If we follow the path in our SpotifyService.cs
file, we can see that the response returns a SpotifyWeb.Playlist
type. This type has a property Tracks
of type PaginatedOfPlaylistTrack
. PaginatedOfPlaylistTrack
then has a number of other properties related to pagination, and a property called Items
, which is a collection of PlaylistTrack
types.
Following that type, we get more properties related to the playlist track's metadata, like who added it and when, as well as a property called Track
, which is of type PlaylistTrackItem
. Finally, this looks like the Track
information we're looking for! Among other properties, it has Id
, Name
, Explicit
, Duration_ms
properties, which match exactly what our Track
class in our GraphQL schema has.
Whew! All that to say, we have to dig a couple levels deeper to return the values we need.
However, we've actually done the first level already. We've turned a SpotifyWeb.Playlist
type into our own Playlist
class using a constructor.
public Playlist(SpotifyWeb.Playlist obj){Id = obj.Id;Name = obj.Name;Description = obj.Description;}
So we can expand on this constructor and initialize the Playlist.Tracks
property using the SpotifyWeb.Playlist
obj
variable.
Let's take this one step at a time! Make sure to lean on your code editor's IntelliSense to get a better sense of what properties are available at each level.
Inside the Playlist(SpotifyWeb.Playlist obj)
constructor, we'll first extract the collection of paginated tracks.
var paginatedTracks = obj.Tracks.Items;
This paginatedTracks
variable is currently a collection of SpotifyWeb.PlaylistTrackItem
objects. This is very close to what we need! We need our own Track
class. This should be a familiar pattern at this point; we've used the same pattern twice before.
We'll create a new, additional Track
constructor that takes a SpotifyWeb.PlaylistTrackItem
object and initializes its fields based on that object's properties. Jumping to the Track.cs
file:
public Track(PlaylistTrackItem obj){Id = obj.Id;Name = obj.Name;DurationMs = obj.Duration_ms;Explicit = obj.Explicit;Uri = obj.Uri;}
Don't forget to import the SpotifyWeb
namespace at the top, since PlaylistTrackItem
is coming from that package.
using SpotifyWeb;
We can now use this Track
constructor back in our Playlist
constructor. We'll map over those items and return a new collection, creating a Track
instance from item.Track
.
var trackObjects = paginatedTracks.Select(item => new Track(item.Track));
Finally, we'll convert to a List
type because that's what the Tracks
property is expecting.
Tracks = trackObjects.ToList();
Putting it all together, here's what the new expanded Playlist
constructor looks like:
public Playlist(SpotifyWeb.Playlist obj){Id = obj.Id;Name = obj.Name;Description = obj.Description;var paginatedTracks = obj.Tracks.Items;var trackObjects = paginatedTracks.Select(item => new Track(item.Track));Tracks = trackObjects.ToList();}
Want something a little shorter? We can simplify into a one-liner:
public Playlist(SpotifyWeb.Playlist obj){Id = obj.Id;Name = obj.Name;Description = obj.Description;Tracks = obj.Tracks.Items.Select(item => new Track(item.Track)).ToList();}
Explorer time!
That was a lot of code, but we should have enough to query for a playlist's tracks now! Make sure our server is running with the latest changes.
Back in Explorer, let's add to our original query with fields for the playlist's tracks.
query GetPlaylistDetails($playlistId: ID!) {playlist(id: $playlistId) {idnamedescriptiontracks {idnamedurationMsexplicituri}}}
With the Variables section set to:
{"playlistId": "6Fl8d6KF0O4V5kFdbzalfW"}
Wow, we've got so many tracks for this playlist!
An alternate path
Now what about the featuredPlaylists
path? It's another entry point to our schema that returns a list of Playlist
types, which then has access to its tracks
field. Let's try it out.
query GetFeaturedPlaylists {featuredPlaylists {idnamedescriptiontracks {idnameexplicituri}}}
When we run this query, we get an errors
array back with the message "Cannot return null for non-nullable field."
. Uh-oh!
Key takeaways
- Resolver functions may involve navigating through multiple levels of data, especially in scenarios with nested objects.
- We recommend choosing descriptive yet concise field names for the schema.
Up next
Let's investigate the source of that error and fix it.
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.