Blog

How to create expiring entitlement overrides in C# with Subscrio

Festival pass above an 80-to-20 capacity step at closing time.

BadgeHarbor runs accreditation desks for a film festival. The venue normally has 12 desks and an eight-desk add-on, but opening weekend needs room for 80. The temporary increase should end at the agreed closing time without someone remembering to remove it.

In this guide we will apply a time-limited feature override in C#, test its expiry boundary, and confirm that the ordinary plan and add-on allowance returns.

A temporary entitlement override replaces the resolved feature value until its expiry time. After expiry, the ordinary plan and add-on capacity applies again, without a cleanup job changing the allowance.

Define the customer promise

Source Desk allowance
Base plan 12
Ordinary desk pack Adds 8
Active festival override Replaces the total with 80
At override expiry Returns to 20

Run the example

The complete BadgeHarbor sample contains the setup and executable assertions used below. Use Windows with the .NET SDK and SQL Server Express LocalDB. The runner connects with Windows integrated authentication, creates an isolated database, and removes it when finished.

git clone https://github.com/subscrio/samples.git
cd samples/examples/timed-feature-overrides-csharp
sqllocaldb start MSSQLLocalDB
dotnet restore --locked-mode
dotnet run --no-restore

The snippets run in order within that program. Check prints the returned value and asserts that it matches the expected result. The outputs below come from the executable sample.

Initialize Subscrio

The database helper supplies this connection-string form. Select the SQL Server provider and install the schema before creating the catalog.

Server=(localdb)\MSSQLLocalDB;Database=SubscrioBlog_<generated name>;Integrated Security=true;TrustServerCertificate=true
var clock = new DemoClock();
using var app = new Subscrio.Core.Subscrio(
    new SubscrioConfig
    {
        Database = new DatabaseConfig
        {
            ConnectionString = database.ConnectionString,
            DatabaseType = DatabaseType.SqlServer
        },
        Clock = clock
    }
);
await app.InstallSchemaAsync();

The full runner supplies the imports, database helper, and cleanup. DemoClock is a sample helper implementing Subscrio’s IClock; setting its UtcNow property changes the time used by the checks below. Its injectable clock lets the boundary checks advance time without waiting. It controls Subscrio’s entitlement/accounting evaluation; it does not change the database server clock.

Create the product

BadgeHarbor’s catalog groups the purchase with the capability it controls.

await app.Products.CreateProductAsync(new("badgeharbor", "BadgeHarbor"));

Define the entitlement

The desks feature is a total capacity allowance. Its zero default gives no accreditation desks without a qualifying agreement.

await app.Features.CreateFeatureAsync(new("desks", "desks", "numeric", "0"));

Associate the feature

Associate desks with BadgeHarbor. The subscription rule selects the largest eligible subscription total; the add-on rule defaults to addition, which lets the eight-desk pack extend the twelve-desk plan.

await app.Products.AssociateFeatureAsync(
    "badgeharbor",
    "desks",
    new FeatureResolutionOptions(SubscriptionRule: "most_generous")
);

Create the plan

Create the Venue plan under BadgeHarbor. Its base desk allowance will remain in place beneath the ordinary add-on and temporary override.

await app.Plans.CreatePlanAsync(new("badgeharbor", "standard", "Venue"));

Set the included value

The venue plan starts with twelve desks. We will add the ordinary desk pack before applying the festival override.

await app.Plans.SetFeatureValueAsync("standard", "desks", "12");

Create the billing cycle

The monthly cycle identifies the catalog offer. Creating it does not charge the customer.

await app.BillingCycles.CreateBillingCycleAsync(
    new("standard", "standard-cycle", "Monthly", "months", DurationValue: 1)
);

Create the customer

The sample uses the key customer for the BadgeHarbor purchaser. Creating this record identifies the recipient; the subscription will assign their plan.

await app.Customers.CreateCustomerAsync(new("customer", "BadgeHarbor customer"));

Give the customer a subscription

The agreement subscription selects the Venue plan through standard-cycle, giving this customer the base twelve-desk allowance. The add-on and temporary override will attach to this same subscription.

await app.Subscriptions.CreateSubscriptionAsync(
    new("agreement", "customer", "standard-cycle", ActivationDate: clock.UtcNow)
);

Create the desk pack

The normal add-on adds eight desks to the plan’s twelve.

await app.Addons.CreateAddonAsync(
    new(
        "desk-pack",
        "badgeharbor",
        "Eight desks",
        FeatureValues: new() { ["desks"] = "8" }
    )
);

Attach the desk pack

This establishes the normal allowance before the temporary festival increase.

await app.Subscriptions.AttachAddonAsync("agreement", "desk-pack");
Check(
    "Normal desks",
    await app.FeatureChecker.GetValueForCustomerAsync<int>(
        "customer",
        "badgeharbor",
        "desks",
        0
    ),
    20
);

Output:

Normal desks: 20

Grant a temporary entitlement override

The override is the complete temporary value: 80 desks, not 80 additional desks. The test clock starts at the current instant so the agreement is already active.

var closesAt = clock.UtcNow.AddDays(3);
await app.Subscriptions.AddFeatureOverrideAsync(
    "agreement",
    "desks",
    "80",
    OverrideType.Timed,
    closesAt
);
Check(
    "Festival desks",
    await app.FeatureChecker.GetValueForCustomerAsync<int>(
        "customer",
        "badgeharbor",
        "desks",
        0
    ),
    80
);

Output:

Festival desks: 80

Verify automatic entitlement expiry

Move the test clock to just before closing, then to closing itself. Resolution ignores the expired override immediately; no cleanup job is required for the allowance to change.

clock.UtcNow = closesAt.AddMilliseconds(-1);
Check(
    "Desks before closing",
    await app.FeatureChecker.GetValueForCustomerAsync<int>(
        "customer",
        "badgeharbor",
        "desks",
        0
    ),
    80
);
clock.UtcNow = closesAt;
Check(
    "Desks at closing",
    await app.FeatureChecker.GetValueForCustomerAsync<int>(
        "customer",
        "badgeharbor",
        "desks",
        0
    ),
    20
);

Output:

Desks before closing: 80
Desks at closing: 20

Inspect the retained override

Expiry changes whether the override contributes, not whether its record exists. The stored record is still available for inspection.

var agreement = await app.Subscriptions.GetSubscriptionAsync("agreement");
Check("Retained override records", agreement!.FeatureOverrides.Count, 1);

Output:

Retained override records: 1

Use the result in your application

At the closing instant, the allowance returns to the plan’s twelve desks plus the eight-desk pack. The expired override remains stored, but it no longer contributes to the decision. This lets BadgeHarbor promise a precise end time without making the access change depend on cleanup.

Run the complete BadgeHarbor example to reproduce the checks. The Subscrio subscription addons guide explains the related model.

For a related TypeScript example, see combining subscription allowances in TypeScript. A related next step is subscription capacity packs in C#.

Written by

Jasen Fici

Founder, Subscrio

Bootstrapped founder. Built Velaro and StatusCast. Now building Subscrio, an entitlement engine for software products.

Screenshot preview