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
@externaland@requiresfederation-specific directives - Resolve fields in the
soundtrackssubgraph 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.csfile.We'll tag the
RecommendedPlaylistsresolver with the[Requires]directive. This directive needs one argument: the field(s) that it depends on. In our case, that's thenamefield.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
Nameresolver. This will be a simplegetterproperty returning a nullablestringtype, to match what therecipessubgraph returns.Note the uppercase
Nhere. We're following C# conventions. Behind the scenes, Hot Chocolate transforms this into a lowercasento match GraphQL schema conventions.Types/Recipe.cspublic string? Name { get; }We'll mark the
Nameresolver 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
RecommendedPlaylistsresolver function parameters, let's add ourSpotifyServicedata source.Types/Recipe.cspublic List<Playlist> RecommendedPlaylists(SpotifyService spotifyService)Don't forget to import the
SpotifyServicepackage at the top of the file.Types/Recipe.csusing SpotifyWeb;We'll also update the function signature to be asynchronous and return a
Tasktype.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.csfile.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
thisif you'd like!).The second parameter is a list of
SearchTypeenums: we're only looking forPlaylisttypes. Then, how many playlists maximum should be returned (limit) and theoffsetif we're working with pagination (0 is the first page). Lastly,nullfor theinclude_externaltype, which we don't really need to worry about.Remember, the
RecommendedPlaylistsresolver function should return aList<Playlist>type. But theresponsefrom theSearchAsyncmethod returns aSearchResultstype, 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.Playlistsproperty, which in turn includes anItemsproperty where the playlists actually live.Types/Recipe.csvar items = response.Playlists.Items;itemsis now a collection ofPlaylistSimplifiedtypes, and we need to convert them toPlaylisttypes. Luckily, we have aPlaylist(PlaylistSimplified obj) constructor that does the trick! So we'll iterate over theitemscollection usingSelectand callnew Playlist(item)on each.Types/Recipe.csvar playlists = items.Select(item => new Playlist(item));Finally, the resolver function is expecting a
Listtype, 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
@requiresdirective 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
@externaldirective 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.