Skip to content

GraphQL Schema Federation

Schema federation allows us to combine GraphQL Schemas from different distributed services (microservices like Positions, ServiceOrders, etc.) into one schema that includes all the available queries, mutations and subscriptions from the services.

This is particularly useful to create BFF gateways, where we want to combine several services and features into one exposed service that will be used by a particular frontend UI.

Terminology

  • Federation: a group of schemas composed into one super-schema.
  • Federated Schema/Service: a service whose schema was included on a federation.

A note on Redis

Schema federation allows the gateway to be notified on real time when there are changes on the schemas of the services. This feature is achieve using a Redis instance as a cache and leveraging its pub/sub pattern.

Configuring Federation on ITsynch Suite

The Schema Federation needs to be properly configured on both the Gateway and the microservices.

Important

For completeness below we explain how to configure federations by code and by configuration file, but bear in mine that, for federations, you should almost always use the configuration files, as we want to keep it flexible.

Microservice Configuration

On the microservice we need to publish its schema into a Federation. This is done using the GraphQLModuleOptions either by code or by configuration file.

Microservice by code

We can use the AddFederation method to publish the GraphQL Schema of the microservice on a given federation. The FederationName identifies the federation, which it is usually a one to one mapping with a given BFF, in the example below we are allowing positions to be exposed through the AdminCenter BFF. The SchemaNamefield is the name that identifies this particular schema when combined with others on the BFF.

C#
1
2
3
4
5
6
7
8
    builder.DependsOn<GraphQLModule, GraphQLModuleOptions>(ops =>
        {
            ops.AddFederation(b =>
            {
                b.FederationName = "AdminCenter";
                b.SchemaName = "positions";
            });
        }

Tip

The method is call AddFederation because you can add as many as needed. For example the positions schema could also by federated into the ServiceOrders BFF.

Microservice by configuration file

To keep the main configuration file clean as the number of Federation growths we have added support for configuring the federations on a separated file called federations.json. The equivalent setting for the example above would be:

JSON
{
    "GraphQLModuleOptions": {
        "Federations": [
            {
                "FederationName": "AdminCenter",
                "SchemaName": "positions"
            }
        ]
    }
}

On the array called Federations we can add multiple federation configurations.

Sometimes it might be necessary to rename a field of the schema being published, for example, to avoid collisions. For example there could be 2 priorities fields being published to the same federation, one coming from the serviceorders microservice and another one coming from the inspections microservice. We can change the name for some fields on a federation by adding them to the RenameFields list on a federation configuration:

JSON
{
    "GraphQLModuleOptions": {
        "Federations": [
            {
                "FederationName": "AdminCenter",
                "SchemaName": "serviceorders",
                "RenameFields": [
                    {
                        "Type": "Query",
                        "Name": "priorities",
                        "NewName": "serviceOrderPriorities"
                    }
                ]
            }
        ]
    }
}

Note

HotChocolate will resolve collisions automatically by adding a prefix to one of the colliding fields, but the setting introduced above allows us to control the renaming.

Similarly, you can also rename GraphQL types, using the RenameTypes list as follows:

JSON
{
    "GraphQLModuleOptions": {
        "Federations": [
            {
                "FederationName": "AdminCenter",
                "SchemaName": "serviceorders",
                "RenameFields": [
                    {
                        "Type": "Query",
                        "Name": "priorities",
                        "NewName": "serviceOrderPriorities"
                    }
                ],
                "RenameTypes": [
                    {
                        "TypeName": "PriorityDto",
                        "NewTypeName": "SOPriorityDto"
                    }
                ]
            }
        ]
    }
}

Gateway Configuration

On the the Gateway we need to configure what Federation the gateway will be exposing and also the endpoint where to reach each of the schemas added to that federation. To do so we have to use the GraphQLGatewayModuleOptions either by code or by configuration file.

Gateway by code

We can use the FederationName property to indicate which federation the gateway is going to expose. We can use the AddRemoteSchema method to match each schema with the address of its corresponding endpoint:

C#
1
2
3
4
5
builder.DependsOn<GraphQLGatewayModule,GraphQLGatewayModuleOptions>(ops =>
{
    ops.FederationName = "AdminCenter";
    ops.AddRemoteSchema("positions", "positions-service");
});

Important

Notice how we are using positions-service for the address, this is because we are relaying on the Service Discovery feature of the ITsynch Suite, so we do not use an actual valid HTTP address, we use the name of the service on the for the Auto Discovery, then on runtime the actual address is automatically resolved based on the deployment setup.

Gateway by configuration file

As with any Module Options, we can set the values of the FederationName property and the entries for the RemoteSchemas dictionary on the appsettings.json using the standard JSON format.

JSON
1
2
3
4
5
6
    "GraphQLGatewayModuleOptions": {
        "RemoteSchemas": {
            "positions": "positions-service",
        },
        "FederationName": "AdminCenter"
    },

Extension files and whitelisting

By default, using the configuration described on the previous sections, the microservice will publish all its content to the federation. We rarely want this to be the case. We need to control what queries and mutations are published to the BFF, remember that the BFF is facing out of the intranet so it is good policy to avoid publishing unnecessary APIs to the public.

Microservice Configuration for Whitelisting

There are two configuration values we need to set, the IgnoreRootTypes which tells the microservice not to publish the queries and mutations to the federation, and the SchemaExtensionFiles which is a list of extension files where we define what is going to be published to the federation.

JSON
{
    "GraphQLModuleOptions": {
        "Federations": [
            {
                "SchemaName": "service_orders",
                "FederationName": "AdminCenter",
                "IgnoreRootTypes": true,
                "SchemaExtensionFiles": ["admin-center.extensions.graphql"]
            }
        ]
    }
}

On the extensions files we can publish existing queries and create new ones by composing them. The framework allows many files to be added so they can be combined and reused, as one microservice can publish to several federations. The extension files are written in GraphQL SDL combined with some HotChocolate directives. What we need to do on the files is to extend the Query and Mutation types to add the fields we want to publish. Publishing an already existing query is quite straight forward, you can copy the field definition from the microservice SDL (found in the playground or on the /graphql?sdl path of the microservice) paste it on the extended type and add the @delegate directive.

GraphQL
extend type Query {
  issues(
    first: Int
    after: String
    last: Int
    before: String
    where: IssueDtoFilterInput
    order: [IssueDtoSortInput!]
  ): IssuesConnection @delegate()
}

Info

Technically what we are doing is creating a new field called issues that is going to be published to the federation that when it is called it will redirect (a.k.a delegate) the call to the real issues query on the microservice. Since both have the same name and the same list of parameter the @delegate directive can automatically find it without us having to pass any parameters to it.

We can pass several parameters to the @delegate directive to redirect the query to another one, but if we do not pass any parameters, then they are inferred, which is great for when we want to publish an existing query as is.

If we want to change the name of the field, either to avoid collision or to give it an appropriate name in the context of the target BFF, we can change it, but since the name will no longer match the real field, we need to tell the @delegate directive where to find it, for example we can rename issues to serviceOrderIssues as follows:

GraphQL
extend type Query {
    serviceOrderIssues(
        first: Int
        after: String
        last: Int
        before: String
        where: IssueDtoFilterInput
        order: [IssueDtoSortInput!]
    ): IssuesConnection @delegate(path: "issues")
}

On the snippet above, the path parameter tells the directive how to find the target query.

We can also create new fields with completely different names and parameters. A good example of this is the me query on the Profiles microservice. In it, we are creating a new query without parameters and delegating it to the userProfileFor query. The userProfileFor query needs a user ID, which we are taking from the scoped context, this way we return the profile for the current logged in user on the me query.

GraphQL
1
2
3
4
5
6
7
extend type Query {
    me: CurrentUserProfile!
        @delegate(
            schema: "profiles"
            path: "userProfileFor(userCorrelationId: $scopedContextData:currentUserId)"
        )
}

Gateway Configuration for Whitelisting

On the previous section we reviewed how to setup a microservice to publish only what is defined on the extension files by extending the Query and Mutation types. We need to also setup the BFF to properly receive it. On the microservice we set the IgnoreRootTypes parameter to true, so we need to somehow make sure there are root types (Query and Mutation) so they can be extended. To do so we need to set the parameter RegisterRootTypes to true on the BFF, either by configuration file or by code:

JSON
1
2
3
4
5
6
7
    "GraphQLGatewayModuleOptions": {
        "RemoteSchemas": {
            "positions": "positions-service",
        },
        "FederationName": "AdminCenter",
        "RegisterRootTypes":true
    },
C#
1
2
3
4
builder.DependsOn<GraphQLGatewayModule, GraphQLGatewayModuleOptions>(opts =>
    {
        opts.RegisterRootTypes = true;
    });

Warning

If we set the BFF to register root types, and one of the microservices publishing to its federation is not ignoring the root types, then there would be to sets of Root Types, one coming from the microservice that is not ignoring them and one published by the BFF itself, this would cause a critical error. If you set the RegisterRootTypes to true, then make sure all the microservices publishing to that BFF have the IgnoreRootTypes set to true for the corresponding federation.