Updates verkaufen: wie eine Lizenz entscheidet, welche Versionen sie ausführen darf
Eine unbefristete Lizenz gilt für immer; „ein Jahr Updates" nicht — und beide stecken im selben Schlüssel. Release-Abdeckung ist die Art, wie eine Lizenz erklärt, welche Versionen sie ausführen darf: als signierte Versionsbereichs- und Zeitfenster-Grants, die der Build lokal, offline und ohne Reaktivierung prüft. Hier erfährst du, wie ein Abdeckungs-Grant geformt ist, wie das Berechtigungsdatum (nicht das Installationsdatum) über einen Hotfix entscheidet, wie der signierte Block mit dem Lease mitreist und wie dein Build ihn ohne einen einzigen Netzwerkaufruf auf dem heißen Pfad durchsetzt.
Eine unbefristete Lizenz und ein Wartungsfenster sind unterschiedliche Versprechen, die Kunden routinemäßig als ein einziges SKU kaufen. „Unbefristet, mit einem Jahr Updates” bedeutet, dass die Lizenz für immer gültig ist, aber nur zu den im ersten Jahr geschnittenen Releases berechtigt. Kodiere das als Ablauf und du hast das Unbefristet-Versprechen gebrochen — die App hört nach einem Jahr auf zu laufen. Ignoriere es und du hast jedes künftige Release für eine Einmalzahlung verschenkt.
Keyright trennt die beiden. Gültigkeit — Signatur, Sitz, Ablauf, Widerruf — beantwortet ist diese Lizenz gut? Die Release-Abdeckung beantwortet eine andere Frage: darf diese Lizenz diese Version ausführen? Abdeckung ist eine Menge signierter Grants, die an eine Lizenz angehängt sind, lokal in deinem Build gegen die Version ausgewertet, die er ist — ohne Reaktivierung und ohne Netzwerk auf dem heißen Pfad. Dieser Beitrag zeigt, wie diese Mechanik geformt ist und wie du sie in einen .NET-Build einbaust.
Ein Grant ist ein Versionsbereich, ein Zeitfenster, oder beides
Ein Abdeckungs-Grant ist die kleinste Einheit von „du bist zu diesen Releases berechtigt”. Er kann nach Version, nach Datum oder nach beidem zugleich einschränken:
- Versionsbereich —
[2.0.0, 3.0.0): jedes 2.x-Release, nichts ab 3.0. Das ist „eine Hauptversion”. - Zeitfenster — Releases, die zwischen zwei Daten geschnitten wurden. Das ist „ein Jahr Updates”, ausgedrückt als die Releases, deren Berechtigungsdatum ins Fenster fällt.
- Beides — ein Bereich und ein Fenster, geschnitten: „2.x-Releases, geschnitten in deinem ersten Jahr”.
Du fügst selten rohe Grants von Hand hinzu. Ein Preset erweitert einen klartextlichen Begriff zur richtigen Grant-Form — lifetime, updates-for-period, one-major, one-major-plus-upgrade-protection, subscription, subscription-with-fallback — sodass aus „ein Jahr Updates ab Kauf” ein am Kaufdatum verankerter Zeitfenster-Grant wird, ohne dass du irgendetwas berechnest.
# Apply a preset to a license; it expands into the concrete grants.
curl -X POST "$BASE/admin/licenses/$ID/coverage/preset" \
-H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"preset":"updates-for-period","months":12,"from":"2026-10-10"}'
Das Berechtigungsdatum ist der ganze Trick
Der subtile Teil eines Zeitfensters ist, welches Datum ein Release misst. Es ist nicht das Installationsdatum des Kunden, nicht die Systemuhr und nicht der Dateizeitstempel. Jedes Release, das du registrierst, trägt ein Berechtigungsdatum — das Datum, gegen das die Abdeckung beurteilt wird — und ein Zeitfenster-Grant deckt ein Release ab, wenn dieses Berechtigungsdatum ins Fenster fällt.
# Register each release so coverage has a date to judge it by.
curl -X POST "$BASE/admin/products/acme-app/releases" \
-H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"version":"3.1.0","entitlementDate":"2026-09-01","channel":"stable"}'
Das ist vor allem bei Hotfixes wichtig. Wenn das Update-Fenster eines Kunden am 1. August schloss und du im Oktober 3.1.4 veröffentlichst, um einen Fehler in 3.1.0 zu beheben (das in seinem Fenster lag), würde 3.1.4 nach seinem eigenen Schnittdatum zu beurteilen es aus dem Fenster schieben und ihn von einer Behebung aussperren, zu der er offensichtlich berechtigt ist. Ein Hotfix erbt stattdessen das Berechtigungsdatum seines Basis-Releases, sodass 3.1.4 genau wie 3.1.0 beurteilt wird. Die Regel: Kalenderzeit auf der Maschine des Kunden spielt nie eine Rolle — nur das Berechtigungsdatum des Releases.
Der signierte Block reist mit dem Lease
Abdeckung ist nur vertrauenswürdig, wenn der Build sich nicht selbst darüber belügen kann, daher werden Grants in einen Abdeckungsblock signiert — mit demselben Mandantenschlüssel, der die Lizenz signiert, offline prüfbar gegen den in dein Binary kompilierten öffentlichen Schlüssel. Die Aktivierung liefert diesen Block und cacht ihn neben dem Lease, sodass zu dem Zeitpunkt, an dem deine App fragt „bin ich abgedeckt?”, die Antwort bereits auf der Platte liegt. Kein separater Abruf, kein Abdeckungsserver, kein Netzwerk auf dem Pfad, der den Start steuert.
Durchsetzung in einem .NET-Build
Das Paket Keyright.NET bringt MSBuild-Targets mit, die die Identität dieses Builds in die Assembly stempeln. Setze sie aus deiner Release-Pipeline, damit das ausgelieferte Binary weiß, welche Version es ist und gegen welches Datum es beurteilt werden soll:
<PropertyGroup>
<KeyrightCoverageVersion>3.1.0</KeyrightCoverageVersion> <!-- this build -->
<KeyrightCoverageEntitlementDate>2026-09-01</KeyrightCoverageEntitlementDate> <!-- base release's date for a hotfix -->
<KeyrightCoverageChannel>stable</KeyrightCoverageChannel> <!-- or beta -->
<KeyrightRequireCoverage>true</KeyrightRequireCoverage> <!-- enforce; build fails (KR0001) if the date is missing -->
</PropertyGroup>
Beim Start fragst du einmal. Das SDK liest den gecachten Block, entscheidet lokal, aktualisiert still, wenn die gecachte Antwort „nein” ist (die Abdeckung könnte seit der Aktivierung erweitert worden sein), und wirft nie eine Ausnahme bei Netzwerkproblemen:
using Keyright.Coverage;
if (CoverageBuildInfo.Current.RequireCoverage)
{
CoverageCheckResult check = await License.CheckCoverageAsync(ct: ct);
if (!check.IsCovered) // Covered and NotConfigured both pass
{
// check.Status: NotCovered (Reason = WindowLapsed → renew,
// OutsideVersionRange → upgrade entitlement)
// or UnavailableOffline (nothing cached, no network → import a coverage file)
RunReadOnlyOrBlock(check);
}
}
// For your own "update available" banner — local, no network, no gating:
bool canTake31 = License.IsCovered("3.1.0");
CoverageInfo cov = License.GetCoverage(); // the terms + support-until date
Dass NotConfigured durchgeht, ist beabsichtigt: ein Produkt, das nie Abdeckung eingerichtet hat, muss weiterlaufen, daher ist die Durchsetzung pro Build optional und eine Lizenz ohne Grants wird nie versehentlich ausgesperrt.
Durchsetzung aktivieren, ohne die auszusperren, die bereits bezahlt haben
Der einzige gefährliche Moment ist das erste Mal, dass du bei einem Produkt durchsetzt, das seit Jahren unbefristete Lizenzen verkauft: keine trägt einen Grant, sodass ein naiver Umschalter jeden bestehenden Kunden abweisen würde. Keyright koppelt das an zwei Schritte:
# 1. How many licenses still have no coverage? This must reach zero first.
curl "$BASE/admin/products/acme-app/coverage/readiness" -H "X-Admin-Token: $TOKEN"
# 2. Give every legacy license a grant so enforcing is a no-op for them.
curl -X POST "$BASE/admin/products/acme-app/coverage/backfill" \
-H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"grant":"lifetime"}' # or a support-end date, or CSV rows
Der Backfill gibt bestehenden Lizenzen einen legacy-Grant — lebenslang, ein Support-Enddatum oder explizite Zeilen pro Lizenz aus einer CSV — sodass die Durchsetzung, einmal aktiviert, für niemanden etwas ändert, der gekauft hat, bevor du Abdeckung hattest. Du setzt erst durch, nachdem die Bereitschaft null nicht abgedeckte Lizenzen meldet. Ein migrate-Endpunkt mit dryRun zeigt den Gewinn vorab, bevor du einen Grant über ein ganzes Produkt anwendest.
Abgeschottet: dieselbe Antwort, nie Netzwerk
Eine Maschine, die den Dienst nie erreichen kann, setzt die Abdeckung trotzdem durch, weil die Auswertung ohnehin lokal ist. Exportiere den signierten Abdeckungsblock aus dem Dashboard und importiere ihn auf der Offline-Maschine:
License.ImportCoverageBlock(File.ReadAllText("acme.coverage.json"));
CoverageCheckResult check = await License.CheckCoverageAsync(ct: ct); // fully offline
Der Import ist der einzige Abdeckungsschritt, den ein abgeschotteter Kunde ausführt, und er trägt seine eigene Signatur: ein manipulierter Block scheitert an der Prüfung und löst sich zu nicht abgedeckt auf, nicht zu einem Freifahrtschein.
Was mitzunehmen ist
Halte Gültigkeit und Abdeckung getrennt: die eine sagt, dass die Lizenz gut ist, die andere, welche Versionen sie ausführen darf — sie zu vermischen bricht entweder ein unbefristetes Versprechen oder verschenkt die künftigen Releases. Modelliere die Abdeckung als signierte Grants — einen Versionsbereich, ein Zeitfenster, oder beides —, beurteile jedes Release nach seinem Berechtigungsdatum statt nach irgendeiner Uhr auf der Maschine des Kunden, lass Hotfixes das Datum ihres Basis-Releases erben und liefere den signierten Block mit dem Lease, damit der Build lokal und offline entscheidet. Bevor du durchsetzt, mache einen Backfill der Lizenzen, die der Abdeckung vorausgehen, damit der Umschalter deine bestehenden Kunden nichts kostet. Der Lohn ist das, was unbefristet-plus-Wartung immer sein wollte: eine App, die für immer weiterläuft, und ein Update-Strom, der genau dort endet, wo die Berechtigung des Kunden endet — entschieden im Binary, ohne Reaktivierung und ohne irgendetwas auf dem heißen Pfad.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.