Skip to content
← All posts
· Delta1 Labs .NETObfuscationGuide

Shipping a Time-Limited Demo of Your .NET App

Sometimes you want a build that simply stops working after a date — a conference demo, an evaluation copy, a pre-release that must not run in production forever. Nebula.NET can bake an expiry into the assembly itself, enforced from a module initializer before your code runs, with a reaction you choose. Here is how it works and when to use it instead of server licensing.

Not every build is meant to live forever. A conference demo should stop working the week after the talk. An evaluation copy handed to a prospect shouldn’t still be running in production a year later. A time-boxed beta needs a hard cutoff so testers upgrade instead of quietly shipping the preview. For all of these, you don’t want a licensing server and accounts — you want the build itself to expire. Nebula.NET can compile that expiry straight into the assembly.

One setting

You add an expiry the same way you configure any other protection — in the Nebula config:

{
  "rename": true,
  "controlFlowObfuscation": true,
  "expiryUtc": "2026-12-31T23:59:59Z",
  "expiryReaction": "exit",
  "expiryMessage": "This evaluation build expired on 2026-12-31. Contact sales for a licensed copy."
}

That is the whole feature surface: a UTC instant, a reaction, and an optional message. Nebula injects the enforcement during obfuscation — there is nothing to add to your source code, no using, no API call, and therefore nothing for someone reading your decompiled code to find and strip out by name.

Where the check runs — before your code

The important design decision is where the date check lives. Nebula injects it into the module initializer — the module-level .cctor that the CLR runs exactly once, automatically, before any type in the module is touched. It runs ahead of Main, ahead of any static constructor of your own, ahead of any entry point:

// injected into <Module>::.cctor, conceptually:
ldc.i8     <expiryTicks>                 // 2026-12-31T23:59:59Z as UTC ticks
call       valuetype [System.Runtime]System.DateTime [System.Runtime]System.DateTime::get_UtcNow()
call       instance int64 [System.Runtime]System.DateTime::get_Ticks()
bgt.s      EXPIRED                        // now > expiry → react
// ... continue normal module init
EXPIRED:
// the configured reaction (exit / throw / callback)

Because it is a module initializer, there is no execution path into your assembly that skips it. Contrast that with sprinkling if (DateTime.UtcNow > …) through your own methods: you’d have to remember every entry point, and each check is a readable branch in your source. The injected guard is singular, automatic, and added at the IL level after your compiler is done.

Choosing the reaction

Enforcement is Nebula’s job; what happens on expiry is yours to pick with expiryReaction:

  • exit (default) — the process terminates immediately. Simplest and hardest to talk your way around; good for a pure demo.
  • throw — an exception is raised that you can catch at your top level and turn into a friendly screen, a logged event, or an orderly shutdown.
  • callback — Nebula invokes a method you nominate, so you keep full control: show a WPF dialog, switch the app into a read-only mode, or phone home that a demo expired.

A callback reaction points at one of your own methods by name:

{
  "expiryUtc": "2026-12-31T23:59:59Z",
  "expiryReaction": "callback",
  "expiryCallback": "MyApp.Startup.OnEvaluationExpired"
}
namespace MyApp;
static class Startup
{
    // Called by the injected guard when the build is past its date.
    public static void OnEvaluationExpired()
    {
        MessageBox.Show("Your evaluation of MyApp has ended. Visit example.com to license it.");
        Environment.Exit(0);
    }
}

The guard calls your method; you decide the experience. If the method returns, you control what happens next — there is no hidden second behaviour.

It pairs with the rest of the protection

An expiry on its own is only as hidden as the code around it, which is why it is a transform in the same pipeline as everything else. Run it alongside renaming, control-flow obfuscation and — on Enterprise — method encryption or virtualization, and the comparison, the expiry constant and the reaction are buried in the same flattened, encrypted output as the rest of your logic, not sitting in an obvious DateTime branch. The guard is also a licensed (paid) feature: a Free-edition build silently drops it rather than shipping a half-enforced date, so a demo you intend to expire actually does.

When to reach for licensing instead

Be honest about what a baked-in date is and isn’t. It is deterrence for honest use — it makes a demo or evaluation stop on schedule so it doesn’t leak into production or outlive its purpose. It is not a hard boundary against a motivated attacker, because every local date check can be attacked by rolling the clock back, and a fixed build can’t be extended, revoked or converted without shipping a new one.

When the user has an account and you want to manage their access over time — extend a trial, convert it to paid in place, revoke a leaked copy, re-validate online so the local clock doesn’t matter — that is software licensing, not expiry. The two compose cleanly: ship evaluation builds with a hard expiry for the casual case, and a real license for customers. See adding a free trial for the server-backed path, and protecting your license checks once you have them. For a throwaway build that must simply stop on a date, though, one line of config is the whole job.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.