Skip to content

Creating Actions

Creating Actions

When a user in the Xchange platform wants to perform some action against a target system, that request is called an Action. These Actions that can be performed against a system are defined by the connector builders. The service that contains all of the Action Handlers defined in the connector is called Action Processor. The Action Processor, using a path-based factory, is the one that gets the correct Handler to instantiate when a user creates an action payload, and it passes the action payload.

About Action Handlers

The Action Handler decides what should happen in response to the event and is used to change data in your system.

When Action Processing is required, the Xchange platform will create an event that passes the data that needs to be changed to your Handler in the form of an ActionInput.

A Handler uses an Action to define the ActionInput, ActionOutput and ActionFailure models, and it uses them in a request to your system.

Putting Action Handlers into action

We’ll start using the CLI to create your first Action

The CLI command:

xchange action new --module-id app-1 --object-name Employees --name Create

Options for this command

  • --module-id
    • A module allows for the grouping of various functionality in your Connector code. In this specific instance, we’re using a generic default group called: app-1.
    • Required: True
  • --object-name (aliases: -o)
    • The name of the data object to create an Action Handler for. PascalCase is preferred. In this instance we are using Employees as a placeholder for your object's name.
    • Required: True

NOTE! The object-name you use here must match the name of the data object you previously created. If they don’t match, you’ll create an Action that is stand-alone and not related to a data object.

  • --name (aliases: -n, --action-name)

    • Create a name for this Action. In this instance we’ll call this Action Create.
    • Required: True
  • --connector-path

    • The path of the folder in which the connector project lives. Defaults to the current working directory if not set.
    • Required: False
    • Default: Current working directory

This command will create a new folder in your repository, within the App/v1/Employees folder, named Create (or whatever the actual action name is that you provided).

In the CreateEmployeesAction.cs file there will be three new classes for data transfer: CreateEmployeesAction, CreateEmployeesActionInput, CreateEmployeesActionOutput.

CreateEmployeesActionInput will need to be modified to include all the properties required for the Handler. These properties are based on what you want your Handler to do, since they are the input for your connector. The ActionInput property is an instance of this class.

CreateEmployeesActionOutput will need to be modified to include all the properties you want to return to the Xchange platform. These properties will usually match the properties of the data object so that the Xchange cache can be updated for your Connector. The ActionOutput property is an instance of this class.

StandardActionFailure is a Connector SDK defined class that also makes an appearance on CreateEmployeesAction.cs, since the ActionFailure property is an instance of it, and it's a class for error handling. The properties of this class provide details of an error, its message and its source.

For more guidance on how to add properties to your input and output data objects follow our object schema guide.


Modify the ActionHandler placeholder class

With your CLI command a placeholder class file was created called CreateEmployeesHandler.cs. We now want to modify that file so that it does what you want.

We will want to inject the client into CreateEmployeesHandler

Example of a client injection into CreateEmployeesHandler
public class CreateEmployeesHandler : IActionHandler<CreateEmployeesAction>
{   
    private readonly IHostContext _hostContext;
    ILogger<CreateEmployeesHandler> _logger;
    private readonly ApiClient _apiClient;

    public CreateEmployeesHandler(
        IHostContext hostContext,
        ILogger<CreateEmployeesHandler> logger,
        ApiClient apiClient)
    {
        _hostContext = hostContext;
        _logger = logger;
        _apiClient = apiClient;
    }
}

Below are additional basic modifications to the CreateEmployeesHandler with comments inline. Change the HandleQueuedActionAsync method so that it can be used to write changes back to your system.

Example of a basic HandleQueuedActionAsync implementation

In summary what is being done is:

  1. Given the input for the action, make a call to your API/system.
  2. Build sync operations to update the local cache as well as the Xchange cache system (if the data type is cached).
  3. If an error occurs, we want to create a failure result for the action that matches the failure type for the action. Common to create extension methods to map to StandardActionFailure.
public async Task<ActionHandlerOutcome> HandleQueuedActionAsync(ActionInstance actionInstance, CancellationToken cancellationToken)
{
    var input = JsonSerializer.Deserialize<CreateEmployeesActionInput> (actionInstance.InputJson)!;
    try
    {
        // (1) 
        var response = await _apiClient.CreateEmployee(input, cancellationToken).ConfigureAwait(false);

        if (!response.IsSuccessful || response.Result == default)
            return ActionHandlerOutcome.Failed(new StandardActionFailure
            {
                Code = response.StatusCode.ToString(),
                Errors = new []
                {
                    new Error
                    {
                        Source = new [] { nameof(CreateEmployeesHandler) },
                        Text = $"Failed to create employee with status code {response.StatusCode}"
                    }
                }
            });

        // (2)
        var operations = new List<SyncOperation>();
        var keyResolver = new DefaultDataObjectKey();
        var key = keyResolver.BuildKeyResolver()(response.Result);
        operations.Add(SyncOperation.CreateSyncOperation(UpdateOperation.Upsert.ToString(), key.UrlPart, key.PropertyNames, response.Result));

        var resultList = new List<CacheSyncCollection>
        {
            new CacheSyncCollection() { DataObjectType = typeof(EmployeesDataObject), CacheChanges = operations.ToArray() }
        };

        return ActionHandlerOutcome.Successful(response.Result, resultList);
    }
    catch (ApiException exception)
    {
        // (3)

        var errorSource = new List<string> { nameof(CreateEmployeesHandler) };
        if (string.IsNullOrEmpty(exception.Source)) errorSource.Add(exception.Source!);

        return ActionHandlerOutcome.Failed(new StandardActionFailure
        {
            Code = "400",
            Errors = new []
            {
                new Error
                {
                    Source = errorSource.ToArray(),
                    Text = exception.Message
                }
            }
        });
    }
}

KeyResolver and the Sync Cache Operations

The Sync Operations update the connector's current cache. When new data is created, or existing is updated, the cache needs to be updated to reflect the changes. The DefaultDataObjectKeyobject gets the key of the data object. First, a list of operations needs to be initialized.

var operations = new List<SyncOperation>();

Then, the DefaultDataObjectKey resolves the key of the Data Object that's being used.

var keyResolver = new DefaultDataObjectKey();
var key = keyResolver.BuildKeyResolver()(resource);

By default, the DefaultDataObjectKey uses the property name "id" as the key. However, it could also receive a customId name as a parameter if the key's name is not "id".

var keyResolver = new DefaultDataObjectKey("customId");

Once the key is resolved, a new SyncOperation object with the new data is created and added to the operations list.

operations.Add(SyncOperation.CreateSyncOperation(UpdateOperation.Upsert.ToString(), key.keyName, key.keys, resource));

Testing

To test your connector locally, check out our guide for local testing here.