If you want to read more about Apollo Federation in general, you can head over to the Apollo Federation documentation, which provides a robust overview and set of examples for this GraphQL architectural pattern. Many of the core principles and concepts are referenced within this document.
Hot Chocolate includes an implementation of the Apollo Federation v1 specification for creating Apollo Federated subgraphs. Through Apollo Federation, you can combine multiple GraphQL APIs into a single API for your consumers.
The documentation describes the syntax for creating an Apollo Federated subgraph using Hot Chocolate and relates the implementation specifics to its counterpart in the Apollo Federation docs. This document will not provide a thorough explanation of the Apollo Federation core concepts nor will it describe how you go about creating a supergraph to stitch together various subgraphs, as the Apollo Federation team already provides thorough documentation of those principles.
You can find example projects of the Apollo Federation library in Hot Chocolate examples.
Get Started
To use the Apollo Federation tools, you need to first install v12.6 or later of the HotChocolate.ApolloFederation
package.
dotnet add package HotChocolate.ApolloFederation
HotChocolate.*
packages need to have the same version.After installing the necessary package, you'll need to register the Apollo Federation services with the GraphQL server.
builder.Services .AddGraphQLServer() .AddApolloFederation();
Defining an entity
Now that the API is ready to support Apollo Federation, we'll need to define an entity—an object type that can resolve its fields across multiple subgraphs. We'll work with a Product
entity to provide an example of how to do this.
public class Product{ [ID] public string Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }}
Define an entity key
Once we have an object type to work with, we'll define a key for the entity. A key in an Apollo Federated subgraph effectively serves as an "identifier" that can uniquely locate an individual record of that type. This will typically be something like a record's primary key, a SKU, or an account number.
In an implementation-first approach, we'll use the [Key]
attribute on any property or properties that can be referenced as a key by another subgraph.
public class Product{ [ID] [Key] public string Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }}
Define a reference resolver
Next, we'll need to define an entity reference resolver so that the supergraph can resolve this entity across multiple subgraphs during a query. Every subgraph that contributes at least one unique field to an entity must define a reference resolver for that entity.
In an implementation-first approach, a reference resolver will work just like a regular resolver with some key differences:
- It must be annotated with the
[ReferenceResolver]
attribute - It must be a
public static
method within the type it is resolving
public class Product{ [ID] [Key] public string Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }
[ReferenceResolver] public static async Task<Product?> ResolveReference( // Represents the value that would be in the Id property of a Product string id, // Example of a service that can resolve the Products ProductBatchDataLoader dataLoader ) { return await dataloader.LoadAsync(id); }}
Some important details to highlight about [ReferenceResolver]
methods.
- The name of the method decorated with the
[ReferenceResolver]
attribute does not matter. However, as with all programming endeavors, you should aim to provide a descriptive name that reveals the method's intention. - The parameter name and type used in the reference resolver must match the GraphQL field name of the
[Key]
attribute, e.g., if the GraphQL key field isid: String!
orid: ID!
then the reference resolver's parameter must bestring id
. - If you're using nullable reference types, you should make sure the return type is marked as possibly null, i.e.,
T?
. - If you have multiple keys defined for an entity, you should include a reference resolver for each key so that the supergraph is able to resolve your entity regardless of which key(s) another graph uses to reference that entity.
public class Product{ [Key] public string Id { get; set; }
[Key] public int Sku { get; set; }
[ReferenceResolver] public static Product? ResolveReferenceById(string id) { // Locates the Product by its Id. }
[ReferenceResolver] public static Product? ResolveReferenceBySku(int sku) { // Locates the product by SKU }}
A note about reference resolvers
It's recommended to use a dataloader to fetch the data in a reference resolver. This helps the API avoid an N+1 problem when a query resolves multiple items from a given subgraph.
Register the entity
After our type has a key or keys and a reference resolver defined, you'll register the type in the GraphQL schema, which will register it as a type within the GraphQL API itself as well as within the auto-generated _service { sdl }
field within the API.
Entity type registration
builder.Services .AddGraphQLServer() .AddApolloFederation() .AddType<Product>() // other registrations... ;
Testing and executing your reference resolvers
After creating an entity, you'll likely wonder "how do I invoke and test this reference resolver?" Entities that define a reference resolver can be queried through the auto-generated _entities
query at the subgraph level.
You'll invoke the query by providing an array of representations using a combination of a __typename
and key field values to invoke the appropriate resolver. An example query for our Product
would look something like the following.
Entities query
query { _entities( representations: [ { __typename: "Product", id: "<id value of the product>" } # You can provide multiple representations for multiple objects and types in the same query ] ) { ... on Product { id name price } }}
Entities query result
{ "data": { "_entities": [ { "id": "<id value of the product>", "name": "Foobar", "price": 10.99 } // Any other values that were found, or null ] }}
Note: The
_entities
field is an internal implementation detail of Apollo Federation that is necessary for the supergraph to properly resolve entities. API consumers should not use the_entities
field directly nor should they send requests to a subgraph directly. We're only highlighting how to use the_entities
field so that you can validate and test your subgraph and its entity reference resolvers at runtime or using tools likeMicrosoft.AspNetCore.Mvc.Testing
.
Referencing an entity type
Now that we have an entity defined in one of our subgraphs, let's go ahead and create a second subgraph that will make use of our Product
type. Remember, all of this work should be performed in a separate API project.
In the second subgraph, we'll create a Review
type that is focused on providing reviews of Product
entities from the other subgraph. We'll do that by defining our Review
type along with a service type reference that represents the Product
.
In our new subgraph API we'll need to start by creating the Product
. When creating the extended service type, make sure to consider the following details
- The GraphQL type name must match. Often, this can be accomplished by using the same class name between the projects, but you can also use tools like the
[GraphQLName(string)]
attribute orIObjectTypeDescriptor<T>.Name(string)
method to explicitly set a GraphQL name. - The extended type must include at least one key that matches in both name and GraphQL type from the source graph.
- In our example, we'll be referencing the
id: ID!
field that was defined on ourProduct
- In our example, we'll be referencing the
[ExtendServiceType]public class Product{ [ID] [Key] public string Id { get; set; }}
// In your Programbuilder.Services .AddGraphQLServer() .AddApolloFederation() .AddType<Product>();
Next, we'll create our Review
type that has a reference to the Product
entity. Similar to our first class, we'll need to denote the type's key(s) and the corresponding entity reference resolver(s).
public class Review{ [ID] [Key] public string Id { get; set; }
public string Content { get; set; }
[GraphQLIgnore] public string ProductId { get; set; }
public Product GetProduct() => new Product { Id = ProductId };
[ReferenceResolver] public static Review? ResolveReference(string id) { // Omitted for brevity; some kind of service to retrieve the review. }}
// In your Programbuilder.Services .AddGraphQLServer() .AddApolloFederation() .AddType<Product>() .AddType<Review>();
In the above snippet two things may pop out as strange to you:
- Why did we explicitly ignore the
ProductId
property?- The
ProductId
is, in essence, a "foreign key" to the other graph. Instead of presenting that data as a field of theReview
type, we're presenting it through theproduct: Product!
GraphQL field that is produced by theGetProduct()
method. This allows the Apollo supergraph to stitch theReview
andProduct
types together and represent that a query can traverse from theReview
to theProduct
it is reviewing and make the API more graph-like. With that said, it is not strictly necessary to ignore theProductId
or any other external entity Id property.
- The
- Why does the
GetProduct()
method instantiate its ownnew Product { Id = ProductId }
object?- Since our goal with Apollo Federation is decomposition and concern-based separation, a second subgraph is likely to have that "foreign key" reference to the type that is reference from the other subgraph. However, this graph does not "own" the actual data of the entity itself. This is why our sample simply performs a
new Product { Id = ProductId }
statement for the resolver: it's not opinionated about how the other data of aProduct
is resolved from its owning graph.
- Since our goal with Apollo Federation is decomposition and concern-based separation, a second subgraph is likely to have that "foreign key" reference to the type that is reference from the other subgraph. However, this graph does not "own" the actual data of the entity itself. This is why our sample simply performs a
With our above changes, we can successfully connect these two subgraphs into a single query within an Apollo supergraph, allowing our API users to send a query like the following.
query { # Example - not explicitly defined in our tutorial review(id: "<review id>") { id content product { id name } }}
As a reminder, you can create and configure a supergraph by following either the Apollo Router documentation or @apollo/gateway
documentation.
Contributing fields through resolvers
Now that our new subgraph has the Product
reference we can contribute additional fields to the type. Similar to other types in Hot Chocolate, you can create new fields by defining different method or property resolvers. For a full set of details and examples on creating resolvers, you can read our documentation on resolvers.
For now, we'll focus on giving our supergraph the ability to retrieve all reviews for a given product by adding a reviews: [Review!]!
property to the type.
[ExtendServiceType]public class Product{ [ID] [Key] public string Id { get; set; }
public async Task<IEnumerable<Review>> GetReviews( ReviewRepository repo // example of how you might resolve this data ) { return await repo.GetReviewsByProductIdAsync(Id); }}
These changes will successfully add the new field within the subgraph! However, our current implementation cannot be resolved if we start at a product such as query { product(id: "foo") { reviews { ... } } }
. To fix this, we'll need to implement an entity reference resolver in our second subgraph.
As mentioned above, since this subgraph does not "own" the data for a Product
, our resolver will be fairly naive, similar to the Review::GetProduct()
method: it will simply instantiate a new Product { Id = id }
. We do this because the reference resolver should only be directly invoked by the supergraph, so our new reference resolver will simply assume the data exists. However, if there is data that needs to be fetched from some kind of data store, the resolver can still do this just as any other data resolver in Hot Chocolate.
[ExtendServiceType]public class Product{ [ID] [Key] public string Id { get; set; }
public async Task<IEnumerable<Review>> GetReviews( ReviewRepository repo // example of how you might resolve this data ) { return await repo.GetReviewsByProductIdAsync(Id); }
[ReferenceResolver] public static Product ResolveProductReference(string id) => new Product { Id = id };}
With the above changes, our supergraph can now support traversing both "from a review to a product" as well as "from a product to a review"!
# Example root query fields - not implemented in the tutorialquery { # From a review to a product (back to the reviews) review(id: "foo") { id content product { id name price reviews { id content } } } # From a product to a review product(id: "bar") { id name price reviews { id content } }}