Overview
In order to recommend better (and real) playlists for a specific recipe, the soundtracks
subgraph needs more specific data from a recipe.
In this lesson, we will:
- Learn about the
@external
and@requires
federation-specific directives - Resolve fields in the
soundtracks
subgraph using data fromrecipes
Searching for the perfect playlists
The soundtracks
subgraph uses a Spotify REST API as a data source. We have a handy endpoint we can use to search for the perfect playlists to accompany a recipe: the /search
endpoint.
The /search
endpoint takes in a few parameters, most notably: a query string to search for and the type of results to return (albums, artists, songs, playlists). We already know we want playlists, and we'll leave the rest of the parameters as default. But what about the query string? What search terms or keywords about the recipe can we provide?
The soundtracks
subgraph already has access to a recipe's unique id
, but we can't make helpful recommendations based on an id
!
A recipe's name
, however, might be a bit more useful. How can we use that bit of information from the recipes
subgraph to determine what we return from soundtracks
? Currently, that information only lives in the recipes
subgraph.
When logic in one subgraph depends on data from another, we can use the @requires
and @external
directives. Let's dive into these one by one, and see how we use them together.
Using the @requires
and @external
directives
The @requires
directive is used on a field in the schema to indicate that this particular field depends on the values of other fields that are resolved by other subgraphs. This directive tells the router that it needs to fetch the values of those externally-defined fields first, even if the original GraphQL operation didn't request them. These externally-defined fields need to be included in the subgraph's schema and marked with the @external
directive.
In our case, the Recipe
entity's recommendedPlaylists
field requires the name
field.
In Hot Chocolate, we'll be using annotations for these directives: [Requires]
and [External]
.
Let's see these directives in action.
Open up the
Recipe.cs
file.We'll tag the
RecommendedPlaylists
resolver with the[Requires]
directive. This directive needs one argument: the field(s) that it depends on. In our case, that's thename
field.Note that this field needs to start with a lowercase n to match what it looks like in our schema.
Types/Recipe.cs[Requires("name")]public List<Playlist> RecommendedPlaylists()Next, we'll need to add the
Name
resolver. This will be a simplegetter
property returning a nullablestring
type, to match what therecipes
subgraph returns.Note the uppercase
N
here. We're following C# conventions. Behind the scenes, Hot Chocolate transforms this into a lowercasen
to match GraphQL schema conventions.Types/Recipe.cspublic string? Name { get; }We'll mark the
Name
resolver with the[External]
attribute. This lets the router know that another subgraph is responsible for providing the data for this field.Types/Recipe.cs[External]public string? Name { get; }
The change in query plan
Restart the server and let rover dev
do its composition magic before heading back to Sandbox at http://localhost:4000. We can see the effects of using the two directives in our query plan.
Let's run that query again:
query GetRecipeWithPlaylists {randomRecipe {namerecommendedPlaylists {idname}}}
Then, let's look at the query plan as text.
QueryPlan {Sequence {Fetch(service: "recipes") {{randomRecipe {__typenameidname}}},Flatten(path: "recipe") {Fetch(service: "soundtracks") {{... on Recipe {__typenameidname}} =>{... on Recipe {recommendedPlaylists {idname}}}},},},}
Compared with the previous query plan we had(before we introduced @requires
and @external)
, there's a small difference here: the entity representation (the lines highlighted) now includes the name
field. Interesting!
Let's make one small tweak to the query and comment out the recipe's name.
query GetRecipeWithPlaylists {randomRecipe {# namerecommendedPlaylists {idname}}}
Run the query and take a look at the query plan again... the query plan remains the same!
This is because the process of resolving recommendedPlaylists
requires that the value of the name
field be passed along to, even if the original GraphQL operation didn't ask for it. For this reason, the router always fetches a recipe's name
data first, and ensures it's passed along to the soundtracks
subgraph.
Accessing the required field
Now let's do something with that name
!
Jumping back to our Recipe.cs
file, let's find the reference resolver.
[ReferenceResolver]public static Recipe GetRecipeById(string id){return new Recipe(id);}
We'll update the list of parameters to include the name
, and use that in the constructor call.
public static Recipe GetRecipeById(string id,string? name) {return new Recipe(id, name);}
And our constructor needs to account for that name
value as well!
public Recipe(string id, string? name){Id = id;if (name != null){Name = name;}}
Calling the /search
endpoint
Back to our
RecommendedPlaylists
resolver function parameters, let's add ourSpotifyService
data source.Types/Recipe.cspublic List<Playlist> RecommendedPlaylists(SpotifyService spotifyService)Don't forget to import the
SpotifyService
package at the top of the file.Types/Recipe.csusing SpotifyWeb;We'll also update the function signature to be asynchronous and return a
Task
type.Types/Recipe.cspublic async Task<List<Playlist>> RecommendedPlaylists(SpotifyService spotifyService)Inside the body of the function, we'll use the
spotifyService.SearchAsync()
method.Hint: Hover over the method to see its signature, and click through to find the function definition in the
SpotifyService.cs
file.Types/Recipe.csvar response = await spotifyService.SearchAsync(this.Name,new List<SearchType> { SearchType.Playlist },3,0,null);The first parameter is the search term, which is the recipe's name. We have access to it now through the class (you can omit
this
if you'd like!).The second parameter is a list of
SearchType
enums: we're only looking forPlaylist
types. Then, how many playlists maximum should be returned (limit
) and theoffset
if we're working with pagination (0 is the first page). Lastly,null
for theinclude_external
type, which we don't really need to worry about.Remember, the
RecommendedPlaylists
resolver function should return aList<Playlist>
type. But theresponse
from theSearchAsync
method returns aSearchResults
type, which doesn't match! We'll need to dig in a little deeper to get the return type we want.It looks like the playlist results are included in the
response.Playlists
property, which in turn includes anItems
property where the playlists actually live.Types/Recipe.csvar items = response.Playlists.Items;items
is now a collection ofPlaylistSimplified
types, and we need to convert them toPlaylist
types. Luckily, we have aPlaylist(PlaylistSimplified obj
) constructor that does the trick! So we'll iterate over theitems
collection usingSelect
and callnew Playlist(item)
on each.Types/Recipe.csvar playlists = items.Select(item => new Playlist(item));Finally, the resolver function is expecting a
List
type, so we'll call theToList()
method before returning the results.Types/Recipe.csreturn playlists.ToList();Perfect! And feel free to bring those three lines into one clean line.
Types/Recipe.csreturn response.Playlists.Items.Select(item => new Playlist(item)).ToList();We'll also remove the hard-coded return array.
Types/Recipe.cs- return new List<Playlist>- {- new Playlist("1", "Grooving"),- new Playlist("2", "Graph Explorer Jams"),- new Playlist("3", "Interpretive GraphQL Dance")- };
Querying the dream query
We're all set! Restart the server and give rover dev
a moment to compose the new supergraph schema with all our changes.
In Sandbox, let's give that dream query a spin, in full glorious detail!
query GetRecipeWithPlaylists {randomRecipe {namedescriptioningredients {text}instructionsrecommendedPlaylists {idnamedescriptiontracks {explicitidnameuridurationMs}}}}
👏👏👏 Recipe details, instructions, ingredients… and the perfect playlists to cook along to. Woohoo!
Key takeaways
- The
@requires
directive is used to indicate that a field in the schema depends on the values of other fields that are resolved by other subgraphs. This directive ensures that externally-defined fields are fetched first, even if not explicitly requested in the original GraphQL operation. In Hot Chocolate, this directive is represented by the[Requires]
attribute. - The
@external
directive is used to mark a field as externally defined, indicating that the data for this field comes from another subgraph. In Hot Chocolate, this directive is represented by the[External]
attribute.
Up next
We've made lots of changes to our server, and rover dev
helped us test everything out in a locally composed supergraph. Emphasis on the word local; to get our changes actually "live" (at least in the tutorial sense of the word), we need to tell GraphOS about them!
In the next lesson, we'll take a look at how we can land these changes safely and confidently using schema checks and launches.
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.