10. Using the @requires and @external directives
5m

Overview

In order to recommend better (and real) playlists for a specific recipe, the soundtracks needs more specific data from a recipe.

In this lesson, we will:

  • Learn about the @external and @requires federation-specific s
  • Resolve in the soundtracks using data from recipes

Searching for the perfect playlists

The soundtracks uses a Spotify REST API as a . We have a handy endpoint we can use to search for the perfect playlists to accompany a recipe: the /search endpoint.

https://spotify-demo-api-fe224840a08c.herokuapp.com/v1/docs/#/Playlists/search

Spotify REST API search endpoint documentation

The /search endpoint takes in a few parameters, most notably: a 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 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 to determine what we return from soundtracks? Currently, that information only lives in the recipes .

When logic in one depends on data from another, we can use the @requires and @external . Let's dive into these one by one, and see how we use them together.

Using the @requires and @external directives

The @requires is used on a in the schema to indicate that this particular field depends on the values of other that are resolved by other . This tells the that it needs to fetch the values of those externally-defined first, even if the original didn't request them. These externally-defined fields need to be included in the subgraph's schema and marked with the @external .

In our case, the Recipe 's recommendedPlaylists requires the name .

In Hot Chocolate, we'll be using annotations for these : [Requires] and [External].

Let's see these in action.

  1. Open up the Recipe.cs file.

  2. We'll tag the RecommendedPlaylists with the [Requires] . This directive needs one : the (s) that it depends on. In our case, that's the name .

    Note that this 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()
  3. Next, we'll need to add the Name . This will be a simple getter property returning a nullable string type, to match what the recipes returns.

    Note the uppercase N here. We're following C# conventions. Behind the scenes, Hot Chocolate transforms this into a lowercase n to match GraphQL schema conventions.

    Types/Recipe.cs
    public string? Name { get; }
  4. We'll mark the Name with the [External] attribute. This lets the know that another is responsible for providing the data for this .

    Types/Recipe.cs
    [External]
    public string? Name { get; }

The change in query plan

Restart the server and let rover dev do its magic before heading back to Sandbox at http://localhost:4000. We can see the effects of using the two in our .

Let's run that again:

query GetRecipeWithPlaylists {
randomRecipe {
name
recommendedPlaylists {
id
name
}
}
}

Then, let's look at the as text.

Query plan
QueryPlan {
Sequence {
Fetch(service: "recipes") {
{
randomRecipe {
__typename
id
name
}
}
},
Flatten(path: "recipe") {
Fetch(service: "soundtracks") {
{
... on Recipe {
__typename
id
name
}
} =>
{
... on Recipe {
recommendedPlaylists {
id
name
}
}
}
},
},
},
}

Compared with the previous we had(before we introduced @requires and @external), there's a small difference here: the representation (the lines highlighted) now includes the name . Interesting!

Let's make one small tweak to the and comment out the recipe's name.

query GetRecipeWithPlaylists {
randomRecipe {
# name
recommendedPlaylists {
id
name
}
}
}

Run the and take a look at the again... the query plan remains the same!

This is because the process of resolving recommendedPlaylists requires that the value of the name be passed along to, even if the original didn't ask for it. For this reason, the always fetches a recipe's name data first, and ensures it's passed along to the soundtracks .

Accessing the required field

Now let's do something with that name!

Jumping back to our Recipe.cs file, let's find the reference .

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

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

Types/Recipe.cs
public Recipe(string id, string? name)
{
Id = id;
if (name != null)
{
Name = name;
}
}

Calling the /search endpoint

  1. Back to our RecommendedPlaylists function parameters, let's add our SpotifyService .

    Types/Recipe.cs
    public List<Playlist> RecommendedPlaylists(
    SpotifyService spotifyService
    )
  2. Don't forget to import the SpotifyService package at the top of the file.

    Types/Recipe.cs
    using SpotifyWeb;
  3. We'll also update the function signature to be asynchronous and return a Task type.

    Types/Recipe.cs
    public async Task<List<Playlist>> RecommendedPlaylists(
    SpotifyService spotifyService
    )
  4. 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.cs
    var 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 for Playlist types. Then, how many playlists maximum should be returned (limit) and the offset if we're working with pagination (0 is the first page). Lastly, null for the include_external type, which we don't really need to worry about.

  5. Remember, the RecommendedPlaylists function should return a List<Playlist> type. But the response from the SearchAsync method returns a SearchResults 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 an Items property where the playlists actually live.

    Types/Recipe.cs
    var items = response.Playlists.Items;
  6. items is now a collection of PlaylistSimplified types, and we need to convert them to Playlist types. Luckily, we have a Playlist(PlaylistSimplified obj) constructor that does the trick! So we'll iterate over the items collection using Select and call new Playlist(item) on each.

    Types/Recipe.cs
    var playlists = items.Select(item => new Playlist(item));
  7. Finally, the function is expecting a List type, so we'll call the ToList() method before returning the results.

    Types/Recipe.cs
    return playlists.ToList();
  8. Perfect! And feel free to bring those three lines into one clean line.

    Types/Recipe.cs
    return response.Playlists.Items.Select(item => new Playlist(item)).ToList();
  9. 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 with all our changes.

In Sandbox, let's give that dream a spin, in full glorious detail!

query GetRecipeWithPlaylists {
randomRecipe {
name
description
ingredients {
text
}
instructions
recommendedPlaylists {
id
name
description
tracks {
explicit
id
name
uri
durationMs
}
}
}
}
Task!

👏👏👏 Recipe details, instructions, ingredients… and the perfect playlists to cook along to. Woohoo!

Key takeaways

  • The @requires is used to indicate that a in the schema depends on the values of other that are resolved by other . This ensures that externally-defined are fetched first, even if not explicitly requested in the original . In Hot Chocolate, this directive is represented by the [Requires] attribute.
  • The @external is used to mark a as externally defined, indicating that the data for this field comes from another . 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 . Emphasis on the word local; to get our changes actually "live" (at least in the tutorial sense of the word), we need to tell about them!

In the next lesson, we'll take a look at how we can land these changes safely and confidently using and .

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.