Protecting a .NET SDK or Class Library You Ship to Customers
Obfuscating a .NET class library or SDK is different: the public API must stay callable while internals are protected. Here is how to exclude the surface and ship safely.
Protecting an app you ship is one problem. Protecting a library you ship — an SDK, a component, a NuGet package other developers build against — is a different one, and the difference trips people up constantly. With an app, you can rename almost everything, because nothing outside calls in by name. With a library, half the point is that people do call in by name. Rename the wrong thing and you haven’t hardened your SDK, you’ve broken every customer’s build.
The trick is a clean split: the public API stays exactly as published, and everything behind it gets protected. This post is how to draw that line in .NET and ship the result safely.
TL;DR
- The public API must keep its exact names, or consumer code won’t compile or bind. Exclude the whole public surface from renaming.
- Protect the internals behind it: rename private/internal members, encrypt strings, flatten internal control flow.
- Reserve method encryption or virtualization for the specific internal methods that are the actual value — proprietary algorithms, licensing logic.
- Also preserve anything resolved by name at runtime: reflection, serialization, DI, public XML docs.
- Test against a real consumer before publishing, and keep the symbol map private per version.
Why a library is different from an app
When you obfuscate an application, the assembly is a closed world. The only entry point is Main, and nothing external references your types by name, so an aggressive obfuscator can rename practically everything and the app still runs. That’s the case our how to protect .NET code from decompilation guide assumes by default.
A library inverts that. Its reason to exist is to be called from code you’ll never see. Every public type name, method name, property name and parameter is part of a contract:
// Your customer's code, compiled against your SDK:
var client = new AcmeSdk.PaymentClient(apiKey);
var result = client.Charge(amount, currency);
If your obfuscator renames PaymentClient to a and Charge to b, that customer’s code no longer compiles — the names it was written against are gone. Worse, if the rename ships in a minor update, you break everyone who upgrades. So the first rule of protecting a library is: the public surface is off-limits to renaming. What you protect is everything the caller doesn’t name — the implementation behind the contract.
Step 1: preserve the public surface
Every serious obfuscator can preserve members by accessibility, and for a library you want the visible surface kept intact. In Nebula.NET this is a preservation/keep rule that excludes public and protected members from renaming while leaving the rest fully renameable. Conceptually:
{
"inputs": ["publish/AcmeSdk.dll"],
"outputDirectory": "protected",
"rename": {
"keepPublicApi": true
},
"encryptStrings": true,
"controlFlow": true
}
keepPublicApi (however your tool spells it) preserves the names a consumer can reference — public and protected types, methods, properties, events and their parameters — so the compiled contract is unchanged. Everything internal, private, or otherwise invisible to a consumer stays renameable, which is exactly where you want the obfuscation to bite.
A subtlety worth calling out: internal members are not part of your public contract unless you’ve exposed them with [InternalsVisibleTo]. If you have — to a sibling assembly or a test project you also ship — treat those as public for preservation purposes, or that sibling breaks.
Step 2: preserve what’s resolved by name at runtime
Accessibility rules catch the compile-time contract, but libraries lean heavily on runtime name resolution, and those names don’t show up as “public” in the obvious way. The usual suspects:
- Serialization — if your DTOs are serialized to JSON/XML by a consumer (or by you), the property names are a wire contract. Rename them and round-tripping breaks.
- Reflection — anything your library or its consumers look up by string (
GetType,GetMethod, attribute-driven discovery) must keep its name. - Dependency injection — types registered and resolved by a container, or discovered by convention.
A good tool detects the common cases automatically, but you should add explicit keep rules for anything custom rather than hope. This is the single biggest source of “it worked in my tests, it broke at the customer” — the same failure mode we flag for apps in CI, just with more surface area because a library’s consumers do unpredictable things.
Step 3: protect the internals — properly
With the surface pinned down, the internals are where you actually harden. This is the part that gives your SDK its value and the part a competitor or a curious customer would most like to read. Layer it:
- Rename all internal and private types and members — turn your implementation’s self-documenting names into noise.
- Encrypt strings so internal endpoints, messages, format templates and keys don’t hand your logic away to anyone searching the binary. See string encryption in .NET.
- Flatten internal control flow so the implementation behind each public method decompiles to a
gotomaze instead of clean logic. - Reserve the heavy artillery for the crown jewels. The handful of internal methods that are the product — a proprietary matching algorithm, key derivation, a licensing check — are the candidates for method encryption or code virtualization. Don’t virtualize the whole library; protect the specific methods worth the runtime cost.
The mental model: a consumer sees a clean, stable, documented API; a reverse-engineer who opens the DLL sees a preserved surface wrapped around an implementation that’s expensive to read.
Step 4: keep IntelliSense and docs working
A common worry is that obfuscation ruins the developer experience of your SDK. It doesn’t, if you do it right. IntelliSense reads names from the assembly and descriptions from your XML documentation file — and you’ve preserved the public names and you ship the XML doc alongside the package as usual. Obfuscation changes what’s inside your methods, not the public signatures IntelliSense displays. So consumers get full IntelliSense on your API, with docs, against a protected implementation.
The one thing you must not do is ship the internal symbol map inside the package. That map reverses your renaming; it’s for you to read production stack traces, and it stays private (more on that below).
Step 5: pack and publish safely
Order matters when a library becomes a NuGet package. Obfuscate the assembly first, then pack the protected DLL into the .nupkg — never pack first and hope. Then, before you publish:
- Obfuscate the library with the public API preserved.
- Pack the protected assembly (plus the XML docs) into the package.
- Build a real consumer — a small sample project that references the packed package (not the project) and exercises the public API, plus any reflection/serialization paths a customer would hit.
- Publish only after that consumer test passes.
That consumer test is the equivalent of running your app’s test suite against the protected build — it’s the only check that proves the contract survived protection. Do it against the packed package specifically, because packing is where a preservation mistake actually surfaces.
The honest limits
The same honesty applies here as everywhere in this space:
- A preserved public API is, by definition, readable. You chose to keep those names and signatures — a decompiler will show them cleanly, because it must for the library to be usable. Protection is about the implementation behind the surface, not the surface itself. If a method has to be public, its shape is public.
- Client-side protection raises cost, it doesn’t make code uncrackable. A library runs entirely on the consumer’s machine. Obfuscation and virtualization make the internals expensive to reverse; they don’t make them impossible. If your SDK enforces licensing, the trustworthy decision belongs on a server you control — see protecting .NET licensing checks.
- Preservation widens the attack surface a little. Every keep rule is a readable anchor. Keep the minimum that the contract and runtime resolution require, not a generous margin.
Get started
Protecting a library is mostly about drawing the line in the right place and then testing against a real consumer. Nebula.NET preserves your public API automatically, detects the common reflection and serialization cases, and lets you add explicit keep rules for the rest — then renames, encrypts and flattens everything behind the surface, with method encryption and virtualization for the internals that matter.
The free edition runs the whole loop — obfuscate, pack, test a sample consumer — so you can prove the contract survives before you commit; it caps how much you can encrypt or virtualize, not whether you can try. If you’re deciding between tiers for a shipping SDK, which Nebula edition is right for you and pricing lay out the options. Keep the API your customers love; protect the implementation that’s yours.
Try Nebula.NET
Harden your .NET code in minutes — start with the free edition.