Browse the library
Documentation libraryGet started by language
Get started with C# and .NET
This guide is for WinForms, WPF, console and service software on .NET 8 or later: install the SDK, a few lines of code, a test run, the result on the dashboard, errors, and release. Unity and .NET Framework use the C library and have their own guide: [Get started with Unity](/docs/start-unity).
5 min read
What you need
- The .NET 8 SDK or later. The package runs on .NET 8 and newer, and brings two packages from nuget.org:
BouncyCastle.CryptographyandSystem.Security.Cryptography.ProtectedData. - An app on the dashboard. Its Integration › Connect software page has the API address, the App ID, the public key and a Make a test key button. The API address ends in
/api, with no/v1:https://<your ClavisAUH site>/api.
1. Install the SDK
Download ClavisAuh.0.1.0.nupkg from Downloads, check its SHA-256 as that page shows, and put the file in a packages folder next to your .csproj. Create a nuget.config, also next to the .csproj:
<configuration>
<packageSources>
<add key="clavisauh" value="packages" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
Then add the package:
dotnet add package ClavisAuh --version 0.1.0
The path in nuget.config is relative to that file, and Visual Studio reads the same file. nuget.org serves the two packages that come with it, so the first restore needs a network. If your file has a newer version number, change the number in the command to match. The SDK is not on NuGet yet; once it is, this guide will show that way too.
2. The minimal code
The blocks below are parts of one file; the whole file, with its helpers, is in step 3.
Create the client once, when your software starts. The API address, App ID and public key are the three values on the Connect software page: the sample reads them from environment variables, your software writes them in its code. The device key lives in a file, sealed on Windows with DPAPI for the current user: sessions and offline tokens are bound to it, while the seat on the license follows the machine's hashed identifiers. The sample keeps this in Settings.cs, shared by the five scenarios, with the helpers that read the settings and show an error:
using ClavisAuh;
namespace ClavisSamples;
/// <summary>What every scenario reads from the environment, and the client they all start from.</summary>
internal static class Settings
{
public static string Env(string name) =>
Environment.GetEnvironmentVariable(name) ?? throw new InvalidOperationException($"{name} is not set");
public static string? Optional(string name) => Environment.GetEnvironmentVariable(name);
public static string Lang => Optional("CLAVIS_LANG") ?? "en";
public static string State => Optional("CLAVIS_STATE_DIR") ?? ".clavis";
public static ClavisClient Client(string? appVersion = null) => new(new ClavisOptions
{
BaseUrl = Env("CLAVIS_BASE_URL"),
AppId = Env("CLAVIS_APP_ID"),
AppPublicKeys = [Env("CLAVIS_APP_KEY")],
Device = ClavisDevice.Default(),
// The same key every run: sessions and offline tokens stay with this machine.
DeviceKey = ClavisDevice.LoadOrCreateKey(Path.Combine(State, "device.key")),
AppVersion = appVersion,
});
/// <summary>The sentence for the person using the software, then a line for the CI to read.</summary>
public static int Fail(ClavisException e)
{
Console.WriteLine(e.UserMessage(Lang));
Console.WriteLine($"FAIL {e.Code}");
return 1;
}
}
Activate the customer's key. Without a network, the offline token saved last time keeps the software running:
JsonObject? grant = null;
try
{
await client.InitAsync();
grant = await client.ActivateAsync(Settings.Env("CLAVIS_LICENSE"));
}
catch (ClavisException e)
{
// No answer (network_error, timeout...): a token saved last time keeps the software working.
var offline = e.Retryable && File.Exists(saved) ? client.VerifyOffline(File.ReadAllText(saved)) : null;
if (offline is null) return Settings.Fail(e);
Console.WriteLine("Offline: using the license saved on this machine");
}
The public key only checks signatures, so embedding it in your software is safe. Never embed any secret key.
3. Run it
The quickest way is the starter project, with the SDK and your app's settings already wired in:
- On the Connect software page, pick C#, select Make a test key, then Download the starter (.zip), and unpack it.
- In the unpacked folder, run
.\run.cmd <test key>(Windows) or./run.sh <test key>(Linux, macOS). The script runsdotnet run --project ClavisSamples -c Release -- k1.
In a project of your own instead: add Settings.cs above and the whole K1Key.cs below to a console project that has the package, and make Program.cs the single line return await ClavisSamples.K1Key.RunAsync();. Set the four settings from the Connect software page (CLAVIS_APP_KEY is the public key, not a secret), then dotnet run:
$env:CLAVIS_BASE_URL = "<API address>"
$env:CLAVIS_APP_ID = "<App ID>"
$env:CLAVIS_APP_KEY = "<public key>"
$env:CLAVIS_LICENSE = "<test key>"
export CLAVIS_BASE_URL="<API address>" CLAVIS_APP_ID="<App ID>" CLAVIS_APP_KEY="<public key>" CLAVIS_LICENSE="<test key>"
It prints License active until <date>, then OK k1 active on the last line. Run it again on the same machine and it still succeeds without using another seat: the server knows the machine by its hashed identifiers (Devices, HWID and transfers). The device key, which signs the requests, stays in the .clavis folder under the folder you run it from (or in CLAVIS_STATE_DIR). A container started afresh for each run looks like a new machine every time, see Questions. The whole K1 scenario in the starter:
using System.Text.Json.Nodes;
using ClavisAuh;
namespace ClavisSamples;
/// <summary>K1 · License key only: activate on this machine, keep the session alive, work offline for a while.</summary>
internal static class K1Key
{
public static async Task<int> RunAsync()
{
var client = Settings.Client("1.0.0");
var saved = Path.Combine(Settings.State, "offline.token");
JsonObject? grant = null;
try
{
await client.InitAsync();
grant = await client.ActivateAsync(Settings.Env("CLAVIS_LICENSE"));
}
catch (ClavisException e)
{
// No answer (network_error, timeout...): a token saved last time keeps the software working.
var offline = e.Retryable && File.Exists(saved) ? client.VerifyOffline(File.ReadAllText(saved)) : null;
if (offline is null) return Settings.Fail(e);
Console.WriteLine("Offline: using the license saved on this machine");
}
if (grant is not null)
{
var license = grant["license"]!;
Console.WriteLine($"License {license["status"]} until {license["expiresAt"]?.ToString() ?? "forever"}");
// Every few minutes while the software runs; sessions end after an hour without one.
await client.HeartbeatAsync();
if (client.OfflineToken is { } token) File.WriteAllText(saved, token);
await client.LogoutAsync();
}
Console.WriteLine($"OK k1 {(grant is null ? "offline" : grant["license"]!["status"])}");
return 0;
}
}
4. See it on the dashboard
- Integration › Connect software: step 4 turns to Connected, with
csharp/0.1.0and the machine's operating system. - Licensing & releases › Licenses: the key goes from Unused to Active.
- End users › Devices & sessions: the machine that ran it, and the session it opened.
5. Handle errors
Every refusal is a ClavisException with three things you need:
e.Code: a code from the error codes table, such asinvalid_credentialsordevice_limit.e.UserMessage("en"): a sentence for the person using your software, in English or"vi", ready to show.e.Retryable:truewhen no answer arrived (no network, too slow, a busy server); trying again later may work.
An answer without your app's valid signature is refused with a ClavisVerificationException (also a ClavisException), code unsigned_response, invalid_response_signature, unknown_key or stale_response. Treat it as no answer at all; usually the API address or the public key in your software is wrong.
While your software runs, send a heartbeat regularly to keep the session, every 5 to 30 minutes or so: a session ends after 60 minutes without one.
// Every few minutes while the software runs; sessions end after an hour without one.
await client.HeartbeatAsync();
Save the latest offline token, so the software still opens next time without a network:
if (client.OfflineToken is { } token) File.WriteAllText(saved, token);
Offline tokens come on every plan and last 7 days by default; Premium chooses the length, and Online-Only Mode turns them off. See Offline licenses.
6. Package and release
- Write the API address, App ID and public key in your code or in a config file shipped with the build. Customers' machines need no environment variables.
- Put the state folder (device key, offline token) in
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), under your software's name. Never in the install folder. - Pass
AppVersionas the version you release: the server uses it to block builds that are too old and to announce new ones, see Signed releases and updates. - In WinForms and WPF, call the
…Asyncmethods withawaitin your interface's event handlers. Do not block on.Resultor.Wait(): the window would freeze while the network answers. dotnet publish -c Releaseships the SDK with your software like any NuGet package. Run the published build toOK k1 activeon a clean machine before you release it.- Before you sell, turn on Publicly released in Integration › App settings. Every TEST key stops working, so a test key that got out cannot be used.
Questions
Does the software need a network every time it opens? Activation and heartbeats do. Without a network, the saved token keeps the software running for 7 days by default, or the length chosen on Premium, checked on the machine without the server.
What if a customer reinstalls the system or changes machines? After a reinstall on the same machine, the server still recognizes it from its hashed hardware identifiers. A new machine takes a new seat; when none is left the customer gets device_limit, and you Reset devices on the Licenses page. See Devices, HWID and transfers.
How do I limit features by license plan? client.Has("feature_name") tells whether the license grants that feature; create features in License plans and entitlements.
My software still runs on .NET Framework 4.x. This SDK needs .NET 8. On .NET Framework, call the C library through P/Invoke exactly as in Get started with Unity: the same code, nothing Unity-specific.