DEV Community

Cover image for Trial License Implementation Patterns in C#: A Technical Deep Dive
Olivier Moussalli
Olivier Moussalli

Posted on • Originally published at soraco.co

Trial License Implementation Patterns in C#: A Technical Deep Dive

Trial licenses are critical for software customer acquisition, yet many developers implement them incorrectly, leading to easy bypasses or poor user experience. This guide explores proven implementation patterns with production-ready C# code that balances security, usability, and conversion optimization.

The Trial License Challenge

A well-implemented trial system must satisfy competing requirements:

  • Tamper-resistant - Users shouldn't reset trials by reinstalling or changing system settings
  • Offline-capable - Must work without constant internet connectivity
  • User-friendly - Shouldn't interrupt legitimate evaluation
  • Conversion-optimized - Should encourage but not force purchase decisions

Common naive implementations fail on one or more of these fronts.

Anti-Pattern: Registry-Based Trial Tracking

Many developers start with a simple registry entry:

// ❌ ANTI-PATTERN - Easily bypassed
public static bool CheckTrial()
{
    using (RegistryKey key = Registry.CurrentUser.OpenSubKey("Software\\MyApp"))
    {
        if (key == null)
        {
            CreateTrialEntry(DateTime.Now);
            return true;
        }

        DateTime installDate = (DateTime)key.GetValue("InstallDate");
        return (DateTime.Now - installDate).Days <= 30;
    }
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  • User can delete registry key to reset trial
  • Changing system date bypasses check
  • No protection against reinstallation
  • Easy to discover with tools like Process Monitor

Pattern 1: Multi-Location Time Anchoring

Effective trial systems store the trial start date in multiple tamper-resistant locations:

public class TrialManager
{
    private static readonly string[] StorageLocations = new[]
    {
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".app_data"),
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ".config"),
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), ".system")
    };

    public static DateTime GetTrialStartDate()
    {
        var dates = new List<DateTime>();

        // Read from all locations
        foreach (var location in StorageLocations)
        {
            if (File.Exists(location))
            {
                try
                {
                    string encrypted = File.ReadAllText(location);
                    DateTime date = DecryptDate(encrypted);
                    dates.Add(date);
                }
                catch { /* Continue checking other locations */ }
            }
        }

        if (dates.Count == 0)
        {
            DateTime now = DateTime.UtcNow;
            InitializeTrialDate(now);
            return now;
        }

        return dates.Min();
    }

    private static void InitializeTrialDate(DateTime date)
    {
        string encrypted = EncryptDate(date);

        foreach (var location in StorageLocations)
        {
            try
            {
                Directory.CreateDirectory(Path.GetDirectoryName(location));
                File.WriteAllText(location, encrypted);
                File.SetAttributes(location, FileAttributes.Hidden | FileAttributes.System);
            }
            catch { /* Some locations may be protected */ }
        }
    }

    private static string EncryptDate(DateTime date)
    {
        string machineKey = GetMachineFingerprint();
        byte[] data = Encoding.UTF8.GetBytes(date.ToBinary().ToString());

        using (var aes = Aes.Create())
        {
            aes.Key = DeriveKeyFromString(machineKey);
            aes.GenerateIV();

            using (var encryptor = aes.CreateEncryptor())
            using (var ms = new MemoryStream())
            {
                ms.Write(aes.IV, 0, aes.IV.Length);
                using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                {
                    cs.Write(data, 0, data.Length);
                }
                return Convert.ToBase64String(ms.ToArray());
            }
        }
    }

    private static DateTime DecryptDate(string encrypted)
    {
        string machineKey = GetMachineFingerprint();
        byte[] data = Convert.FromBase64String(encrypted);

        using (var aes = Aes.Create())
        {
            aes.Key = DeriveKeyFromString(machineKey);

            byte[] iv = new byte[16];
            Array.Copy(data, 0, iv, 0, 16);
            aes.IV = iv;

            using (var decryptor = aes.CreateDecryptor())
            using (var ms = new MemoryStream(data, 16, data.Length - 16))
            using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
            using (var reader = new StreamReader(cs))
            {
                string value = reader.ReadToEnd();
                long binary = long.Parse(value);
                return DateTime.FromBinary(binary);
            }
        }
    }

    private static string GetMachineFingerprint()
    {
        var components = new[]
        {
            Environment.MachineName,
            Environment.ProcessorCount.ToString(),
            Environment.OSVersion.ToString(),
            GetVolumeSerial()
        };

        string combined = string.Join("|", components);
        using (var sha = SHA256.Create())
        {
            byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(combined));
            return Convert.ToBase64String(hash);
        }
    }

    private static string GetVolumeSerial()
    {
        try
        {
            var drive = new DriveInfo(Path.GetPathRoot(Environment.SystemDirectory));
            return drive.VolumeLabel + drive.TotalSize.ToString();
        }
        catch
        {
            return "unknown";
        }
    }

    private static byte[] DeriveKeyFromString(string input)
    {
        using (var sha = SHA256.Create())
        {
            return sha.ComputeHash(Encoding.UTF8.GetBytes(input));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Trial date stored in multiple locations (hard to find and modify all)
  • Encrypted with machine-specific key (can't copy to another machine)
  • Uses earliest date found (prevents gaming)
  • Hidden system files

Usage:

public static bool IsTrialValid()
{
    DateTime trialStart = TrialManager.GetTrialStartDate();
    int daysElapsed = (DateTime.UtcNow - trialStart).Days;
    int daysRemaining = 30 - daysElapsed;

    if (daysRemaining <= 0)
    {
        return false;
    }

    if (daysRemaining <= 7)
    {
        MessageBox.Show($"Trial expires in {daysRemaining} days. Purchase a license to continue.",
            "Trial Expiring", MessageBoxButtons.OK, MessageBoxIcon.Information);
    }

    return true;
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Time-Based Feature Degradation

Rather than hard-blocking after trial expiry, gradually reduce functionality:

public enum TrialPhase
{
    Full,           // Days 1-25: All features
    Limited,        // Days 26-30: Some features disabled
    Expired         // Day 31+: Core features only
}

public class TrialFeatureManager
{
    public static TrialPhase GetCurrentPhase()
    {
        DateTime trialStart = TrialManager.GetTrialStartDate();
        int daysElapsed = (DateTime.UtcNow - trialStart).Days;

        if (daysElapsed <= 25)
            return TrialPhase.Full;
        else if (daysElapsed <= 30)
            return TrialPhase.Limited;
        else
            return TrialPhase.Expired;
    }

    public static bool IsFeatureAvailable(string featureName)
    {
        TrialPhase phase = GetCurrentPhase();

        switch (phase)
        {
            case TrialPhase.Full:
                return true;

            case TrialPhase.Limited:
                var limitedFeatures = new[] { "Export", "Automation", "CloudSync" };
                return !limitedFeatures.Contains(featureName);

            case TrialPhase.Expired:
                var expiredFeatures = new[] { "View", "Open", "Read" };
                return expiredFeatures.Contains(featureName);

            default:
                return false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

private void ExportButton_Click(object sender, EventArgs e)
{
    if (!TrialFeatureManager.IsFeatureAvailable("Export"))
    {
        var result = MessageBox.Show(
            "Export is not available in trial mode. Purchase a license to unlock this feature.",
            "Feature Locked",
            MessageBoxButtons.OKCancel,
            MessageBoxIcon.Information);

        if (result == DialogResult.OK)
        {
            ShowPurchaseDialog();
        }
        return;
    }

    PerformExport();
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Encourages conversion without frustrating users
  • Users can still access their work after trial ends
  • Provides clear value proposition

Pattern 3: Server-Verified Trial Status

For applications with internet connectivity, add server-side verification:

public class ServerVerifiedTrial
{
    private const string TrialServerUrl = "https://yourserver.com/api/trial/verify";

    public static async Task<TrialStatus> VerifyTrialAsync(string machineId)
    {
        try
        {
            using (var client = new HttpClient())
            {
                var request = new TrialVerificationRequest
                {
                    MachineId = machineId,
                    ProductId = "YourProductId",
                    Timestamp = DateTime.UtcNow
                };

                var json = JsonSerializer.Serialize(request);
                var content = new StringContent(json, Encoding.UTF8, "application/json");

                var response = await client.PostAsync(TrialServerUrl, content);

                if (response.IsSuccessStatusCode)
                {
                    string responseJson = await response.Content.ReadAsStringAsync();
                    return JsonSerializer.Deserialize<TrialStatus>(responseJson);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Server verification failed: {ex.Message}");
        }

        return GetLocalTrialStatus();
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Prevents trial reset by reinstallation
  • Provides analytics on trial usage
  • Can extend trials remotely
  • Detects suspicious behavior

Pattern 4: Hybrid Local + Server Validation

Combine local and server checks for optimal balance:

public class HybridTrialValidator
{
    private static DateTime? lastServerCheck = null;
    private static TrialStatus cachedServerStatus = null;

    public static async Task<bool> ValidateTrialAsync()
    {
        bool localValid = IsLocalTrialValid();

        if (!localValid)
        {
            return false;
        }

        if (ShouldCheckServer())
        {
            try
            {
                string machineId = TrialManager.GetMachineFingerprint();
                cachedServerStatus = await ServerVerifiedTrial.VerifyTrialAsync(machineId);
                lastServerCheck = DateTime.UtcNow;

                return cachedServerStatus.IsValid;
            }
            catch
            {
                return localValid;
            }
        }

        if (cachedServerStatus != null)
        {
            return cachedServerStatus.IsValid && localValid;
        }

        return localValid;
    }

    private static bool ShouldCheckServer()
    {
        if (lastServerCheck == null)
            return true;

        return (DateTime.UtcNow - lastServerCheck.Value).TotalHours >= 24;
    }

    private static bool IsLocalTrialValid()
    {
        DateTime trialStart = TrialManager.GetTrialStartDate();
        int daysElapsed = (DateTime.UtcNow - trialStart).Days;
        return daysElapsed <= 30;
    }
}
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Using a License Management Solution

While implementing your own trial system is educational, production applications often benefit from using established license management solutions like Quick License Manager. These solutions handle all the complexity we've discussed and provide additional features.

Here's how trial validation looks with QLM:

public class ManagedTrialValidator
{
    private LicenseValidator lv;

    public ManagedTrialValidator(string settingsFile)
    {
        lv = new LicenseValidator(settingsFile);
    }

    public bool ValidateTrial()
    {
        bool needsActivation = false;
        string errorMsg = string.Empty;

        bool isValid = lv.ValidateLicenseAtStartup(
            ELicenseBinding.ComputerName,
            ref needsActivation,
            ref errorMsg
        );

        if (isValid)
        {
            return true;
        }

        if (lv.QlmLicenseObject.DaysLeft > 0)
        {
            int daysRemaining = lv.QlmLicenseObject.DaysLeft;

            if (daysRemaining <= 7)
            {
                ShowTrialExpirationWarning(daysRemaining);
            }

            return true;
        }

        return false;
    }

    private void ShowTrialExpirationWarning(int daysRemaining)
    {
        string message = daysRemaining == 1
            ? "Your trial expires tomorrow. Purchase a license to continue."
            : $"Your trial expires in {daysRemaining} days.";

        MessageBox.Show(message, "Trial Expiring Soon",
            MessageBoxButtons.OK, MessageBoxIcon.Information);
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits of using QLM or similar solutions:

  • Automatically handles all tamper protection patterns
  • Provides ready-to-use activation UI
  • Manages trial-to-paid conversion workflow
  • Handles edge cases (system date changes, VM cloning, etc.)
  • Includes e-commerce integration
  • Provides usage analytics

You can download QLM for free trial evaluation.

UI/UX Best Practices

Display Trial Status Prominently

public class TrialStatusDisplay : UserControl
{
    private Label lblStatus;
    private ProgressBar progressBar;
    private Button btnPurchase;

    public void UpdateTrialStatus(int daysRemaining, int totalDays = 30)
    {
        if (daysRemaining <= 0)
        {
            lblStatus.Text = "Trial Expired - Purchase License to Continue";
            lblStatus.ForeColor = Color.Red;
            progressBar.Value = 0;
            btnPurchase.Visible = true;
        }
        else
        {
            lblStatus.Text = $"Trial: {daysRemaining} days remaining";
            lblStatus.ForeColor = daysRemaining <= 7 ? Color.Orange : Color.Green;
            progressBar.Maximum = totalDays;
            progressBar.Value = daysRemaining;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Non-Intrusive Reminders

Don't show reminders more than once per day, and only in the final week:

private static DateTime? lastReminderShown = null;

public static void ShowTrialReminderIfNeeded(int daysRemaining)
{
    if (lastReminderShown.HasValue && 
        (DateTime.Now - lastReminderShown.Value).TotalHours < 24)
    {
        return;
    }

    if (daysRemaining > 7)
    {
        return;
    }

    // Show reminder dialog
    var reminder = new Form
    {
        Text = "Trial Reminder",
        Size = new Size(400, 200),
        StartPosition = FormStartPosition.CenterScreen
    };

    // Add controls and show
    reminder.ShowDialog();
    lastReminderShown = DateTime.Now;
}
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

Cache validation results to avoid impacting application performance:

public class CachedTrialValidator
{
    private static bool? cachedStatus = null;
    private static DateTime? cacheTime = null;
    private static readonly TimeSpan CacheTimeout = TimeSpan.FromMinutes(5);

    public static bool IsTrialValid()
    {
        if (cachedStatus.HasValue && 
            cacheTime.HasValue && 
            DateTime.UtcNow - cacheTime.Value < CacheTimeout)
        {
            return cachedStatus.Value;
        }

        cachedStatus = PerformTrialValidation();
        cacheTime = DateTime.UtcNow;

        return cachedStatus.Value;
    }

    private static bool PerformTrialValidation()
    {
        DateTime trialStart = TrialManager.GetTrialStartDate();
        return (DateTime.UtcNow - trialStart).Days <= 30;
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing Your Trial Implementation

Always test these scenarios:

[TestClass]
public class TrialTests
{
    [TestMethod]
    public void TestNewInstallation()
    {
        var validator = new TrialValidator();
        Assert.IsTrue(validator.IsTrialValid());
    }

    [TestMethod]
    public void TestTrialExpiry()
    {
        var trialStart = DateTime.UtcNow.AddDays(-31);
        TrialManager.InitializeTrialDate(trialStart);

        var validator = new TrialValidator();
        Assert.IsFalse(validator.IsTrialValid());
    }

    [TestMethod]
    public void TestReinstallation()
    {
        var validator1 = new TrialValidator();
        DateTime firstStart = TrialManager.GetTrialStartDate();

        var validator2 = new TrialValidator();
        DateTime secondStart = TrialManager.GetTrialStartDate();

        Assert.AreEqual(firstStart, secondStart);
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Effective trial license implementation requires balancing security, usability, and conversion optimization. Key principles:

  • Store trial data in multiple tamper-resistant locations
  • Encrypt with machine-specific keys
  • Implement graceful degradation rather than hard blocks
  • Use server verification when possible, with offline fallback
  • Display trial status prominently and non-intrusively
  • Test thoroughly including edge cases

For production applications, consider using established license management solutions like Quick License Manager that handle these complexities and provide tested implementations of these patterns.

Resources


What trial implementation patterns have you used? Share your experience in the comments! 👇

Top comments (0)