Skip to content

Creating the Application Service

On the previous article we learned how to expose a query from the inside to the outside using the Backend for Frontend. This approach works great for read operations, but what about writing, a.k.a. creation and update of entities?

Write operations in GraphQL are done using Mutations, which are very similar to Queries. That being said, the microservices themselves do not provide mutations as all creations and updates have to be done using messages over the bus.

Those messages are produced by the Application services, which implements the use cases that the application needs. We are going to introduce a new service on the EMA bounded context that will be in charge of exposing GraphQL Mutations for our equipment entity and translate them into messages for our Equipments microservice.

Creating the service

We are going to use the suiteaspnetapp template to create the application service. To do so, run the following command:

Important

Don't forget to activate the console with the source activate.sh command.

Bash
dotnet new suiteaspnetapp -n ITsynch.Suite.EMA.Application -o src/services/EMA/ITsynch.Suite.EMA.Application

This template doesn't require any special parameters, just the name and the output directory. After running the command we need to close and reopen Visual Studio using the slngen.sh script, after doing so, we should see the newly created project inside the EMA bounded context.

The first thing were are going to do is to rename the SuiteWebAppModule to EmaApplicationModule, remember to rename both the class and the file.

Next we need to reference the GraphQLModule. Open the ITsynch.Suite.EMA.Application project file and add the following project reference:

XML
<ProjectReference Include="$(ModulesPath)ITsynch.Suite.GraphQLModule\ITsynch.Suite.GraphQLModule.csproj" />

Creating the DTOs

In order to be able to create and update equipments we need to create DTOs for both operations. By convention, we store input and output types on Contracts projects. So lets create a new project called ITsynch.Suite.EMA.Application.Contracts on the EMA bounded context using the suitemodule template:

Bash
 dotnet new suitemodule -n ITsynch.Suite.EMA.Application.Contracts -o src/services/EMA/ITsynch.Suite.EMA.Application.Contracts

As usual we need to reopen the solution with ./slngen.sh to see the newly created project.

Now lets delete de default Module.cs file and lets create a new folder called GraphQL. Inside that folder add a new class called CreateEquipment as follows:

C#
namespace ITsynch.Suite.EMA.Application.GraphQL
{
    /// <summary>
    /// DTO for creating a new Equipment.
    /// </summary>
    public class CreateEquipment
    {
        /// <summary>
        /// Gets or sets the DisplayName.
        /// </summary>
        public string DisplayName { get; set; } = null!;

        /// <summary>
        /// Gets or sets the Code.
        /// </summary>
        public string Code { get; set; } = null!;
    }
}

This is the shape the the client will need to send us to create a new Equipment. It doesn't have a correlation ID because that field is generated by the service, not the client.

Now lets add another class for updating an equipment:

C#
namespace ITsynch.Suite.EMA.Application.GraphQL
{
    /// <summary>
    /// This is used to update an equipment.
    /// </summary>
    public class UpdateEquipment
    {
        /// <summary>
        /// Gets or sets the CorrelationId.
        /// </summary>
        public Guid CorrelationId { get; set; }

        /// <summary>
        /// Gets or sets the DisplayName.
        /// </summary>
        public string DisplayName { get; set; } = null!;

        /// <summary>
        /// Gets or sets the Code.
        /// </summary>
        public string Code { get; set; } = null!;
    }
}

We do need the CorrelationId here so we know which entity to update.

Important

Both classes are public. Also pay attention to the namespace.

Exposing the message as mutations

As we mentioned on the introduction, we need to somehow publish a message based on a GraphQL mutation, thankfully the Suite Framework provides a way to do this declaratively: the Exposed Message as Mutation feature.

In our EMA.Application project, we need to get a reference to the message that we need to produce in order to create an Equipment, so we are gonna reference the Equipments.Contracts project.

We also need to add a reference to the EMA.Application.Contracts project we created on the section above so that we have access to the GQL objects we declared. We will also need a reference to the MassTransitModule.

This is how the EMA.Application project should look like now:

XML
1
2
3
<ProjectReference Include="$(ServicesPath)Equipments\ITsynch.Suite.Equipments.Application.Contracts\ITsynch.Suite.Equipments.Application.Contracts.csproj" />
<ProjectReference Include="$(ServicesPath)EMA\ITsynch.Suite.EMA.Application.Contracts\ITsynch.Suite.EMA.Application.Contracts.csproj" />
<ProjectReference Include="$(ModulesPath)ITsynch.Suite.MassTransitModule\ITsynch.Suite.MassTransitModule.csproj" />

Now lets override the SetupModule method on our EmaApplicationModule. Inside it we are going to depend on the MassTransitModule and on the GraphQLModule and use the GraphQLModuleOptions to add some configuration:

C#
public override void SetupModule(IModuleBuilder builder)
{
    base.SetupModule(builder);

    builder.DependsOn<MassTransitModule>();

    builder.DependsOn<GraphQLModule, GraphQLModuleOptions>(ops =>
    {
        // ..
    });
}

The GraphQLModuleOptions class provides a method call ExposedMessageAsMutation that we can use to expose a mutation with a given DTO that is going to be translated into a message and then sent to the bus when the mutation is called:

C#
public override void SetupModule(IModuleBuilder builder)
{
    base.SetupModule(builder);
    builder.DependsOn<GraphQLModule, GraphQLModuleOptions>(ops =>
    {
        ops.ExposeMessageAsMutation<GraphQL.CreateEquipment, CreateOrUpdateEquipment, EquipmentUpdated>(
            "createEquipment",
            b => b.UseIdField(e => e.CorrelationId));
    });
}

The first generic type parameter, GraphQL.CreateEquipment, is the input DTO that will be exposed for the UI to produce.

The second one, CreateOrUpdateEquipment, is the message that is going to be sent when the mutation is executed.

The third one, EquipmentUpdated, is the response that the consumer sends if the creation is successful.

The "createEquipment" parameter is the name of the mutation and the second one is a builder expression that allows us to do some configuration.

For the creation case, when we produce the CreateOrUpdateEquipment, we want to generate a NewId in the CorrelationId field. The UseIdField does just that: it receives an expression that returns a field which will get a NewId set.

Now lets add a similar configuration for the update operation:

C#
public override void SetupModule(IModuleBuilder builder)
    {
        base.SetupModule(builder);
        builder.DependsOn<GraphQLModule, GraphQLModuleOptions>(ops =>
        {
            // ..
            ops.ExposeMessageAsMutation<GraphQL.UpdateEquipment, CreateOrUpdateEquipment, EquipmentUpdated>(
                "updateEquipment");
        });
    }

As you can see, it is very similar, but simpler, as we do not need to use the builder, because no GUID is going to be generated. The CorrelationId is part of the UpdateEquipment message, because the client needs to tell the service what equipment should be updated.

Setting up AutoMapper

As we mentioned, with the configuration done on the previous section the framework will automatically translate the CreateEquipment and UpdateEquipment DTOs into the CreateOrUpdateEquipment message. But how does it know how to translate from one object to the other?

It uses AutoMapper, so to finish our setup we need to add the corresponding profiles for it. Lets start by adding the AutoMapper module to our EMA.Application project:

XML
<ProjectReference Include="$(ModulesPath)ITsynch.Suite.AutoMapperModule\ITsynch.Suite.AutoMapperModule.csproj" />

Then lets add a folder called AutoMapperProfiles on our EMA.Application project, and inside it, a new class called EquipmentProfile. This class must inherit from Profile and in its constructor we will define the mappings for both DTOs.

C#
namespace ITsynch.Suite.EMA.Application
{
    public class EquipmentProfile : Profile
    {
        public EquipmentProfile()
        {
            this.CreateMap<GraphQL.CreateEquipment, CreateOrUpdateEquipment>()
                .ForMember(x => x.Code, x => x.MapFrom(x => x.Code))
                .ForMember(x => x.DisplayName, x => x.MapFrom(x => x.DisplayName))
                .ForMember(x => x.CorrelationId, opt => opt.Ignore());

            this.CreateMap<GraphQL.UpdateEquipment, CreateOrUpdateEquipment>()
                .ForMember(x => x.Code, x => x.MapFrom(x => x.Code))
                .ForMember(x => x.DisplayName, x => x.MapFrom(x => x.DisplayName))
                .ForMember(x => x.CorrelationId, x => x.MapFrom(x => x.CorrelationId));
        }
    }
}

Both mappings are quite similar, the only difference is that on creation one we tell AutoMapper not to worry about the CorrelationId property, as the framework will fill it in (this is related to the usage of UseIdField when exposing the message). For the update we map the CorrelationId as usual, since we want to use the CorrelationId sent by the client.

If we try to execute our service now, we will run into a problem. The CreateOrUpdateEquipment record has no parameterless constructor, so AutoMapper will not be able to create it. We need to modify it:

C#
public record CreateOrUpdateEquipment
{
    /// <summary>
    /// Initializes a new instance of the <see cref="CreateOrUpdateEquipment"/> class.
    /// </summary>
    public CreateOrUpdateEquipment()
    {
    }

    /// <summary>
    /// Gets or sets the CorrelationId.
    /// </summary>
    public Guid CorrelationId { get; set; }

    /// <summary>
    /// Gets or sets the DisplayName.
    /// </summary>
    public string DisplayName { get; set; } = null!;

    /// <summary>
    /// Gets or sets the Code.
    /// </summary>
    public string Code { get; set; } = null!;
}

Important

By changing the CreateOrUpdateEquipment record you will also need to change the Tests we created on a previous article, in the places where the record is instantiated, to use an object initializer instead of the constructor. Since these changes are trivial, they are omitted.

Publishing the schema to the BFF

As we mentioned in previous articles the only entrance point to the ecosystem is through the BFF, so we cannot directly called the mutations we declared, we need to publish them to the correct federation, as we did with the query from the Equipments microservice. To do so, lets add a federations.json file at the root of our EMA.Application project as follows:

JSON
{
    "GraphQLModuleOptions": {
        "Federations": [
            {
                "SchemaName": "equipment_management",
                "FederationName": "EMA"
            }
        ]
    }
}

We also need to tell the BFF how to find the service to forward the calls. On the appsettings.json file we need to add the entry for the Service Discovery and the federation:

JSON
    "ServiceDiscoveryDefaultProviderOptions": {
        "services": {
            "equipments-service": {
                "addresses": [ "localhost:22009" ]
            },
            "ema-app": {
                "addresses": [ "localhost:22010" ]
            }
        }
    },

    "GraphQLGatewayModuleOptions": {
        "RemoteSchemas": {
            "equipments": "equipments-service",
            "equipment_management": "ema-app"
        },
        "FederationName": "EMA"
    },
//....
}

Since we have set the port 22010 for the new service, we need to modify the launchSettings.json file on the EMA.Application project to use it:

JSON
{
    "profiles": {
        "ITsynch.Suite.EMA.Application": {
            "commandName": "Project",
            "launchBrowser": true,
            "launchUrl": "swagger",
            "applicationUrl": "http://localhost:22010",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        }
    }
}

Running all

Now we should be able to start the 3 processes, the Equipments microservice, the EMA.Application service and the EMA.Bff. On the playground of the EMA.Bff we should be able to create a new equipment as follows:

GraphQL
1
2
3
mutation {
    createEquipment(input: { code: "A001", displayName: "Test Equipment" })
}

We can run the following query to check that new equipment was created:

GraphQL
1
2
3
4
5
6
7
8
9
query {
    equipments {
        nodes {
            correlationId
            displayName
            code
        }
    }
}

And we can see the logs of the BFF, Application, and backend service to see what happened.

You can now grab the CorrelationId of the new equipment and try to create an updateEquipment mutation.