Odyssey

Intro to GraphQL with .NET (C#) & Hot Chocolate
beta

Overview and setupWhat is GraphQL?Hot ChocolateHello worldApollo Sandbox ExplorerBuilding a schemaThe Query entry pointREST as a data sourceResolvers & data sourcesGraphQL argumentsA playlist's tracksResolver chainsMutation responseMutation input
11. A playlist's tracks
5m

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

Mockup design of a playlist's tracks

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:

Mockup design of a playlist's tracks

  • 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 a double 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 this uri.
  • 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:

Types/Track.cs
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.

Playlist.cs
[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.

Didn't quite follow the trail of text above? Here are some screenshots of the code in SpotifyService.cs to help you out. Pay attention to the breadcrumbs of class and method paths at the top of each file.

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. Remember, you can Cmd/Ctrl+Click on the type to jump to its definition.

Code - GetPlaylistAsync function

This SpotifyWeb.Playlist class has a property Tracks of type PaginatedOfPlaylistTrack.

Code - Playlist class

PaginatedOfPlaylistTrack then has a number of other properties related to pagination, and a property called Items, which is a collection of PlaylistTrack types.

Code - PaginatedOfPlaylistTrack class

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.

Code - PlaylistTrack class

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.

Code - PlaylistTrackItem class

However, we've actually done the first level already. We've turned a SpotifyWeb.Playlist type into our own Playlist class using a constructor.

Playlist.cs
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.

Playlist.cs
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:

Track.cs
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.

Track.cs
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.

Playlist.cs
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.

Playlist.cs
Tracks = trackObjects.ToList();

Putting it all together, here's what the new expanded Playlist constructor looks like:

Playlist.cs
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:

Playlist.cs
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();
}
using SpotifyWeb;
namespace Odyssey.MusicMatcher;
[GraphQLDescription("A curated collection of tracks designed for a specific activity or mood.")]
public class Playlist
{
[GraphQLDescription("The ID for the playlist.")]
[ID]
public string Id { get; }
[GraphQLDescription("The name of the playlist.")]
public string Name { get; set; }
[GraphQLDescription("Describes the playlist, what to expect and entices the user to listen.")]
public string? Description { get; set; }
[GraphQLDescription("The playlist's tracks.")]
public List<Track> Tracks { get; set; }
public Playlist(string id, string name)
{
Id = id;
Name = name;
}
public Playlist(PlaylistSimplified obj)
{
Id = obj.Id;
Name = obj.Name;
Description = obj.Description;
}
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();
}
}
using SpotifyWeb;
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;
}
public Track(PlaylistTrackItem obj)
{
Id = obj.Id;
Name = obj.Name;
DurationMs = obj.Duration_ms;
Explicit = obj.Explicit;
Uri = obj.Uri;
}
}

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.

GraphQL operation
query GetPlaylistDetails($playlistId: ID!) {
playlist(id: $playlistId) {
id
name
description
tracks {
id
name
durationMs
explicit
uri
}
}
}

With the Variables section set to:

Variables
{
"playlistId": "6Fl8d6KF0O4V5kFdbzalfW"
}
https://studio.apollographql.com/sandbox/explorer

Explorer - get playlist's tracks

Wow, we've got so many tracks for this playlist!

Task!

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.

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

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.

Previous
Next

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.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              fields

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              GraphQL

              An open-source query language and specification for APIs that enables clients to request specific data, promoting efficiency and flexibility in data retrieval.

              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              GraphQL

              An open-source query language and specification for APIs that enables clients to request specific data, promoting efficiency and flexibility in data retrieval.

              attribute

              Key-value pairs that add contextual metadata to telemetry. The router supports a built-in set of standard attributes from OpenTelemetry semantic conventions, and custom selectors to extract data from the router request lifecycle.

              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              GraphQL schema

              A GraphQL schema defines the structure and types of data that can be queried or mutated, serving as a contract between the server and clients.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              GraphQL schema

              A GraphQL schema defines the structure and types of data that can be queried or mutated, serving as a contract between the server and clients.

              variable

              A placeholder for dynamic values in an operation allowing parameterization and reusability in requests. Variables can be used to fill arguments or passed to directives.

              query GetUser($userId: ID!) {
              user(id: $userId) {
              firstName
              }
              }

              In the query above, userId is a variable. The variable and its type are declared in the operation signature, signified by a $. The type of variable is a non-nullable ID. A variable's type must match the type of any argument it's used for.

              variable

              A placeholder for dynamic values in an operation allowing parameterization and reusability in requests. Variables can be used to fill arguments or passed to directives.

              query GetUser($userId: ID!) {
              user(id: $userId) {
              firstName
              }
              }

              In the query above, userId is a variable. The variable and its type are declared in the operation signature, signified by a $. The type of variable is a non-nullable ID. A variable's type must match the type of any argument it's used for.

              fields

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              query

              A request for specific data from a GraphQL server. Clients define the structure of the response, enabling precise and efficient data retrieval.

              query

              A request for specific data from a GraphQL server. Clients define the structure of the response, enabling precise and efficient data retrieval.

              fields

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              query

              A request for specific data from a GraphQL server. Clients define the structure of the response, enabling precise and efficient data retrieval.

              Resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }

              NEW COURSE ALERT

              Introducing Apollo Connectors

              Connectors are the new and easy way to get started with GraphQL, using existing REST APIs.

              Say goodbye to GraphQL servers and resolvers—now, everything happens in the schema!

              Take the course