Custom Connectors
IdentityMesh ships connectors for Active Directory, SQL Server, CSV files, Entra ID, Okta and Workday. Any other identity source is reached with a custom connector: a .NET class library you build with the Connector SDK and drop into a folder on the sync engine or a relay agent. The engine treats it exactly like a shipped connector. It runs on the same schedules, writes to the same run history and audit log, and its objects join and project with the same rules.
This runbook takes you from an empty project to a connector running in your environment. Read it once end to end before starting, because two of the steps have a single mistake each that fails silently.
Before you start
- .NET 10 SDK on the machine where you build, and Visual Studio 2022 or later (or any editor that builds .NET projects).
- The Connector SDK kit. It ships with IdentityMesh: open
Connectors in the console and click Download Connector SDK.
The zip holds the SDK as a NuGet package, a
nuget.configthat restores from it, the sample connector solution with its tests, and the licence. It builds offline. Unzip it somewhere outsideProgram Files. If the button is missing, the server was installed from a release that predates the kit; upgrade it or ask your IdentityMesh contact for the zip. - A code-signing certificate if the connector will run on a relay agent. The agent only loads signed connectors from publishers it trusts. The sync engine itself does not require a signature.
- Operator access to the IdentityMesh console with permission to create connectors, and local administrator rights on the engine or agent host to copy files and restart the service.
How a connector works
A connector is one class. It tells IdentityMesh four things:
| What | How |
|---|---|
| Its name | A Name property such as SampleCsv. This is the connector type you pick in the console, and it has to match exactly. |
| Its object types | A list of names such as Employee and TeamMembership, each marked as importable, exportable or both. |
| How to read one object type | One method that takes an object type and that type’s last watermark and returns the changes since, plus the new watermark. |
| How to write one change | One method that takes an add, update or delete for one object and returns whether it was written. |
The SDK base class does the rest: it calls the read method once per object type, keeps one watermark per type, isolates a failing type so the others still import, writes additions before removals, and returns one confirmation per change in the order the engine sent them.
Configuration is a JSON document you type into the console when you
create the connector. It reaches the connector as a string at start-up
and the connector parses it however it likes. Credentials go in an
Auth object inside that JSON (Mode, Username, Password,
ClientId, ClientSecret, ApiKey or CertificateThumbprint).
IdentityMesh moves those values into its secret store and hands them to
the connector at run time, so they never sit in the configuration you
can read back.
Step 1: Create the project
Start from the sample solution. It is a complete connector over two CSV files, small enough to read in one sitting, with tests that run without an engine or a source system.
-
Build and test the kit as downloaded first, so you know the toolchain works before you change anything:
dotnet build dotnet test -
Copy the kit folder and rename the sample project. Keep
nuget.configand thepackagesfolder beside your solution. They are what restore the SDK package:<PackageReference Include="IdentityMesh.Connector.Sdk" Version="1.4.0" />If your team has its own NuGet feed, push the package there instead and point
nuget.configat it. -
Set the assembly name so it matches the pattern the engine scans for:
<AssemblyName>IdentityMesh.Connectors.YourSystem</AssemblyName>The engine and the relay agent load only files named
IdentityMesh.Connectors.*.dll. A DLL with any other name is not rejected. It is never looked at, and the connector simply never appears. -
Do not change the package’s build settings. The package keeps
IdentityMesh.Connector.Sdk.dllout of your build output on purpose. The reason is in Step 4.
Step 2: Implement the connector
Derive from ConnectorBase and implement the name, the object types,
and the two methods:
using IdentityMesh.Abstractions;
using IdentityMesh.Connector.Sdk;
using IdentityMesh.Core.Abstractions;
public sealed class HrConnector : ConnectorBase
{
private HrConfig _config = new();
public override string Name => "HrSystem";
protected override IReadOnlyList<ObjectTypeDeclaration> DeclaredObjectTypes =>
[
new ObjectTypeDeclaration("Employee", ImportEnabled: true, ExportEnabled: true),
new ObjectTypeDeclaration("Department", ImportEnabled: true, ExportEnabled: false)
];
public override async Task InitializeAsync(string configJson, AuthMaterial auth, CancellationToken ct)
{
await base.InitializeAsync(configJson, auth, ct); // first, or Logger and Auth are not set
_config = HrConfig.Parse(configJson);
}
protected override Task<ImportObjectTypeResult> ImportObjectTypeAsync(
string objectType, string? watermark, CancellationToken ct)
{
// Read everything of this type changed since the watermark.
// Return the changes and the new watermark.
}
protected override Task<WriteOutcome> ExportChangeAsync(ConnectorChange change, CancellationToken ct)
{
// Write one add, update or delete. Return what happened.
}
}
Three rules that are easy to get backwards:
- A failing object type must not throw from the whole import.
Throw from
ImportObjectTypeAsyncand the base class records that type as failed, keeps its watermark where it was, and imports the other types. Throw from anywhere else and every type is lost for that run. - Return the right outcome from an export.
NothingChangedis a failure: the write missed and the change stays outstanding.AlreadyInDesiredStateis a success: there was nothing left to do. Running out of retries returnsFailed, never an exception. - An empty object-type list means “not declared”, never “none”. Declare every type the connector can handle, even when the operator switches one off in the configuration.
Test Connection in the console calls the connector’s
TestConnectionAsync method. The SDK gives it a default that reports
“not implemented”, so override it to check reachability and
credentials and the console shows your message instead.
Describe your settings
A connector can describe its own settings. You override one method,
DescribeConfiguration, and return the sections, groups and
parameters the connector reads. IdentityMesh draws the settings form
from that description, in the same way MIM management agents describe
their configuration pages, so the person who sets up the connector
fills in labelled fields instead of writing JSON by hand.
This step is optional. A connector that does not override it keeps working, and its settings are edited as JSON.
Here is the declaration from the sample connector, trimmed to the essentials:
public override ConnectorConfigSchema? DescribeConfiguration() => new(
[
new ConfigSection("Files",
[
new ConfigGroup("Location",
[
Param.Text("DataDirectory", "Data directory", required: true,
help: "Full path to the folder holding both CSV files."),
Param.Text("EmployeeFile", "Employee file", @default: "employees.csv",
help: "File name inside the data directory, not a full path."),
]),
new ConfigGroup("Object types",
[
Param.Toggle("ImportEmployees", "Import employees", @default: true,
help: "Read people from the employee file."),
Param.Toggle("ExportEmployees", "Export employees", @default: true,
help: "Write people to the employee file."),
]),
]),
],
Summary: "Reads and writes two CSV files on disk.");
Each parameter’s Key is the name your connector reads from its
settings JSON, so the form and your code stay in step. Declare each
key once, ignoring capitalisation.
Parameter kinds
| Kind | The operator sees |
|---|---|
Text | A single-line text box. Can carry a pattern the value must match. |
Multiline | A larger text box for several lines. |
Number | A number box. Can carry a minimum and a maximum. |
Toggle | A switch, on or off. |
Choice | A drop-down list of the options you declare. |
Url | A text box for a web address. |
Secret | A masked box. The value is stored in the IdentityMesh secret store. |
Most parameters can also set required, help text and a default.
Toggles have no required, and Multiline, Url and Secret parameters
have no default.
visibleWhen shows a parameter only when another one has a given
value, written as OtherKey=Value. The other parameter must be a
Choice or a Toggle.
Secrets
A Secret parameter must use one of three keys:
Auth.PasswordAuth.ClientSecretAuth.ApiKey
These are the credentials IdentityMesh moves into its secret store when the connector is saved and hands back to the connector at run time. IdentityMesh handles only these three, so a secret declared under any other key makes the declaration invalid.
Check the settings on save
Override ValidateConfiguration to check the settings when the
operator saves them. It receives the settings JSON and returns a list
of errors, each naming the field it belongs to. An empty list means
the settings are acceptable.
public override IReadOnlyList<ConfigValidationError> ValidateConfiguration(string configJson)
{
// CsvConnectorConfig is the sample's own settings class. Parse throws
// when the JSON is not usable, so turn that into an error instead.
CsvConnectorConfig config;
try
{
config = CsvConnectorConfig.Parse(configJson);
}
catch (InvalidOperationException ex)
{
return [new ConfigValidationError("DataDirectory", ex.Message)];
}
return Path.IsPathFullyQualified(config.DataDirectory)
? []
: [new ConfigValidationError(
"DataDirectory", "Enter the full path to the folder, for example D:\\feeds. A relative path is not accepted.")];
}
A ValidateConfiguration that throws is reported to the operator as
“could not validate” with the option to save anyway, so catch the
problem and return an error instead.
Keep these rules in mind:
- It runs before the connector is initialised. Use only the JSON you are given.
- It must not reach the source system. Do not open files, folders or network connections. The machine that checks the settings is rarely the one that can see the source, so a check there would be wrong or slow. Reachability is the job of Test Connection.
- Unchanged secrets arrive as references. A secret the operator
did not retype appears as an entry ending in
...SecretRef, not as its value. Treat it as “present”.
What the operator sees
The sync engine or relay agent that runs your connector publishes the declaration to the Admin API as soon as it connects. The console uses it to draw the settings form in the connector editor: one rail section for each section you declare, then Object types. A connector that declares nothing gets a Configuration section where the operator edits the settings as JSON.
If the declaration breaks the rules (a duplicate key, a choice with fewer than two options, a default outside its range, a secret under an unlisted key, more than 200 parameters or 20 sections, and so on), IdentityMesh treats the connector as not having declared anything. The console shows the first problem it found and lets the operator edit the settings as JSON.
Step 3: Test it
The sample’s test project shows the pattern: hand-written stubs, real
files in a temporary folder, no engine and no mocking library. For a
connector over an HTTP API, replace the files with a stub
HttpMessageHandler and keep the rest.
dotnet build
dotnet test
The output you deploy is the one DLL in your bin folder named
IdentityMesh.Connectors.YourSystem.dll, plus any third-party
libraries it needs. There must be no IdentityMesh.Connector.Sdk.dll
beside it. If there is, something changed the package’s build settings.
Step 4: Deploy to the sync engine
-
Copy your connector DLL, and only the third-party libraries it needs, into the engine’s connector folder:
C:\Program Files\IdentityMesh\engine\Connectors\Copy individual files. Do not mirror your whole
binfolder, which is how the SDK DLL ends up there. -
Restart the engine:
sc stop IdentityMeshEngine sc start IdentityMeshEngine -
Confirm it loaded. Open today’s engine log file:
C:\ProgramData\IdentityMesh\identitymesh-<yyyyMMdd>.logand look for a line reading
Loaded connector: HrSystemwith your connector’s name. If instead you seeNo connector was loaded fromfollowed by your DLL’s path, read the message that follows it. It names the cause.
Never put IdentityMesh.Connector.Sdk.dll in the Connectors
folder. The SDK is the contract between your connector and the
engine, and the engine already has its own copy. A second copy beside
your connector is loaded separately, your connector’s interface
becomes a different type from the engine’s, and the engine finds no
connector in your DLL. Current engine builds log a warning naming the
duplicate file. Older builds say nothing at all. If a connector you
deployed is missing from the console with no error anywhere, check for
that file first.
Step 5: Deploy to a relay agent
Skip this step if the connector runs on the sync engine. A connector reaches a source behind a firewall through a relay agent, and the agent has one requirement the engine does not: it loads only Authenticode-signed connectors from publishers listed in its configuration, and it checks the signature’s revocation status.
-
Sign the connector DLL with your code-signing certificate.
-
On the agent host, open:
C:\Program Files\IdentityMesh Relay Agent\appsettings.jsonand add your certificate to the trusted publishers under
Relay:ConnectorVerification. The common name is the certificate’s subject CN and the thumbprint is its SHA-1 thumbprint:"ConnectorVerification": { "RequireRevocationCheck": true, "AllowedPublishers": [ { "SubjectCommonName": "STEADYDEVELOPER LLC", "Thumbprint": "<shipped connectors>" }, { "SubjectCommonName": "Contoso Ltd", "Thumbprint": "<your certificate thumbprint>" } ] }Keep the existing entry. It covers the shipped connectors.
-
Copy your connector DLL and its third-party libraries into:
C:\Program Files\IdentityMesh Relay Agent\Connectors\The SDK DLL is already present in that folder for the shipped connectors, so your connector finds it. Do not copy another one.
-
Restart the agent:
sc stop IdentityMeshRelayAgent sc start IdentityMeshRelayAgent -
Confirm it loaded. In the console, open Relay Agents. The agent’s card lists every connector type it loaded, and yours should be among them once the next heartbeat arrives. If it is not, open Event Viewer, Applications and Services Logs → IdentityMesh, and filter on source IdentityMesh Relay Agent. A rejected connector logs an error naming the DLL, the reason (not signed, publisher not trusted, invalid signature, or revocation status unknown) and the certificate it found.
Step 6: Create the connector in the console
- Open Connectors and click Add connector.
- On Type & name, look for your connector’s card. A connector
that an engine or relay agent has loaded appears there, marked
Custom. If the host that runs it is offline, click My
connector is not listed, enter the name your connector reports
(
HrSystemin the example above), and click Use this type. The name must match exactly. - Give the connector a Name. If it runs on a relay agent, pick the agent under Run on.
- Fill in the connector’s sections. A connector that does not
describe its settings has one section, Configuration, where
you paste the JSON your connector expects, including the
Authobject if it needs credentials. - Click Create connector. In Schedules you can add a
schedule now or leave it for later.
See
admin-ui.mdfor the whole page. - Use Test connection on the connector. Then open Activity, click Run Now beside the connector, and watch the run on the same page. The first import of a new source is the moment to check object counts against what the source should have returned.
Upgrades
The IdentityMesh installer does not remove files it did not install, so your connector stays in the Connectors folder across engine and agent upgrades. Two things can still require a rebuild:
- A new SDK version. Each connector is stamped with the SDK
version it was built against. After an upgrade, check the engine log
for the
Loaded connectorline. If the connector no longer loads, the log message names the SDK versions involved. Rebuild it against the package that matches the upgraded engine and redeploy. - An expired signing certificate on a relay agent. The agent checks the signature on every start, so re-sign and redeploy before the certificate expires, and add the new certificate’s thumbprint to the agent’s trusted publishers.
SDK 1.4 adds DescribeConfiguration and ValidateConfiguration as
optional members. A connector built against 1.3 keeps loading and
running with no change, and its settings continue to be edited as
JSON.
The reverse does not hold. A connector built against SDK 1.4 does not load on a sync engine or relay agent that still carries SDK 1.3. Upgrade the sync engine and every relay agent first, then rebuild your connector against 1.4 and redeploy it.
Replacement DLLs go in the same way: stop the service, copy the file over the old one, start the service.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Connector never appears, no error anywhere | IdentityMesh.Connector.Sdk.dll is beside the connector | Delete it from the Connectors folder and restart. Check the project’s package reference was not altered. |
| Connector never appears, no error anywhere | Assembly name does not start with IdentityMesh.Connectors. | Set <AssemblyName> and rebuild. |
No connector was loaded from … in the engine log | The DLL contains no class implementing the connector contract, or a duplicate SDK copy | Read the rest of the message. It names the types it found and the duplicate’s path. |
Relay agent logs Rejected connector … NotSigned | DLL is not Authenticode-signed | Sign it. |
Relay agent logs Rejected connector … PublisherNotTrusted | Certificate not in AllowedPublishers | Add its subject CN and SHA-1 thumbprint to the agent’s configuration and restart. |
Relay agent logs Rejected connector … RevocationUnknown | The host cannot reach the certificate authority’s revocation service | Allow outbound access to the CA’s CRL or OCSP endpoint. |
| Run fails at start with a configuration message | The JSON on the Configuration step does not match what the connector parses | Fix the JSON in the console. The message is the one your connector threw. |
| Test Connection reports “not implemented” | The connector does not override TestConnectionAsync | Optional. Override it to give operators a real check. |
| Import succeeds but one object type is missing | That type failed and the others carried on | The run shows the failed type and its message. Its watermark did not move, so the next run re-reads it. |