Eine .NET-Komponentenbibliothek lizenzieren: die Entwicklerlizenz, der Schlüssel, der in die App Ihres Kunden kompiliert wird, und was der Endanwender nie sieht
Eine Komponentenbibliothek hat zwei Zielgruppen, und nur eine davon ist Ihr Kunde. Der Entwickler, der Ihre PDF-Engine oder Ihr Diagramm-Steuerelement kauft, hält die Lizenz; die Endanwender, die die damit gebauten Apps ausführen, haben nie von Ihnen gehört, dürfen nie nach einem Schlüssel gefragt werden und arbeiten auf Maschinen, die Sie nicht aktivieren können. Das ändert, wo eine Lizenz geprüft wird und was in der ausgelieferten Binärdatei mitreist. Dieser Beitrag baut mit Keyright das Design, auf das die meisten Hersteller von Steuerelementen hinauslaufen: eine unbefristete, nicht an eine Maschine gebundene Entwicklerlizenz, die zur Build-Zeit von einem MSBuild-Target in Ihrem NuGet-Paket verifiziert, als signiertes Attribut in die Assembly des Kunden eingebettet und zur Laufzeit offline erneut gegen den öffentlichen Schlüssel in Ihrer Bibliothek verifiziert wird — mit einer updates-until-Berechtigung statt eines Ablaufdatums, damit die App des Kunden nie stehen bleibt, einem Audience-Claim, damit eine aus einer App herausgelöste Lizenz keine andere freischaltet, und einem als Wasserzeichen tauglichen Lizenznehmernamen, der in jede Binärdatei signiert ist, die sie verwendet.
Die meisten Ratschläge zur Lizenzierung gehen davon aus, dass die Person, die Ihren Code ausführt, dieselbe ist, die dafür bezahlt hat. Eine Komponentenbibliothek bricht diese Annahme entzwei. Sie verkaufen eine PDF-Engine, ein Diagramm-Steuerelement, ein Barcode-SDK an einen Entwickler. Der Entwickler kompiliert es in eine Anwendung und liefert diese Anwendung an Endanwender aus — Menschen, die nie von Ihnen gehört haben, nicht nach einem Schlüssel gefragt werden können und auf Maschinen arbeiten, die Sie nie aktivieren werden. Ihr Kunde ist der Entwickler. Ihr Code läuft überall sonst.
Diese Trennung ändert zwei Dinge: wo die Lizenz geprüft wird und was in der Binärdatei mitreisen muss, die Ihr Kunde ausliefert. Dieser Beitrag baut mit Keyright das Design, auf das die meisten Hersteller von Steuerelementen hinauslaufen — eine Entwicklerlizenz, die zur Build-Zeit verifiziert, in die Assembly des Kunden eingebettet und zur Laufzeit offline erneut verifiziert wird —, und geht die drei Stellen durch, an denen die naheliegende Variante davon schiefgeht.
Zwei Parteien, zwei Prüfungen
Die Herstellerseite ist vertraut: Sie stellen eine Lizenz aus, der Entwickler aktiviert sie, Sie zählen Plätze. Der unvertraute Teil ist der zweite Sprung. Was immer Ihre Bibliothek zur Laufzeit braucht, um „lizenziert oder Evaluierung“ zu entscheiden, muss innerhalb der Anwendung des Kunden liegen, zur Build-Zeit dort platziert und auf einer Maschine verifizierbar, die keine Lizenzdatei, keine Umgebungsvariable, kein Netzwerk und keine Ahnung hat, wer Sie sind. Das Lizenzdokument selbst ist das Richtige, um es dort abzulegen — es ist eine signierte Nutzlast, kein Geheimnis —, aber nur, wenn es in einer Form ausgestellt wird, die die Reise übersteht. Diese Form ist das Erste, was richtig sein muss.
Die Entwicklerlizenz in einer Form ausstellen, die reisen kann
Eine Keyright-Lizenz ist ein kleines signiertes JSON-Dokument: Lizenznehmer, Stufe, Ablauf, Produkt, Berechtigungen, Signatur. Für eine Desktop-App würden Sie sie typischerweise an eine Maschine gebunden und, bei einem Abonnement, mit Ablaufdatum ausstellen. Für eine Komponentenbibliothek ist beides falsch, denn das Dokument wird in eine App kopiert, die auf Maschinen läuft, von denen die Lizenz nichts weiß.
Stellen Sie sie stattdessen so aus:
{
"licensee": "Contoso Ltd",
"expiryUtc": null,
"tier": "Pro",
"trial": false,
"product": "acme-pdf",
"licenseId": "lic_8f3a2c71",
"entitlements": {
"redistribute": "true",
"developers": "5",
"updates-until": "20271015",
"audience": "Contoso.Reports;Contoso.Reports.Desktop"
},
"signature": "…"
}
Vier Entscheidungen sind darin kodiert:
expiryUtcist null. Die Lizenz ist unbefristet. Wenn Sie „ein Jahr Updates“ verkauft haben, ist das kein Ablaufdatum — ein Ablaufdatum würde jede App, die Contoso je ausgeliefert hat, an dem Tag abschalten, an dem die Wartung ausläuft. Das Wartungsfenster ist die Berechtigungupdates-until, und die Bibliothek vergleicht sie mit ihrem eigenen Veröffentlichungsdatum, nicht mit der Wanduhr. Ein 2027er Build Ihrer Bibliothek prüft, dassupdates-until≥ seinem Build-Datum ist; ein 2028er Build weist dieselbe Lizenz zur Build-Zeit zurück, direkt vor den Augen des Entwicklers, und genau dort gehört diese Meldung hin. (Keyrights Release-Abdeckung formalisiert genau das für Anwendungen; für eine Bibliothek ist eine Berechtigung, die die Bibliothek selbst liest, das Einfachste, das überall funktioniert.)- Keine
machineId. Das Dokument ist nicht an eine Maschine gebunden; es muss auf der Maschine jedes Endanwenders validieren. Entwicklerplätze werden auf der Seite des Entwicklers über die Aktivierung durchgesetzt — jeder Entwickler aktiviert den Schlüssel, der Ausstellungsdienst zähltdevelopersPlätze, und das signierte Lease, das das SDK cacht, ist das, was an seine Maschine gebunden ist. Das Lease wird nie ausgeliefert. redistributeist ein explizites Flag. Dasselbe Produkt kann auch eine Lizenz für den internen Gebrauch an ein Team verkaufen, das Geschäftsanwendungen für die eigenen Mitarbeiter baut; diese bekommtredistribute: false, und das Build-Target weigert sich, sie einzubetten. Das Flag explizit zu machen schlägt das Ableiten aus der Stufe.audiencelistet die Assemblys auf, für die die Lizenz verkauft wurde. Das ist die Maßnahme gegen Kopieren, weiter unten behandelt.
Zur Build-Zeit prüfen, aus Ihrem NuGet-Paket heraus
Der Build des Entwicklers ist der Ort, an dem Sie noch mit Ihrem Kunden sprechen können. Ihr NuGet-Paket liefert eine build/Acme.Pdf.targets mit, die MSBuild automatisch in jedes Projekt importiert, das darauf verweist; das Target läuft vor CoreCompile, validiert die Lizenz des Entwicklers und generiert das Attribut, nach dem die Laufzeit suchen wird.
<!-- build/Acme.Pdf.targets — imported automatically by the PackageReference -->
<Project>
<PropertyGroup>
<!-- Optional explicit path; otherwise ACME_PDF_LICENSE env var, then the default app-data path. -->
<AcmePdfLicense Condition="'$(AcmePdfLicense)' == ''"></AcmePdfLicense>
<!-- Set true on CI to turn 'no valid license' into a build error instead of an evaluation build. -->
<AcmePdfRequireLicense Condition="'$(AcmePdfRequireLicense)' == ''">false</AcmePdfRequireLicense>
<_AcmePdfLicenseFile>$(IntermediateOutputPath)AcmePdfLicense.g.cs</_AcmePdfLicenseFile>
</PropertyGroup>
<UsingTask TaskName="Acme.Pdf.Build.EmbedLicense"
AssemblyFile="$(MSBuildThisFileDirectory)..\tools\Acme.Pdf.Build.dll" />
<Target Name="AcmePdfEmbedLicense" BeforeTargets="CoreCompile"
Inputs="$(MSBuildProjectFullPath)" Outputs="$(_AcmePdfLicenseFile)">
<EmbedLicense LicensePath="$(AcmePdfLicense)"
AssemblyName="$(AssemblyName)"
RequireLicense="$(AcmePdfRequireLicense)"
OutputFile="$(_AcmePdfLicenseFile)" />
<ItemGroup>
<Compile Include="$(_AcmePdfLicenseFile)" />
<FileWrites Include="$(_AcmePdfLicenseFile)" />
</ItemGroup>
</Target>
</Project>
Der Task selbst ist ein dünner Wrapper um das Keyright SDK. Er löst die Lizenz über die normale Rangfolge des SDK auf — ein expliziter Pfad gewinnt, dann eine Umgebungsvariable, dann der benutzerspezifische Standard —, validiert sie offline, wendet die bibliotheksspezifischen Regeln an und schreibt den generierten Quelltext:
public sealed class EmbedLicense : Microsoft.Build.Utilities.Task
{
public string? LicensePath { get; set; }
[Required] public string AssemblyName { get; set; } = "";
public bool RequireLicense { get; set; }
[Required] public string OutputFile { get; set; } = "";
// The release date of THIS SDK build, stamped at pack time. Compared against updates-until.
const long SdkReleaseDate = 20261015;
public override bool Execute()
{
var client = KeyrightClient.Initialize(new KeyrightOptions
{
Product = "acme-pdf",
PublicKeyBase64 = AcmePublicKey.Base64, // public key only; safe in a NuGet
LicenseFilePath = string.IsNullOrEmpty(LicensePath) ? null : LicensePath,
EnvVarName = "ACME_PDF_LICENSE", // not the shared KEYRIGHT_LICENSE
});
LicenseInfo info = client.Validate(out LicenseSourceKind source); // offline; never throws
string? problem = Diagnose(info);
if (problem is not null)
{
if (RequireLicense) { Log.LogError("ACMEPDF001: {0}", problem); return false; }
Log.LogWarning("ACMEPDF002: {0} Building in evaluation mode (output is watermarked).", problem);
File.WriteAllText(OutputFile, "// Acme.Pdf: no license embedded (evaluation build)\n");
return true;
}
// Embed the signed document verbatim. It is public data; the signature is the protection.
string json = File.ReadAllText(ResolvedPath(client, source)); // the file `source` resolved to
string b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
File.WriteAllText(OutputFile,
$"[assembly: Acme.Pdf.Licensing.AcmePdfLicense(\"{b64}\")]\n");
Log.LogMessage(MessageImportance.High,
"Acme.Pdf: licensed to {0} ({1}); embedded into {2}.", info.Licensee, info.LicenseId, AssemblyName);
return true;
}
string? Diagnose(LicenseInfo info)
{
if (!info.IsValid) return $"No valid Acme.Pdf developer license ({info.Status}: {info.Message}).";
if (info.IsTrial) return "Trial licenses cannot be embedded in a redistributable build.";
if (info.ExpiryUtc.HasValue) return "Only perpetual developer licenses can be embedded; this one expires.";
if (!info.Entitlements.IsEnabled("redistribute"))
return "This license does not include redistribution rights.";
if (info.Entitlements.GetLimit("updates-until") < SdkReleaseDate)
return $"This license covers updates until {info.Entitlements.GetRaw("updates-until")}; this SDK build is newer.";
var audience = (info.Entitlements.GetRaw("audience") ?? "").Split(';', StringSplitOptions.RemoveEmptyEntries);
if (audience.Length > 0 && !audience.Contains(AssemblyName, StringComparer.OrdinalIgnoreCase))
return $"This license is issued for {string.Join(", ", audience)}, not '{AssemblyName}'.";
return null;
}
}
Drei Details tragen Gewicht. EnvVarName ist auf eine produktspezifische Variable gesetzt statt auf den SDK-Standard KEYRIGHT_LICENSE, denn ein Build-Agent kann Lizenzen für mehrere Keyright-lizenzierte Produkte halten, und Sie wollen nicht, dass Ihre aus der Datei eines anderen aufgelöst wird. Validate(out source) teilt dem Task mit, welche Quelle gewonnen hat, und das macht aus „bei mir funktioniert es, in der Pipeline nicht“ eine Einzeiler-Diagnose — das Muster ist dasselbe, das eine headless CLI verwendet. Und Diagnose weist zur Build-Zeit jede Form von Lizenz zurück, die später auf der Maschine eines Endanwenders scheitern würde: Testlizenzen, alles mit Ablaufdatum, alles ohne Weiterverteilungsrechte, alles, wofür der SDK-Build zu neu ist. Jede dieser Meldungen ist eine, auf die der Entwickler reagieren kann. Keine davon ist eine, die Sie je einem Endanwender zustellen könnten.
Was eingebettet wird und warum das sicher ist
Die generierte Datei ist eine Zeile:
[assembly: Acme.Pdf.Licensing.AcmePdfLicense("eyJsaWNlbnNlZSI6ICJDb250b3NvIEx0ZCIsIC…")]
Dieser String ist das signierte Lizenzdokument des Entwicklers, base64-kodiert, und sitzt in den Assembly-Metadaten des Kunden, wo jeder mit einem Decompiler es lesen kann. Das ist in Ordnung, und es lohnt sich, genau zu sagen, warum. Eine Keyright-Lizenz ist durch ihre Signatur geschützt, nicht durch Geheimhaltung — das SDK verifiziert die Signatur bei jeder Validierung erneut mit dem in Ihre Bibliothek kompilierten öffentlichen Schlüssel. Nichts in dem Dokument erlaubt es irgendjemandem, eine weitere Lizenz auszustellen, und ein einziges geändertes Byte macht die Signatur ungültig. Dass Lizenznehmername und Lizenz-ID in der Binärdatei lesbar sind, ist keine Schwäche; es ist ein Wasserzeichen, das Sie gratis bekommen, und es ist das, was Ihnen erlaubt, die Frage „wessen Lizenz steckt in dieser gecrackten App?“ allein aus der Binärdatei zu beantworten.
Der Attributtyp lebt in Ihrer Bibliothek, ist trivial und muss die Obfuskierung namentlich überstehen — die Laufzeit schlägt ihn per Reflection nach:
namespace Acme.Pdf.Licensing;
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class AcmePdfLicenseAttribute : Attribute
{
public AcmePdfLicenseAttribute(string licenseBase64) => LicenseBase64 = licenseBase64;
public string LicenseBase64 { get; }
}
Die Laufzeitprüfung: Attribut finden, offline verifizieren, nichts schreiben
Innerhalb der Bibliothek läuft das Gate einmal, verzögert, beim ersten Mal, wenn etwas Lizenziertes verwendet wird. Es muss das Attribut auf der Assembly finden, die gegen Sie kompiliert wurde, und das ist nicht immer die Einstiegs-Assembly — Ihr Steuerelement kann in einem Plugin, einer Klassenbibliothek oder einem Test-Host stecken —, also durchsucht es die geladenen Assemblys, statt etwas anzunehmen:
internal static class LicenseGate
{
static readonly Lazy<LicenseInfo> Current = new(Resolve, LazyThreadSafetyMode.ExecutionAndPublication);
public static bool IsLicensed => Current.Value.IsValid;
public static string Licensee => Current.Value.Licensee;
static LicenseInfo Resolve()
{
// Find the first assembly carrying our attribute: usually the entry assembly, but a
// plugin host or test runner may put the licensed consumer further down the list.
var carrier = AppDomain.CurrentDomain.GetAssemblies()
.Select(a => (asm: a, attr: a.GetCustomAttribute<AcmePdfLicenseAttribute>()))
.FirstOrDefault(t => t.attr is not null);
if (carrier.attr is null)
return LicenseInfo.Free(LicenseStatus.NoLicense, "No Acme.Pdf license embedded in the host application.");
string json = Encoding.UTF8.GetString(Convert.FromBase64String(carrier.attr.LicenseBase64));
var client = KeyrightClient.Initialize(new KeyrightOptions
{
Product = "acme-pdf",
PublicKeyBase64 = AcmePublicKey.Base64, // same public key the build task used
LicenseString = json, // authoritative: no env var, no file, no lease lookup
EnvVarName = "", // belt and braces: never consult the end user's environment
});
LicenseInfo info = client.Validate(); // signature + product + entitlements, offline, never throws
if (!info.IsValid) return info;
// The same rules the build enforced, re-checked where the code actually runs.
if (!info.Entitlements.IsEnabled("redistribute"))
return LicenseInfo.Free(LicenseStatus.NoLicense, "License does not include redistribution.");
if (info.Entitlements.GetLimit("updates-until") < BuildStamp.SdkReleaseDate)
return LicenseInfo.Free(LicenseStatus.Expired, "License does not cover this Acme.Pdf release.");
var audience = (info.Entitlements.GetRaw("audience") ?? "").Split(';', StringSplitOptions.RemoveEmptyEntries);
if (audience.Length > 0 && !audience.Contains(carrier.asm.GetName().Name, StringComparer.OrdinalIgnoreCase))
return LicenseInfo.Free(LicenseStatus.NoLicense, "License audience does not match the host assembly.");
return info;
}
}
LicenseString ist das Detail, das das korrekt macht. Im Keyright SDK ist ein expliziter Lizenz-String maßgeblich: Wenn er gesetzt ist, validiert der Client dieses Dokument und sonst nichts — er schaut weder auf Umgebungsvariablen noch auf Konfigurationspfade, den Standardort in den App-Daten oder ein gecachtes Lease. Auf der Maschine eines Endanwenders wäre es falsch, irgendetwas davon zu konsultieren; dort gibt es keine Datei, und wenn es zufällig eine gäbe (der Endanwender ist selbst Entwickler mit irgendeinem anderen Keyright-lizenzierten Produkt), darf sie nicht aufgegriffen werden. Der explizite String schließt all das kurz.
Und weil die eingebettete Lizenz unbefristet ist, schreibt das SDK nichts auf die Festplatte des Endanwenders. Keyright hält lokalen Zustand — First-seen- und Last-seen-Zeitstempel für die Testdauer und die Erkennung von Uhr-Rückstellungen — nur für zeitgebundene Lizenzen. Ein unbefristetes Dokument wird rein aus seiner Signatur und seinem Inhalt validiert, es gibt also keine state.json, keinen Lease-Cache, keinen Ordner mit dem Namen Ihres Produkts in den App-Daten des Benutzers. Das ist der richtige Fußabdruck für Code, der in der Anwendung eines anderen läuft.
Was die Bibliothek mit IsLicensed == false macht, ist eine Produktentscheidung, und für eine Komponente lautet die Antwort fast immer Evaluierungsmodus statt Verweigerung: das Diagramm mit einem diagonalen „Evaluation“-Wasserzeichen rendern, die PDF-Fußzeile stempeln, die Barcode-Anzahl deckeln. Ein Entwickler, der Sie evaluiert, sollte Ihre Bibliothek funktionieren sehen. Ein Endanwender, dessen Anbieter einen unlizenzierten Build ausgeliefert hat, sollte etwas sehen, das den Anbieter dazu bringt, es zu beheben — was ein Wasserzeichen tut, und eine Ausnahme beim Start auch, aber bei dessen Kunden statt bei Ihren.
Die drei Arten, auf die das naheliegende Design scheitert
Die ablaufende Lizenz. Der häufigste Fehler ist, Entwicklerlizenzen genau wie App-Lizenzen auszustellen — ein Zwölfmonatsabonnement mit gesetztem expiryUtc — und sie einzubetten. Ein Jahr lang funktioniert das perfekt. Dann läuft die Verlängerung des Entwicklers aus oder kommt nur verspätet, und jede Anwendung, die er je ausgeliefert hat, fällt gleichzeitig auf den Maschinen seiner Kunden in den Evaluierungsmodus. Der Build-Task oben weigert sich genau aus diesem Grund, irgendetwas mit Ablaufdatum einzubetten; das Wartungsfenster lebt in updates-until und wird gegen das Veröffentlichungsdatum der Bibliothek durchgesetzt, das sich nach der Auslieferung nicht mehr bewegt.
Die maschinengebundene Lizenz. Ebenso naheliegend, ebenso falsch: die Entwicklerlizenz an die Maschine des Entwicklers binden, damit sie nicht geteilt werden kann. Eingebettet schlägt sie auf dem PC jedes Endanwenders mit MachineMismatch fehl. Das Platzproblem ist real, aber es wird auf der anderen Seite der Grenze gelöst — der Entwickler aktiviert, der Ausstellungsdienst zählt Plätze gegen developers, und das maschinengebundene Artefakt ist das Lease, das das SDK lokal cacht, nie das Lizenzdokument. Wenn Sie die zusätzliche Sicherheit brauchen, kann der Build-Task ein gültiges Lease auf der Maschine des Entwicklers verlangen (source == LicenseSourceKind.Lease) und trotzdem das ungebundene Dokument einbetten.
Die herausgelöste Lizenz. Das Dokument ist in der ausgelieferten Binärdatei lesbar, also kann ein Entwickler, der nicht bezahlt hat, es aus der App eines Konkurrenten ziehen und in seine eigene einfügen. Die Berechtigung audience ist die Antwort: Bei der Ausstellung halten Sie die Assembly-Namen fest, für die die Lizenz verkauft wurde, der Build-Task weigert sich, in irgendeine andere einzubetten, und die Laufzeit prüft den Namen der tragenden Assembly gegen die Liste. Seien Sie ehrlich, was das bringt. Ein Angreifer kann seine Assembly in Contoso.Reports umbenennen und kommt durch. Aber er muss jetzt wissen, dass er das tun muss, sein Produkt wird unter dem Assembly-Namen eines Fremden ausgeliefert, und seine Binärdatei trägt Contosos Lizenznehmernamen und Lizenz-ID, signiert, für Sie zum Finden — das ist die Art von Beweis, mit der ein Lizenzstreit gewonnen wird. Für den Angriff „die Prüfung herauspatchen“ ist das Gate oben gewöhnliches C#, das Nebula schützen kann: autoProtectLicensing findet die Methoden, die das Keyright SDK aufrufen, und gibt ihnen die stärksten Transformationen, die die Edition anwendet, sodass die IsLicensed-Verzweigung kein einzelnes brfalse ist, das man umdreht. Die Lizenzprüfung schützen behandelt diese Seite.
Wenn die Bereitstellung das ist, was Sie lizenzieren
Alles oben ist das Modell pro Entwickler, lizenzgebührenfreie Laufzeit — das, das zu Steuerelementen, SDKs und Engines passt, die an Entwickler verkauft werden, die Desktop- und Mobil-Apps bauen. Es gibt ein zweites Modell für Bibliotheken, die auf Servern laufen: Sie lizenzieren die Bereitstellung, nicht den Entwickler, denn ein einzelner Entwickler kann tausend Instanzen hochfahren, und der Wert skaliert mit den Instanzen.
Dieses Modell lässt das Einbetten vollständig weg. Die Host-Anwendung aktiviert beim Start mit ihrem eigenen Schlüssel — ActivateAsync mit dem Produkt-Namespace des Hosts —, und die Berechtigung Ihres SDK reist in der Lizenz des Hosts als Flag (acme-pdf: true) oder als Limit (acme-pdf-pages-per-day: 50000) mit. Ihre Bibliothek fragt dann die aufgelöste LicenseInfo des Hosts, statt eine eigene zu tragen; Container und automatisch skalierte Dienste lizenzieren ist das Muster. Die beiden Modelle schließen sich nicht aus. Eine Bibliothek, die auf beide Arten verkauft wird, stellt zwei Produkt-Namespaces aus, bettet für die Desktop-SKU ein und liest für die Server-SKU die Berechtigung des Hosts; das Laufzeit-Gate prüft einfach zuerst das eingebettete Attribut und danach die Host-Lizenz.
Die Form, in einem Absatz
Eine Komponentenbibliothek wird an jemanden lizenziert, der nicht da ist, wenn sie läuft. Prüfen Sie die Lizenz also dort, wo der Kunde ist — in seinem Build, aus Ihrem NuGet-Paket heraus, mit Meldungen, auf die ein Entwickler reagieren kann —, und legen Sie das signierte Dokument in die Binärdatei, die er ausliefert, in einer Form, die die Reise übersteht: unbefristet, ungebunden, mit dem Wartungsfenster als Berechtigung, die die Bibliothek mit ihrem eigenen Veröffentlichungsdatum vergleicht, und einer Audience, für die die Lizenz verkauft wurde. Verifizieren Sie dieses Dokument zur Laufzeit offline mit dem öffentlichen Schlüssel, den Sie einkompiliert haben, schreiben Sie nichts auf die Festplatte des Endanwenders, und fallen Sie in einen Evaluierungsmodus zurück, der den Anbieter blamiert, statt dessen Kunden zu bestrafen. Das Lease bleibt zu Hause. Die Lizenz reist. Der Endanwender erfährt nie, dass es Sie gibt, und genau so soll es sein.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.