Moonbase.Net
4.2.0
dotnet add package Moonbase.Net --version 4.2.0
NuGet\Install-Package Moonbase.Net -Version 4.2.0
<PackageReference Include="Moonbase.Net" Version="4.2.0" />
<PackageVersion Include="Moonbase.Net" Version="4.2.0" />
<PackageReference Include="Moonbase.Net" />
paket add Moonbase.Net --version 4.2.0
#r "nuget: Moonbase.Net, 4.2.0"
#:package Moonbase.Net@4.2.0
#addin nuget:?package=Moonbase.Net&version=4.2.0
#tool nuget:?package=Moonbase.Net&version=4.2.0
Moonbase.Net Licensing SDK
This package contains API client, persistence layers and device ID resolvers to handle software licensing easily.
It requires you to have an account on https://moonbase.sh, and more documentation on this package can be found in our docs at https://moonbase.sh/docs/licensing/sdks/dotnet/.
Install
dotnet add package Moonbase.Net
Create a licensing client
using Moonbase.Net;
using Moonbase.Net.Storage;
var licensing = LicensingFactory.CreateInstance(opts =>
{
opts.Endpoint = new Uri("https://demo.moonbase.sh");
opts.ProductId = "demo-app";
opts.AccountId = Environment.GetEnvironmentVariable("MOONBASE_ACCOUNT_ID");
opts.PublicKey = Environment.GetEnvironmentVariable("MOONBASE_PUBLIC_KEY");
// Optional analytics reported with every activation and validation.
opts.ApplicationVersion = "1.2.3";
opts.Metadata = new Dictionary<string, string> { { "Host", "Console" } };
// Optionally adjust where the license is stored, or swap in another
// ILicenseStore to persist the token somewhere else entirely.
opts.LicenseStore = new FileLicenseStore();
});
PublicKeyis the Moonbase public key used to verify signed license tokens. It is on the product page in your Moonbase account, under the integration guide.FileLicenseStorepersists the license locally (license.mbby default).
Validate on startup
Only delete the stored license when Moonbase says the license is gone. Deleting it on any error makes the user re-activate every time a check fails for a reason that has nothing to do with the license, such as opening the app offline.
using System.Net.Http;
using Moonbase.Net;
using Moonbase.Net.Exceptions;
async Task<License?> LicenseOnStartup()
{
var localLicense = await licensing.Store.LoadLocalLicense();
if (localLicense == null)
{
return null; // show your activation flow
}
if (localLicense.ActivationMethod == ActivationMethod.Online)
{
try
{
// Ask Moonbase first: it is how a revocation reaches the app, and how a token whose
// expiry has passed is refreshed after a renewal.
var refreshed = await licensing.Client.ValidateLicense(localLicense);
await licensing.Store.StoreLocalLicense(refreshed);
return refreshed;
}
catch (Exception exception) when (exception is LicenseRevokedException or LicenseExpiredException)
{
// Moonbase says the license can no longer be used on this device.
await licensing.Store.DeleteLocalLicense();
return null; // show your activation flow
}
catch (Exception exception) when (
exception is StoreClosedException or HttpRequestException or TaskCanceledException)
{
// The store has closed for good (see below), or Moonbase could not be reached right
// now. Neither says anything against the license, so check it locally instead.
}
}
try
{
// Signature, device binding and expiry, which needs no network.
return await licensing.Validator.ValidateLicense(localLicense.Token);
}
catch (Exception exception) when (exception is LicenseInvalidException or LicenseExpiredException)
{
// Keep the stored token: only Moonbase can say the license is gone, and the next launch
// that reaches it can still refresh a token that has expired.
return null; // show your activation flow
}
}
To limit how long an app keeps running without reaching Moonbase, compare License.LastVerifiedUtc
with a limit of your choosing before returning the locally validated license.
When the store has closed
When a store closes its Moonbase account, buyers keep using what they bought on the devices they have already activated. The next time such a device validates its license online, Moonbase answers with the same license as an offline license, signed with the store's key, and the example above stores it. From then on the license is checked on the device alone and Moonbase is never asked again: a license bought outright keeps working for good, and a subscription until its period ends.
Everything else on a closed store throws StoreClosedException: activating a new device, starting a
trial, or validating a trial. When the activation flow throws it, say that the store has closed rather
than asking the user to try again.
Do not delete the stored license on StoreClosedException, and do not relax anything because of it.
It deliberately does not derive from LicenseInvalidException, so a handler that deletes the stored
license on an invalid one leaves it alone; catch it where you catch the failures that mean Moonbase
could not be reached, and keep any limit you set on how long the app runs without reaching Moonbase.
The answer is not signed, so anyone can fake one; only the signed offline license makes a device
independent of Moonbase.
Device fingerprint
Every license is bound to the machine via a device id, stored in the token's sig claim and
re-checked on each local validation. As of 4.0.0 the default MoonbaseDeviceIdResolver computes it
from the Moonbase fingerprint spec (moonbase:fingerprint:v2): a
SHA-256 of stable native hardware identifiers, stamped with the spec version.
mbd2_9f3c… // 'mbd' + version + source tag + '_' + 64 lowercase hex characters
mbd2n_9f3c… // 'n': the opt-in host-name fallback (see below)
mbd2s_9f3c… // 's': an app-scoped id, which only iOS and Android produce
Sources are SMBIOS on Windows, IOPlatformUUID on macOS, and machine-id plus world-readable DMI
on Linux. The spec is language-neutral by design: any Moonbase SDK that implements it and passes the
shipped fingerprint-vectors.json computes the same id on a given
machine, so a license activated through one validates through the others.
Adoption is per-SDK rather than automatic, so check that the SDK you are pairing with implements the spec before relying on that.
This package parses all three forms, including a tag introduced by a newer SDK, which it
compares literally rather than rejecting. Its built-in readers only ever produce mbd2_ or
mbd2n_: a scoped id comes from identifierForVendor on iOS or androidId on Android, and reading
either needs UIKit or the Android framework, which none of this package's target frameworks can
call. If you can reach those APIs, bridge them, and the resolver stamps the result for you:
opts.DeviceIdResolver = new MoonbaseDeviceIdResolver(new MoonbaseDeviceIdResolverOptions
{
Platform = FingerprintPlatform.Ios, // or .Android
Reader = new MyPlatformIdentityReader(),
});
A bridge owes the spec's collection rules along with the value, since canonicalization is shared
with every other SDK and will not reshape a parameter for you: identifierForVendor is
UIDevice.identifierForVendor.UUIDString with every - removed and uppercased, and androidId is
Settings.Secure.getString(resolver, ANDROID_ID) lowercased. A value in the wrong shape is refused
as insufficient identity rather than hashed, so a mistake surfaces immediately instead of as a
license that silently fails to validate against every other SDK.
The source is derived from the platform, never chosen by the caller, so a bridged value cannot be
mislabelled as a hardware fingerprint. A scoped id is stable only within one app scope and must
never be compared with one from another scope, in either direction. That is exactly what the s
tag exists to announce; the spec's Scoped identity section is the
definitive statement of what it does and does not promise.
Both files ship inside the package. If the links above do not resolve where you are reading this, open the
.nupkgor find them in your NuGet package cache undermoonbase.net/<version>/.
The id survives a rename, a locale change, a firmware update, a vCPU resize, and running with or without elevated privileges. The spec's stability contract is the definitive list. Read it before shipping, along with the Linux exceptions, which exist because every per-unit hardware serial is root-only there and the id is tied to the OS installation rather than the hardware:
- A Linux OS reinstall requires re-activation.
- A Linux VM cloned without clearing
/etc/machine-idkeeps its device id, so a license copied with the disk keeps validating.machine-id(5)requires reusable images to ship that file empty; when they do, clones behave correctly. The SDK cannot detect a badly-prepared image, because the value that would distinguish the instances is root-only. - A Linux sandbox that hides
/sys/class/dmi/id(some Flatpak and Snap confinements, and containers started without it) changes the device id, because those four model-level fields drop out of the material. Moving an installed app into or out of such a sandbox therefore requires re-activation. This follows from the spec's collection rules rather than from this SDK, so every conforming SDK computes the same changed id; it is not a cross-SDK divergence. If you ship into a confined environment, ship it confined from the start.
Because the version is part of the id, a mismatch is diagnosable:
try
{
await licensing.Validator.ValidateLicense(localLicense.Token);
}
catch (LicenseDeviceMismatchException exception)
{
// "License does not match current computer", plus any fingerprint version or
// identity-kind difference, each of which points at a different remedy. When either
// id may be app-scoped the lead retreats to "License is bound to a different device
// id", because a scoped difference does not establish a different device.
Console.Error.WriteLine(exception.Message);
}
LicenseDeviceMismatchException derives from LicenseInvalidException, so existing handlers keep
working. Catch it specifically to offer re-activation rather than discarding the license: a mismatch
is not a bad license.
When there is no hardware identity
The resolver throws InsufficientDeviceIdentityException rather than falling back to something
weak, in two cases:
- Nothing readable. A sandboxed process; a platform the spec defines no parameters for (BSD, and anything unrecognised); or iOS and Android, whose parameters this package cannot reach without a bridged reader.
- Only model-level values readable. Vendor, product and board names are byte-identical across
every unit of a product line, so fingerprinting them would let those machines validate one
another's licenses. In practice: a Linux install with no
machine-id, or a machine whose SMBIOS carries an unset UUID and a blank or filler baseboard serial, the usual shape of a cloned VM image.
It is thrown from Validator.ValidateLicense, Client.RequestActivation / RequestTrial /
RequestLicense, and GenerateDeviceToken, wherever a device id is required. Nothing is thrown
from LicensingFactory.CreateInstance, so you always have a usable client to surface the diagnostic
with, and GetDeviceNameAsync() keeps working, so you can still show which machine you are on.
Opt in explicitly if a weaker id beats none. Those ids are stamped mbd2n_ so the server can tell
them apart:
opts.DeviceIdResolver = new MoonbaseDeviceIdResolver(DeviceIdFallback.DeviceName);
The fallback is refused on iOS and Android, which throw regardless of this setting. There the host name is not weak identity, it is not identity at all: since iOS 17
gethostname()returns the literallocalhoston every device. Falling back would give a whole install base one id, so a single activation would unlock every device. Absence there is transient, so raise the error and retry later.
Diagnostics and parity checks
DescribeDeviceAsync() returns the id, spec version, platform and the names of the parameters
that contributed. It is safe to log or attach to a support ticket.
// A MigratingDeviceIdResolver is not itself describable; it exposes the resolver it wraps.
var resolver = licensing.DeviceIdResolver is MigratingDeviceIdResolver migrating
? migrating.Current
: licensing.DeviceIdResolver;
if (resolver is IDescribableDeviceIdResolver describable)
{
var device = await describable.DescribeDeviceAsync();
Console.WriteLine(
$"{device.DeviceId} (v{device.Version}, {device.Platform}, " +
$"from {string.Join(", ", device.ParameterNames)})");
}
Parameter values are never exposed there, and neither are per-parameter hashes. They are hardware serial numbers, and an unsalted per-value digest is no safer to publish than the value, since low-entropy values such as host names or sequential serials fall to a dictionary. Which parameters contributed is the useful diagnostic; what they read is not.
The device id itself is a one-way hash of all of them together, so it discloses no individual
serial. It is, however, derived identically for every Moonbase-powered product. The material
contains no product- or account-specific input, so the same machine yields the same device id
everywhere, and merchants receive that string through the integration API and webhooks. Treat it as
a stable cross-vendor machine identifier. That is more than machine-id(5) intends, which asks that
the Linux machine id only leave the host through an application-specific keyed derivation. If that
matters for your deployment, supply a custom IDeviceIdResolver that mixes in a key of your own.
The lower-level Fingerprint.BuildMaterial, Fingerprint.Digest, Fingerprint.DeviceId and
Fingerprint.ParseStamp helpers are public so you can verify cross-SDK parity against the shipped
vector file.
To see what this machine reports, without writing any code:
dotnet run --project tools/Moonbase.DeviceId
That is the same harness the repository's parity CI runs on Linux, macOS and Windows, comparing its output against the published Node SDK on each. The vectors prove the algorithm; that job is what proves the platform reads.
Migrating from 3.x to 4.0
Device ids computed by 3.x and earlier are not compatible with the spec, so by default every device must re-activate once after you upgrade. That is not free: a new device id is a new activation, so it consumes a fresh seat (the old one is not reclaimed), and any trial gating that keys on the device signature resets. On a license with few seats, a fleet-wide upgrade can exhaust them immediately, which reaches your customers as "Out of activations".
Three options, in increasing order of effort:
1. Let devices re-activate (default). Simplest, and the id is correct from then on. Catch
LicenseDeviceMismatchException and call Client.RequestActivation(). Best when seats are generous
or the install base is small. Budget for a support spike.
2. Accept the old id while binding the new one (recommended for existing fleets).
MigratingDeviceIdResolver keeps recognising ids this device was previously bound to, without ever
issuing one:
using Moonbase.Net;
using Moonbase.Net.Device;
var licensing = LicensingFactory.CreateInstance(opts =>
{
// ...
opts.DeviceIdResolver = new MigratingDeviceIdResolver(
new MoonbaseDeviceIdResolver(), // always what a new activation binds
new LegacyHardwareDeviceIdResolver()); // additionally accepted during validation
});
Existing licenses keep validating untouched, while anything newly activated binds the current fingerprint. The fleet migrates as devices naturally re-activate, with no flag day and no seat churn. The legacy id is computed lazily, only when the fast comparison fails, and then memoized, so applications on the happy path pay nothing. Drop the wrapper in a later release to finish the migration.
The legacy resolvers are marked obsolete, so this produces a compiler warning. That is deliberate: the warning is the reminder to drop the wrapper once the fleet has moved.
If your install base spans several generations, pass them all. The historical resolvers are a
params list, tried in order:
opts.DeviceIdResolver = new MigratingDeviceIdResolver(
new MoonbaseDeviceIdResolver(),
new LegacyHardwareDeviceIdResolver(), // Moonbase.Net 2.0 - 3.x
new LegacyDeviceIdResolver()); // Moonbase.Net 1.x
3. Stay on the old id. Pin opts.DeviceIdResolver = new LegacyHardwareDeviceIdResolver();.
Nothing changes, but you keep the old algorithm's defects and get no cross-SDK compatibility. Use
this only as a short-term hold.
Options 1 and 2 both recompute every accepted id from the machine's own hardware on each process. Nothing about a device binding is ever read from disk, so widening what a validator accepts does not widen what an attacker can assert.
Coming from 1.x? LegacyDeviceIdResolver is unchanged in both name and behaviour: it is still
the 1.x fingerprint, which included the machine name. The resolver that computes the 2.x and 3.x
fingerprint is LegacyHardwareDeviceIdResolver, which was internal before 4.0.0 and is now public
so it can be used for migration.
Custom stores and device resolvers
You can inject your own ILicenseStore and IDeviceIdResolver implementations via the
LicenseStore and DeviceIdResolver options. A custom resolver's id is compared literally, so it
does not need to follow the mbd2_ stamp format.
Implement IMigratingDeviceIdResolver if a custom resolver should also vouch for ids the device was
bound to under an earlier scheme; ValidateLicense consults it only after the direct comparison
fails.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- BouncyCastle.Cryptography (>= 2.4.0)
- DeviceId (>= 6.6.0)
- DeviceId.Linux (>= 6.4.0)
- DeviceId.Mac (>= 6.3.0)
- DeviceId.Windows (>= 6.6.0)
- DeviceId.Windows.WmiLight (>= 6.6.0)
- Microsoft.Extensions.DependencyInjection (>= 6.0.2)
- Microsoft.Extensions.Http (>= 6.0.0)
- Microsoft.Extensions.Options (>= 6.0.1)
- System.IdentityModel.Tokens.Jwt (>= 6.36.0)
- System.Text.Json (>= 8.0.6)
-
.NETStandard 2.1
- BouncyCastle.Cryptography (>= 2.4.0)
- DeviceId (>= 6.6.0)
- DeviceId.Linux (>= 6.4.0)
- DeviceId.Mac (>= 6.3.0)
- DeviceId.Windows (>= 6.6.0)
- DeviceId.Windows.WmiLight (>= 6.6.0)
- Microsoft.Extensions.DependencyInjection (>= 6.0.2)
- Microsoft.Extensions.Http (>= 6.0.0)
- Microsoft.Extensions.Options (>= 6.0.1)
- System.IdentityModel.Tokens.Jwt (>= 6.36.0)
- System.Text.Json (>= 8.0.6)
-
net6.0
- DeviceId (>= 6.6.0)
- DeviceId.Linux (>= 6.4.0)
- DeviceId.Mac (>= 6.3.0)
- DeviceId.Windows (>= 6.6.0)
- DeviceId.Windows.WmiLight (>= 6.6.0)
- Microsoft.Extensions.DependencyInjection (>= 6.0.2)
- Microsoft.Extensions.Http (>= 6.0.0)
- Microsoft.Extensions.Options (>= 6.0.1)
- System.IdentityModel.Tokens.Jwt (>= 6.36.0)
- System.Text.Json (>= 8.0.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.2.0 | 48 | 10/12/2026 |
| 4.1.0 | 133 | 8/3/2026 |
| 4.0.0 | 124 | 7/30/2026 |
| 3.0.0 | 599 | 5/28/2026 |
| 2.0.0 | 654 | 4/5/2026 |
| 1.1.2 | 305 | 4/24/2025 |
| 1.1.1 | 194 | 1/8/2025 |
| 1.1.0 | 218 | 1/3/2025 |
| 1.0.5 | 220 | 10/3/2024 |
| 1.0.4 | 212 | 10/3/2024 |
| 1.0.3 | 206 | 10/3/2024 |
| 1.0.2 | 235 | 7/3/2024 |
| 1.0.1 | 209 | 6/22/2024 |
| 1.0.0 | 240 | 1/27/2024 |
### Minor Changes
- `RequestActivation` gained an overload taking an `ActivationMethod`, so the browser-based activation request can now ask for an offline license with `RequestActivation(ActivationMethod.Offline)`; existing calls are unchanged. The product has to allow offline activations and the license that fulfils the request needs an offline-capable seat, and fulfilling it with a trial still gives an online license. ([#241](https://github.com/Manevann/moonbase/pull/241))
- A store that has closed now throws `StoreClosedException` from every licensing call that reaches Moonbase, where it used to surface as a `LicenseInvalidException` or an `HttpRequestException`, or leave `GetRequestedActivation` polling forever. It is deliberately not a `LicenseInvalidException`: treat it like being offline, keep the stored license and validate it locally, as the README's "Validate on startup" shows, while devices already activated get a signed offline license and keep working. A `catch (LicenseInvalidException)` no longer sees a closed store, so catch `StoreClosedException` where you catch failures to reach Moonbase. ([#239](https://github.com/Manevann/moonbase/pull/239))
### Patch Changes
- `ErrorType` gains `CaptchaRequired`. A store can now require a captcha on sign-up, which an app cannot show, so for such a store `Register` throws a `UserAuthenticationException` saying so: send the customer to the store's sign-up page in a browser instead. ([#193](https://github.com/Manevann/moonbase/pull/193))