Skip to content

Testing A Connector Locally

All connector are expected to be run locally and perform as expected before being submitted for deployment.

At this point

  • You understand what a connector is.
  • You have implemented the API Client and related files.
  • You have a data object and at least one Cache Writer or Action Processor implemented

To test the connector locally, the CLI and connector project offers some useful functionality. You can verify if the connector works by:

  1. Initializing a test-settings.json file via xchange test init
  2. Configuring test-settings.json in your root "Connector" folder with test data and options
  3. Configuring launchSettings.json in the "Properties" folder to choose what will run
  4. Using your development environment or command line to run

Instructions

Run Test Init Command

Always build your project before running xchange test init to keep the file up to date.

Note

Starting from version 1.3.6 of the CLI doing a build first is no longer required. xchange test init will trigger a build automatically

This creates a file named test-settings.json and populates it scaffolding based on your current connecter setup. Like most commands, make sure to execute it in the "Connector" directory where Connector.csproj is located.

Command

xchange test init

Warning

Must execute the command in the directory where the csproj file of the connector is, otherwise you will get an error message like the following:

xchange test init
Error Time: 11:42:29.299 Could not find a CSPROJ file in the directory: /Users/builder/Documents/svc/trimble/connector-product

Expected Results on Success

xchange test init
Information Time: 11:44:41.110 /Users/builder/Documents/svc/trimble/connector-product/Connector/test-settings.json could not be found. Creating a new file.
Information Time: 11:44:41.191 Test data generated in /Users/builder/Documents/svc/trimble/connector-product/Connector/test-settings.json

Configure test-settings.json

The test-settings.json file contains different sections which will need to be configured. By default it is listed in .gitignore and will not be sent to App Xchange on submission. Enter connection credentials here to test your connector, such as username, password, bearer token, etc. as appropriate for your authorization setup. Credentials should appear as if they'd been entered by a user in the UI.

{
  "testSettings": {
    "connectorRegistration": {  }, // api configuration
    "activeConnection": "apiKeyAuth", // definition key of connection to use
    "connections": [ // predefined connections to test against
        {
            "definitionKey": "apiKeyAuth",
            "type": "Standard",
            "configuration": { // Update these values with your connection values / credentials.
            "apiKey": "*YOUR API KEY VALUE*",
            "connectionEnvironment": 1 //Connection Environment is an enum value. When we're using enums, we need to include the numeric value instead of a string.
            }
        }
    ],
    "app-1": { // module to test
      "serviceConfiguration": {  } // configuration of the module
      "data": {  } // test data objects
    }
  }
}

connectorRegistrations

The connectorRegistration section coordinates with what you have under Connector/ConnectorRegistrationConfig.cs. On the App Xchange platform these fields would be completed by the user. It may remain blank, as most connection details are now handled via structured Connections, but may be used for workspace level filtering. Enter test configuration details here as needed.

{
  "connectorRegistration": {
    "companyId": 123,
    "accountId": "myTestAccountIdHere"
  },
}

activeConnection

Define the connection you want to use for your test. Enter the definitionKey of the connection you want to test with.

{
   "activeConnection": "apiKeyAuth",
}

connections

An array of credentials that you want to test against. Only one will be used per test run, configured with the activeConnection key.

    {
      "testSettings": {
        "connectorRegistration": {  },
        "activeConnection": "apiKeyAuth", // definition key of the connection to test.
        "connections": [ // Your created connections.
          {
            "definitionKey": "apiKeyAuth",
            "type": "Standard",
            "configuration": { // Update these values with your connection values / credentials.
            "apiKey": "*YOUR API KEY VALUE*",
            "connectionEnvironment": 1
            }
          }
        ],
        ...
      }
    }

Module ID section (app-1 default example)

Contains the serviceConfiguration and data keys for each module to test. The serviceConfiguration section contains information about what Action Processors and Cache Writers exist for the connector, and which ones to test. Set "UploadObject" to "true" to run and "false" to ignore/skip. The data section contains information on the data objects broken down by event and type.

{
 "app-1": {
  "serviceConfiguration": {
     "actionProcessor": {
        "AddHealthCheckConfig": {
           "ProcessQueuedEvent": true,
            "TestAPIFailure": false,
             "TestCacheSyncFailure": false
          },
         },
      "cacheWriter": {
         "HealthCheckConfig": {
            "UploadObject": true,
            "ValidateCacheInsert": false,
            "ValidateCacheUpdate": false,
            "ValidateCacheDelete": false
          }
        }
      }
  },
  "data": {
     "health-check": {
        "add": [
           {
            "Timestamp": "2024-06-25T22:50:19.9781118Z"
           }
          ]
      }
  }
}
Full Example of a test-settings.json

This example includes the following, identified by numbered comment. 1. Non connection Connector Registration Configuration needed for interacting with the external system API. 2. Which connection type to use 3. List of available connections and details to be entered 4. Flag to enable the CreateEmployees action when the Action Processor Local Development profile runs. 5. Flag to enable the cache writer of the Employees data object when the Cache Writer Local Development profile runs. 6. All the CreateEmployeesActionInput properties that are used to test the CreateEmployees action. 7. All the UpdateEmployeesActionInput properties that are used to test the UpdateEmployees action.

{
  "testSettings": {
    "connectorRegistration": {
      "apiDetails": { // (1)
        "cuisine": "",
        "type": "",
        "diet": "",
      }
    },
  "activeConnection": "basicAuth", // (2)
  "connections": [ // (3)
    {
      "definitionKey": "apiKeyAuth",
      "configuration": {
        "apiKey": "",
        "connectionEnvironment": 1
      }
    },
    {
      "definitionKey": "basicAuth",
      "configuration": {
        "username": "",
        "password": "",
        "connectionEnvironment": 1
      }
    },
    {
      "definitionKey": "oAuth2ClientCredentials",
      "type": "OAuth2ClientCredentials",
      "configuration": {
        "tokenUrl": "",
        "clientId": "",
        "clientSecret": "",
        "scope": "",
        "clientAuthentication": 1, //This is an enum value, so we'll include the numeric value instead of a string.
        "connectionEnvironment": 1,
        "accountId": ""
      }
    }
  ],
    "app-1": {
      "serviceConfiguration": {
        "actionProcessor": {
          "AddHealthCheckConfig": {
            "ProcessQueuedEvent": true,
            "TestAPIFailure": false,
            "TestCacheSyncFailure": false
          },
          "CreateEmployeesConfig": {
            "ProcessQueuedEvent": true // (4)
          },
          "UpdateEmployeesConfig": {
            "ProcessQueuedEvent": true
          },
          "DeleteEmployeesConfig": {
            "ProcessQueuedEvent": true
          }
        },
        "cacheWriter": {
          "HealthCheckConfig": {
            "UploadObject": true,
            "ValidateCacheInsert": false,
            "ValidateCacheUpdate": false,
            "ValidateCacheDelete": false
          },
          "EmployeesConfig": {
            "UploadObject": true // (5)
          }
        }
      },
      "data": {
        "health-check": {
          "add": [
            {
              "Timestamp": "2024-06-25T22:50:19.9781118Z"
            }
          ]
        },
        "employees": {
          "create": [ // (6)
            {
              "firstName": "",
              "lastName": "",
              "phone": "",
              "email": "",
              "address": "",
              "city": "",
              "state": "",
              "zip": ""
            }
          ],
          "update": [ // (7)
            {
              "employeeId": 0,
              "firstName": "",
              "lastName": "",
              "phone": "",
              "email": "",
              "address": "",
              "city": "",
              "state": "",
              "zip": ""
            }
          ],
          "delete": [
            {
              "employeeId": 0
            }
          ]
        }
      }
    }
  }
}

Run Test Locally

Now that you've configured test-settings.json, locate Connector/Properties/launchSettings.json. This file contains information about the launch profile, such as the application URL and the environment variables.

What does the launchSettings file contain and what is its purpose:

Locate Properties/launchSettings.json in your connector directory. This is a standard .NET configuration file, which you can read more about here. It defines various settings related to the application's launch, such as environment variables, command-line arguments, and debugging settings to be used by the development environment to determine how the application should run. For example, in Visual Studio it will enable a green debug run option in the top bar. Other editors, such as Visual Studio Code, offer similar support. You can read more about how to use it in Visual Studio Code here.

Properties/launchSettings.json gets copied to the output directory on build, but isn't compiled, so it doesn't trigger an automatic build from the IDE. After any modifications to this file you'll want to make sure to rebuild your solution/project.

The launchSettings file is organized into the following sections, and each is explained below. Most likely you will run Test Connection, Cache Writer, and Action Processor Local Development in that order, with Direct Run Extraction used for debugging.

Action Processor Local Development 
Cache Writer Local Development
Test Connection Local Development
Direct Run Extraction
These profiles share the following common parameters:

Parameters

isLocal: This parameter is a boolean that defaults to false. If you want to run fully local development without communicating with the xchange test system (easiest set up for local development), it should be set to true.

moduleId: This parameter indicates which module we're testing when we're running in fully local development. Local development only

serviceType: This parameter is for local development and indicates if you're running the cache writer for your module, or an action handler in your module. Its values are CacheWriter (default) or ActionProcessor. Local development only

dataObjectUrl: Used for testing an action processor. This parameter indicates the url part of the data object associated with the action you want to test. Local development only

actionUrl: Used for testing an action processor. This parameter indicates the url part of the action whose handler you want to test. Local development only

Testing a Connection

There is no need to modify this profile. If you need to confirm it is correct, follow these steps:

  1. Locate the section Test Connection Local Development
  2. Under the field/property commandLineArgs, you should see --isLocal true --testingConnection true for the connection defined in test-settings.json.

Command

dotnet run --launch-profile "Test Connection Local Development"

This or any profile can be run via the IDE of your choosing as well. Feel free to use the tool you are most comfortable with.

Expected Results on Success

Local development is currently configured. You'll need to use the Xchange.Connector.SDK.Test.Local library
[09:55:33 INF] Connector (1.2.3.0) runtime started in 1.5096147 seconds
[09:55:36 INF] The connection test was successful
[09:55:36 INF] Service runner shutting down...

Token Storage

If you're working with the OAuth2CodeFlow auth type, and want to run or test your connector locally, you will have to work with Token Storage.

When your connection is triggered for the first time, a pop up window will open; this will prompt you to login. In the process of authentication, a code will be exchanged for a token. If authentication is successful, the response should include an access_token and a refresh_token in its body. The response should be some variation of this:

```json
{
   "access_token":"<access_token>",
   "token_type":"Bearer",
   "expires_in":3600,
   "refresh_token":"<refresh_token>"
 }
```

When working locally, this response will be stored in a new folder called Token Storage, located inside the /bin folder of your connector.

The file generated with the response will be used in future transactions; the OAuth2CodeFlowHandler will get the access_token key and use it to authenticate the request. If authentication fails (a code of 401: Unauthorized is returned), the handler will use the refresh_token to reauthenticate.

Testing Cache Writer

You will likely only need to change the module-id, if anything. This will run the service configuration you set up above for the cache writer, testing a cache write of each object for which uploadObject was set to true in test-settings.json.

Command

dotnet run --launch-profile "Cache Writer Local Development"

This or any profile can be run via the IDE of your choosing as well. Feel free to use which one you are comfortable with.

Example Expected Results on Success

$ dotnet run --launch-profile "Cache Writer Local Development"
Local development is currently configured. You'll need to use the Xchange.Connector.SDK.Test.Local library
<6>ESR.Hosting.RunService[0] ESR (1.0.6.0) Host started in 0.646475 seconds
<6>ESR.Hosting.RunService[0] Running job 60ab563e-06be-491b-ac91-78b0d668288e for Subscriber Id 0
Time to Retrieve Service Run Message and Signal Started: 966
Time to initialize context: 68
Time to validate context: 94
[12:17:28 INF] Writing <connector-key/app/1/employees> (Asynchronous) data objects to cache
[12:17:29 INF] Change detection system determined that there were 10 new records, 0 updated records, 0 unchanged records, and 0 deleted records for object of type <connector-key/app/1/employees>.
[12:17:29 INF] App Network sync summary for connector-key/app/1/employees
Inserts: 10 successful, 0 no change, 0 failed
Updates: 0 successful, 0 no change, 0 failed
Deletes: 0 successful, 0 no change, 0 failed
Time to Finish Service: 752
<6>ESR.Hosting.RunService[0] Service runner shutting down…

Note

When you run a cache writer or action processor a directory named ‘databases’ gets created in the process's current working directory, such as the bin directory. These databases are an emulation of the Xchange platform. More on this in the section Emulation Databases

Testing Action Processor

You will want to change the moduleId, dataObjectUrl, and actionUrl properties in this section to run a local action processor for the action of interest.

  1. Locate the section Action Processor Local Development
  2. Under the field/property commandLineArgs
  3. The default template will appear as "commandLineArgs": "--isLocal true --moduleId app-1 --serviceType ActionProcessor --dataObjectUrl health-check --actionUrl add", (You may not have a health-check object unless your module was called with the optional --add-health-checks). Fill out the --dataObjectUrl XXXX --actionUrl XXXX part of the command line for data object and action - note it is case sensitive based on the CLI command.
  4. After running an Action, confirm that your sync operations functioned as expected to keep the cache up to date. See Sync Operations in Detail and Emulation Databases.

Example

--dataObjectUrl employees --actionUrl create

Command

dotnet run --launch-profile "Action Processor Local Development"

Expected Results on Success

Local development is currently configured. You'll need to use the Xchange.Connector.SDK.Test.Local library
<6>ESR.Hosting.RunService[0] ESR (1.0.6.0) Host started in 0.42326 seconds
<6>ESR.Hosting.RunService[0] Running job 35b583fe-ffb8-4090-bfd9-98a335313a19 for Subscriber Id 0
Time to Retrieve Service Run Message and Signal Started: 928
Time to initialize context: 76
Time to validate context: 89
[12:24:53 INF] Starting action processor for any queued actions
[12:24:54 INF] Action with ID ffd85a7f-335a-4c38-8cfb-d182c974e0c2 closed as Successful 
[12:24:54 INF] Processing actions complete. 1 were handled. 0 queued actions were skipped
Time to Finish Service: 424
<6>ESR.Hosting.RunService[0] Service runner shutting down...

Expected Error If Data Object or Action Command Line Argument Are Wrong

dotnet run --launch-profile "Action Processor Local Development"
Local development is currently configured. You'll need to use the Xchange.Connector.SDK.Test.Local library
<6>ESR.Hosting.RunService[0] ESR (1.0.6.0) Host started in 0.503598 seconds
<6>ESR.Hosting.RunService[0] Running job fc746eda-9a10-49a4-b3ac-9e565be8ecf3 for Subscriber Id 0
Time to Retrieve Service Run Message and Signal Started: 1016
Time to initialize context: 76
Time to validate context: 84
[12:24:38 INF] Starting action processor for any queued actions
[12:24:38 ERR] An error occurred during action processing
System.Exception: Unable to resolve an IAction<> type for the module 'app-1' and the DataObject 'employees' and its action 'Create'
  at Xchange.Connector.SDK.Test.Local.TestDataSource.GetIActionInterface()
  at Xchange.Connector.SDK.Test.Local.TestDataSource.GetTestQueuedData()
  at Xchange.Connector.SDK.Test.Local.Client.LocalAppNetworkClient.GetQueuedActions(String urlPart, String actionPath, CancellationToken cancellationToken)
  at ESR.Hosting.Action.ActionHandlerService.HandleActionsInActionQueueAsync()
<3>ESR.Hosting.RunService[0] Exception while running service System.Exception: Unable to resolve an IAction<> type for the module 'app-1' and the DataObject 'employees' and its action 'Create'    at Xchange.Connector.SDK.Test.Local.TestDataSource.GetIActionInterface()    at Xchange.Connector.SDK.Test.Local.TestDataSource.GetTestQueuedData()    at Xchange.Connector.SDK.Test.Local.Client.LocalAppNetworkClient.GetQueuedActions(String urlPart, String actionPath, CancellationToken cancellationToken)    at ESR.Hosting.Action.ActionHandlerService.HandleActionsInActionQueueAsync()    at Xchange.Connector.SDK.Hosting.ServiceRunner.RunServiceAsync(IConnectorServiceDefinition serviceDefinition, CancellationToken cancellationToken)    at Xchange.Connector.SDK.Hosting.ServiceRunner.RunAsync(CancellationToken cancellationToken)    at ESR.Hosting.RunService.StartAsync(CancellationToken cancellationToken)
<6>ESR.Hosting.RunService[0] Service runner shutting down...

Logging

Note

Starting from version 1.5.2 of the SDK, Debug level logs and above will be visible when testing locally

When debugging a connector, you may need to inspect your API client's response or otherwise look at an object's values at a certain point. Instead of using Console.WriteLine (which must be removed before connector submission), you may leverage the ILogger instance injected into the action handler to write Debug and Information level logs to the terminal's output. You will see these messages when running locally; they will not be visible once running on the Xchange platform. Logs of severity level Warning and above will be visible when running on Xchange. For more information on log severity levels in .NET, please refer to the official documentation.

Log Visibility

* Information level logs are only visible if coming from the following namespaces:

  • Attachments
  • ESR
  • Tools.Cdx
  • Xchange

Notably, this does not include the default Connector namespace.

Log Level Local Run Visibility Xchange Visibility
Trace
Debug
Information ✓*
Warning
Error
Critical

Logging Best Practices

  • Excessive logging may result in noticeable slowdowns of connector actions. Ensure that your logs contain meaningful information. For instance:
    • _logger.LogInformation("Action handler started.") would not be appropriate; it is self-evident that the code is executing. Additionally, it does not provide any context, parameters or data that was received or processed.
    • _logger.LogInformation("Processing user creation request for UserID: {UserId}", request.UserId) is appropriate, as it provides additional context on the request being processed that could be useful for debugging.
  • Ensure that your logs do not expose Personally Identifiable Information (PII) or otherwise sensitive data.
  • Use the appropriate log severity level.
    • This is important because an incorrect level can both block visibility of important events on the Xchange platform and complicate debugging by mixing critical events with low-priority logging.
    • The example _logger.LogDebug("API call failed with status code {StatusCode}. User data could not be fetched.", response.StatusCode) is inappropriate because it logs a critical failure to the Debug level, such that the log will not be visible when running on Xchange and it will not be highlighted during local testing.
  • Use structured logging by leveraging the params of logger methods.
    • _logger.LogInformation("Received response with status {statusCode}", response.StatusCode) allows for filtering by status code once the logs are ingested by Xchange.
    • _logger.LogInformation($"Received response with status {response.StatusCode}") is not acceptable and makes filtering for error auditing unnecessarily difficult.

Resetting Local Cache

If you need to reset the local databases (See Emulation Databases) this command will do so:

Command

xchange test reset

If you're using OAuth2CodeFlow, this command will also reset the Token Storage folder.

Emulation Databases

When you run a Connector locally there is no interaction with Xchange platform, only the target API the Connector is connecting against is interacted with.

There are two databases that are created:

  • Record cache
    • This is found at databases/00000000-0000-0000-0000-000000000000.db
    • The purpose of this database is store records that were sent to the App Network emulation. For example this powers the "App Network sync summary" that you'll see after a data reader finishes during a cache write service run.
  • Change detection
    • This is found at databases/Ryvit/{connector-key}/123/diffyV3.1/{module-key}_v{module-version}_{data-object-key}
    • The purpose of this database is to track a hash of all records passed through a Connector. This has is used to infer if a record is new (insert) or being updated. Additionally, if a record is known in the database but during a full data reader run is not yielded, then the change detection database will infer that the record should be deleted.

These databases are SQLite and thus for auditing and debugging purposes can be inspected to validate or troubleshoot test runs.

Summary

At this point, you should have used the test init command to pre-populate a test-settings.json file and configured it for testing, and run the test according to launchSettings.json with your IDE or command line.

You can repeat the build -> test init -> configure -> launch process as you complete your connector.

Note

Starting from version 1.3.6 of the CLI doing a build first is no longer required. xchange test init will trigger a build automatically