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.

From GQL to Generic UIs

These are the high level steps required to a GQL Query or Mutation and be able to generate a generic UI off it and display it in an App, in this case AdminCenter.

Declare DTOs

DTOs for GraphQL are declared in the .Application.Contracts layer of Backend Services, in the namespace .Application.GraphQL.

It is recommended to create this type of objects as record, if possible. Also, make sure to follow the conventions defined here for the naming of the DTOs.

DTOs for Mutations can use nullability to validate that an argument is or not required. i.e if a non nullable property is not provided when executing the Mutation, it won't be executed.

DTOs for Queries must have all fields nullable, since GQL queries don't query all fields of an entity at once.

Important

The DTOs must have their Id field called CorrelationId and if they have a Name field, it should be mapped to DisplayName

Example:

C#
namespace ITsynch.Suite.Positions.Application.GraphQL
{
    /// <summary>
    /// Data-transfer used to represent a Position Type data.
    /// </summary>
    public record PositionTypeDto
    {
        /// <summary>
        /// Gets or sets the Position Type id.
        /// </summary>
        public Guid? CorrelationId { get; set; }

        /// <summary>
        /// Gets or sets the Position Type name.
        /// </summary>
        public string? DisplayName { get; set; }

        /// <summary>
        /// Gets or sets the Position Type parent.
        /// </summary>
        public Guid? ParentPositionTypeId { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether the Positions, associated to the type, can be taken over by a user.
        /// </summary>
        public bool? CanBeTakenOver { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether the Positions, associated to the type, must be manually assign.
        /// </summary>
        public bool? MustBeManuallyAssign { get; set; }

        /// <summary>
        /// Gets or sets a value indicating the Working Department.
        /// </summary>
        public Guid? WorkingDepartmentId { get; set; }
    }
}

Expose Queries

Backend services should expose queries for their entities.

When we depend on the GraphQL Module, we can use ExposeInPublicApi to expose queries of an Entity projected to a DTO, under the specified name.

C#
1
2
3
4
5
builder.DependsOn<GraphQLModule, GraphQLModuleOptions>(
    opts =>
    {
        opts.ExposeInPublicApi<Issue, GraphQL.IssueDto>("issues");
    });

Then, we can run our service and access http://localhost:5001/graphql to see the GraphQL Playground, where we can test a simple query to see everything works ok. For example, a simple query would be:

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, GraphQL.PositionDto>(
            "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 wether 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.

Similar to queries, when depending on the GraphQL Module we can quickly expose the Publishing of an MT Message as a GQL Mutation.

We still are required to provide a DTO tho, this is so we don't leak the internal messaging API with the frontend.

By providing a DTO, we can guarantee that the public API is not broken, since we can then change the message we publish, or do some other logic when the mutation is executed.

We are also required to provide an expression which returns a field of the MT Message where we want a NewId generated.

Publishing a Message

We can use ExposeMessageAsMutation to generate a mutation that will publish a Message. This mutation will immediately return the generated CorrelationId.

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