Skip to content

GraphQL Generic UIs

The Angular frontend includes features to generate UI based off GQL Queries and Mutations. These are the steps required on the backend to expose them.

Expose list queries

Backend services should expose list queries for their entities. These are particular queries that are exposed through connection types which enable filtering, sorting and pagination capabilities.

When we depend on the GraphQL Module, we can use ExposeInPublicApi to expose list queries under the specified name.

C#
1
2
3
4
5
builder.DependsOn<GraphQLModule, GraphQLModuleOptions>(
    opts =>
    {
        opts.ExposeInPublicApi<Issue>("issues");
    });
GraphQL
1
2
3
4
5
6
7
8
{
    issues {
        nodes {
            code
            displayName
        }
    }
}

Expose Hierarchical Queries

Backend services should expose hierarchical queries for their entities, if they have a parent-child self dependency.

This type of query is a must do for Tree List Pages. To expose an hierarchical query, there are 4 points to take into account:

  • GraphQL module is imported.
  • The entity has an specification for child elements. Example:
C#
1
2
3
4
5
6
7
    public class ChildPositionFilterSpecification : FilterSpecification<Position>
    {
        public ChildPositionFilterSpecification(Guid? parentPositionId)
        {
            this.Criteria = x => x.ParentPosition == null ? parentPositionId == null : x.ParentPosition.CorrelationId == parentPositionId;
        }
    }
  • The ExposeHierarchicalQuery is added to the GraphQL module using the specification from the previous step.
  • An expression to select the entity's CorrelationId must be provided.

Example:

C#
1
2
3
4
5
6
7
8
builder.DependsOn<GraphQLModule, GraphQLModuleOptions>(
    opts =>
    {
        opts.ExposeHierarchicalQuery<Position>(
            "positions",
            parentId => new ChildPositionFilterSpecification(parentId),
            position => position.CorrelationId);
    });

This method will add a query with the name provided and Children appended at the end. From the previous example, if we specified positions, the module will expose a query called positionsChildren. At the GraphQL playground we could perform the following tests:

  • Example 1: This will return all root positions.
GraphQL
1
2
3
4
5
6
7
{
    positionsChildren {
        correlationId
        displayName
        childCount
    }
}
  • Example 2: This will return the children from the position provided.
GraphQL
1
2
3
4
5
6
7
{
    positionsChildren(parentId: "74789315-F841-DC46-B293-1399C18D2FE6") {
        correlationId
        displayName
        childCount
    }
}

Please note that a childCount attribute is automatically added to the projected DTO by the Suite Framework, this is specially useful to know beforehand whether each node has / hasn't children, and helps the UI to render the UI components correctly.

Expose Mutations

Application services should expose mutations for executing the use cases the UI requires.

This can be achieved by means of the GraphQL module as well. In this case, it is required to define an input type for our mutation, which will eventually be mapped to the corresponding command.

ID generation

In some cases, specially when we're executing mutations to insert new data to the database, we expect an ID to be generated for us. The GraphQL module is responsible for doing this, and we'll only need to reference the input field that we expect to use as the id field.

Publishing a Message

We can use ExposeMessageAsMutation to generate a mutation that will publish a message. This mutation will immediately return the generated correlation id.

C#
1
2
3
4
5
6
builder.DependsOn<GraphQLModule, GraphQLModuleOptions>(ops =>
{
    ops.ExposeMessageAsMutation<UserCreationDataTransfer, CreateAdminCenterUser>(
        "createAdminCenterUser",
        m => m.CorrelationId);
});

If the message being exposed requires the ID of the logged in user, we need to provide an expression which returns the field where we want the ID of the user to be automatically set.

C#
1
2
3
4
5
6
7
    ops.ExposeMessageAsMutation<UserCreationDataTransfer, CreateAdminCenterUser>(
                     "createAdminCenterUser",
                     b =>
                     {
                         b.UseIdField(m => m.CorrelationId)
                         .UseCurrentUserIdField(m => m.OperationOwnerId);
                     });

We are using the version of the ExposeMessageAsMutation method that takes a builder that allows us to configure the behavior. The UseIdField indicates where we want the new ID for the message to be stored, usually CorrelationId and the UseCurrentUserIdField indicates where we want the current user ID to be set.

Important

If the exposed message requires a logged in user and none is present, the mutation will return an Authentication error.

Request/Response Message

We can pass a third generic parameter to ExposeMessageAsMutation to set the response message type. This will cause the message to be sent using the Request/Response pattern, the GraphQL call will wait for the response message to arrive before returning.

C#
1
2
3
4
5
6
7
    ops.ExposeMessageAsMutation<UserCreationDataTransfer, CreateAdminCenterUser, AdminCenterCreated>(
                     "createAdminCenterUser",
                     b =>
                     {
                         b.UseIdField(m => m.CorrelationId)
                         .UseCurrentUserIdField(m => m.OperationOwnerId);
                     });

We can also pass a forth generic parameter to ExposeMessageAsMutation to set the type of the DTO to be returned. The mutation will automatically mapped the response message into the return DTO using Automapper.

C#
1
2
3
4
5
6
7
    ops.ExposeMessageAsMutation<UserCreationDataTransfer, CreateAdminCenterUser, AdminCenterCreated, AdminCenterCreatedDto>(
                     "createAdminCenterUser",
                     b =>
                     {
                         b.UseIdField(m => m.CorrelationId)
                         .UseCurrentUserIdField(m => m.OperationOwnerId);
                     });

Important

Make sure to modify your Saga or Consumer to Respond to that Request.

Add RemoteSchema to BFF

The final steps for us to see the Queries and Mutations in the UI is to add the backend service as a Remote Schema in the Backend for Frontend.

The BFF combines the queries/mutations of all Remote Schemas into a single schema, which is what the front end uses. This is achieved through a feature called Schema Federation.

Generic UIs

The Suite Framework for Angular has modules for generating user interfaces based off Queries and Mutations.

Generic Creation Page

Generic List Page