Skip to content

.NET SDK

Consystence.Sdk is the .NET client library that the csy CLI is built on. It targets .NET 10, wraps the HTTP surface behind a set of strongly-typed clients, and is built for dependency injection (DI) first.

The SDK is not packaged for integrators. To integrate with Consystence from your own code, call the REST API directly with an API key. This page describes the library as the CLI uses it, and maps the route families the CLI drives.

The library is deliberately thin: it owns HTTP composition (a named HttpClient, auth and tenant message handlers, retry policy, JSON), and it mirrors the server’s wire contracts as SDK-local records so it never has to reference the server’s internal contract assemblies.

A host adds the SDK to an IServiceCollection with AddConsystenceClient (in the Consystence.Sdk.Extensions namespace); the CLI does this at startup. Two overloads are provided.

Configure options inline:

using Consystence.Sdk.Extensions;
builder.Services.AddConsystenceClient(options =>
{
// The server for the current context: an org's cell-direct host for /api routes,
// or the account host for account commands.
options.BaseUrl = "https://acme.consystence.com";
options.TenantCode = "acme";
});

Or bind from configuration. With no argument it binds the Consystence section, validates data annotations, and validates on start:

builder.Services.AddConsystenceClient(); // binds "Consystence"
{
"Consystence": {
"BaseUrl": "https://acme.consystence.com",
"TenantCode": "acme"
}
}

Either overload registers everything needed: the named ConsystenceApi HttpClient, the message handlers, the default credential provider, every typed client as a singleton, and the IConsystenceClient aggregate. Resolve and use it:

var client = serviceProvider.GetRequiredService<IConsystenceClient>();
var sites = await client.Sites.ListAsync(cancellationToken);

ConsystenceClientOptions (section name Consystence):

PropertyDefaultPurpose
BaseUrlset it (the built-in value is a placeholder, not a Consystence host)Base address of the named ConsystenceApi client (all typed clients).
TenantCoderequiredSent on every API request as X-Tenant-Code.
MaxRetryAttempts3Polly transient-error retry count (idempotency-gated — see the HTTP pipeline).
InitialRetryDelay500 msFirst retry delay; backoff doubles each attempt.
HttpTimeout30 sPer-request timeout.
UserAgentnull → Consystence-Sdk/<version>User-Agent sent on every request; hosts set it to identify themselves.

IConsystenceClient exposes the typed clients as properties. Each is also independently injectable.

PropertyInterfaceRoute familyPurpose
SetupISetupClientGET /api/setup/statusServer setup status (mode, version, activation state).
AdminIAdminClient/api/admin/*Org/tenant administration; tenant YAML import (cloud cell only), export (a cloud cell for the sites it hosts, or a site server for its own site) and restore (RestoreTenantAsync, on a site server); staged tenant import (preflight, plan, apply, discard); API keys (create, list, revoke); and KPI settings.
SitesISiteClient/api/sites, /api/kpi-definitionsSite create/list/get/update; delete and purge, each with a confirmation code; operator policy; and site operations (get, diff, set, clear). Operational reads: active alarms (ReadAlarmsAsync), alarm history, the command-audit ledger (ReadCommandsAsync), the logbook, KPIs and KPI definitions, the maintenance register, the simulation ledger and cloud sync status. Alarm acknowledgement (AcknowledgeAlarmAsync, which records the acknowledgement; nothing is written to a controller), logbook append (attributed to the signed-in person), and site simulation control (status, start, stop, reset, scenarios).
ControllersIControllerClient/api/sites/{siteId}/controllers, /api/sites/{siteId}/controller-chainsController create/list/get/update/assign/decommission, plus each controller’s heartbeat chain (ListChainsAsync).
InstancesIInstanceClient/api/sites/{siteId}/instancesDevice-instance create/list/get/update/delete, plus live reads: current tag values (ReadTagsAsync), a historian trend window (ReadTrendAsync), and the instance’s command audit (ReadCommandsAsync).
ElectricalIElectricalClient/api/sites/{siteId}/electricalPower-distribution nodes, edges, and topology.
AccountIAccountClient/api/me, /api/tenants, /api/orgs/* (account host)Account control-plane self-service (account-of-record, tenants, org/member/licence management).
ConfigIConfigClient/api/admin/config, /api/admin/orgs/{slug}/configStored configuration overrides, for the server or a tenant: list/set/unset, audit trail, and reload (server only).
AiIAiClient/api/aiAI plane (org-admin-gated): provider health (HealthAsync) and test completions (CompleteAsync).
TrendImportsITrendImportClient/api/historian/import-batchesBulk historian import: SubmitAsync uploads a trend CSV + tag-mapping CSV (a duplicate-file_hash 409 resolves to the existing batch rather than failing); GetStatusAsync polls a batch; DeleteAsync deletes the historian rows a batch wrote.
DisplaysIDisplayClient/api/sites/{code}/displaysA site’s authored displays: list, get, diff, put, remove (an owner or admin of the site’s tenant). A display is presentation only and never issues a command.

The SDK sends one credential per request, never both:

  • Bearer — a person’s access token, sent as Authorization: Bearer …. The SDK has no login surface: its token store is internal and is filled by the CLI. The CLI signs in with OIDC against the platform’s identity provider (Entra External ID) and renews the token against its issuer — the identity provider for a hosted sign-in, or the site server’s POST /auth/site/refresh for a site-PIN session. See the CLI’s authentication.
  • API key — a tenant API key (csk_ prefix), sent as X-Api-Key and never refreshed: the key is the credential.

Over the REST API, a read-only key reads, and an operator key can acknowledge alarms on its own site; no key creates or changes sites, controllers or displays. A change needs a person’s sign-in. See API keys and the authentication model.

All typed clients use the named ConsystenceApi client, whose pipeline is:

flowchart LR
  A[Typed client<br/>e.g. Sites.ListAsync] --> B[Tenant header<br/>adds X-Tenant-Code]
  B --> C[Credential<br/>X-Api-Key XOR Bearer]
  C --> D[Polly retry<br/>transient HTTP errors]
  D --> E[Consystence server]
  • Tenant header — every request that does not already carry X-Tenant-Code gets X-Tenant-Code: {TenantCode}.
  • Credential — the SDK asks the credential provider for the active credential. On the API-key path it clears any Authorization header, sets X-Api-Key, and does no refresh or 401 retry. On the Bearer path it attaches Authorization, and — if the token is expired or a 401 comes back and a refresh token exists — refreshes once and resends the request.
  • Polly wraps the named client with HandleTransientHttpError and exponential WaitAndRetryAsync backoff governed by MaxRetryAttempts and InitialRetryDelay.
  • Both the Polly retry and the post-401 resend are idempotency-gated: GET/HEAD/OPTIONS/TRACE, PUT, and DELETE requests may always be re-sent; a POST or PATCH is re-sent only when it declares an idempotency contract — a stable Idempotency-Key header the endpoint honours, or an endpoint whose duplicate-submission 409 resolves to the existing resource. Undeclared POST/PATCH requests are never retried, so a transient failure cannot duplicate a side effect.
var all = await client.Sites.ListAsync(cancellationToken);
var one = await client.Sites.GetAsync("CHPP", cancellationToken);

The /api/sites endpoints return the bare SiteInfo record directly (no {site} envelope). With an API key, the site reads need an admin-role key. See the org and site model for what a site is.

Controllers (the term Consystence uses, not “PLC”) are hand-created per site. The server mints each controller’s canonical ctl_… id — the request carries no id of your own. ControllerConnectionConfig defaults Port to 44818 (EtherNet/IP). ControllerPlatform records the controller’s family (ControlLogix, CompactLogix, Micro800, MicroLogix, PLCnext, Other, or Vplc for a virtual PLC). It is a model field, not a support statement: see controller communication for the drivers the platform ships.

Controllers.GetAsync returns the controller plus its live connection health. Controller records carry the controller’s network endpoint and are never readable with an API key; the CLI’s controller commands read and change them with a person’s sign-in.

Licence issuance and revocation live in the account console — the cell API no longer mints or revokes licences, and the SDK has no dedicated licence client. Licences are read-only through two surviving paths, both with a person’s sign-in:

org.Licences
// Cell-direct: org details carry the licences inline.
var org = await client.Admin.GetOrgAsync("acme", cancellationToken);
// Account host: the org's licence pools and allocations.
var licences = await client.Account.GetOrgLicencesAsync(orgId, cancellationToken);
// licences.Pools, licences.Allocations, licences.Assignments

See licensing for the licence model.

The account control-plane client targets the account host (the CLI scopes these to an account context with an account-audienced bearer).

var me = await client.Account.GetMeAsync(cancellationToken); // null if no account-of-record yet
var tenants = await client.Account.ListTenantsAsync(cancellationToken);

GetMeAsync returns null on a 404 (authenticated, but no account row) rather than throwing — distinct from a 401, which throws.

IAccountClient also carries the org and member family (ListOrgsAsync, CreateOrgAsync, GetOrgAsync, GetOrgLicencesAsync, RequestOrgTierAsync, ProvisionUnderOrgAsync, plus invite and member management — mutations are Owner-only server-side).

The SDK includes a source-generated JSON context, configured with the web defaults and string enums, so enums serialise as strings bidirectionally. It registers the account, licence, setup, site, and admin contract types for AOT-clean, reflection-free (de)serialisation.

The account client, the admin client’s org, member and domain endpoints, and the static platform-status client read through that generated context; the remaining clients currently use the plain web defaults.

The SDK detects a non-JSON / HTML response — for example an SPA shell from a path that matched no route, or an endpoint the server’s baked mode does not serve — and surfaces a clean HttpRequestException naming the request URI, instead of a bare JsonException or a write that silently appears to succeed.