DEV Community

Cover image for Replace hardcoded plan checks with feature entitlements in C#
Feech for Subscrio

Posted on AI-assisted

Replace hardcoded plan checks with feature entitlements in C#

Checking a customer's plan name is an easy way to decide whether they can use a paid feature. It also ties the application to the names in your pricing catalog. When a new plan includes an existing feature, the customer can have the right subscription and still fail the check.

This article walks through that failure in a C# export function. You'll move the access rule into a subscription catalog, make the function check the customer's access to CSV export, and verify that a new paid plan works without opening the feature to free customers.

We'll use Subscrio, the entitlement library we build. An entitlement describes what a customer can use under their subscription. Subscrio resolves that access; the application uses the answer to allow or deny the operation.

Before you get started

To run the example locally, you'll need:

The complete code is in the FeedbackDesk sample in the Subscrio samples repository. The clone and run commands are at the end of this article.

A paid customer who cannot export

FeedbackDesk is a fictional application where software teams collect customer feedback and prioritize feature requests. A product manager uses CSV export to download feedback and vote counts for a planning meeting. The Free plan supports collecting feedback; the Pro plan also includes exports.

FeedbackDesk introduces an Enterprise plan for larger teams. It includes CSV export too. But when an Enterprise customer's product manager clicks Export, the application denies the request. The export function still contains a condition written when Pro was the only plan with that feature:

bool CanExportUsingPlanName(string planKey) => planKey == "pro";
Enter fullscreen mode Exit fullscreen mode

CanExportUsingPlanName is a function in our example application. Its name describes the existing approach: decide export access by comparing a plan name. Here is what it returns for the two paid plans:

Customer's plan Function returns What happens What should happen
Pro true Export allowed Export allowed
Enterprise false Export denied Export allowed

The Enterprise denial is the bug. Nothing in this condition describes what Enterprise includes; it only recognizes the word "pro".

Adding || planKey == "enterprise" would fix this particular denial. That is a reasonable small change if the plans rarely change. As more features and offerings appear, however, each plan-name check becomes another place to keep synchronized with the catalog.

We'll give the export function a different question to ask: "Does this customer have CSV export?" To answer it, we first need to describe that feature and which plans include it.

Give CSV export a rule of its own

The rule we want is simple:

Plan CSV export
Free Denied
Pro Allowed
Enterprise Allowed

In Subscrio, the product groups the plans and features for FeedbackDesk. A feature represents a capability such as CSV export. A plan assigns a value to that feature. For an operation that is either included or excluded, we'll use a toggle with the values true and false.

The following sections are successive parts of one console program. They show the catalog setup, connect three example customers to their plans, and then replace the export check. The complete program's imports and database setup are linked in the run instructions below.

Connect to the sample database

Subscrio needs a database to store the catalog and subscriptions. The sample includes a LocalDatabase helper that creates a disposable SQL Server LocalDB database and supplies its connection string. That helper belongs to the sample; InstallSchemaAsync is the Subscrio call that creates its database tables.

await using var database = new LocalDatabase();
await database.CreateAsync();

using var app = new Subscrio.Core.Subscrio(
    new SubscrioConfig
    {
        Database = new DatabaseConfig
        {
            ConnectionString = database.ConnectionString,
            DatabaseType = DatabaseType.SqlServer
        }
    }
);
await app.InstallSchemaAsync();
Enter fullscreen mode Exit fullscreen mode

The sample's LocalDatabase.cs contains the connection and cleanup code. In this program, app is the Subscrio client we'll use for the remaining calls.

Register FeedbackDesk and its export feature

Create a product identified by feedbackdesk. The key gives subsequent calls a consistent way to refer to this application.

await app.Products.CreateProductAsync(new("feedbackdesk", "FeedbackDesk"));
Enter fullscreen mode Exit fullscreen mode

Next, define csv-export. Its default is false: a customer needs an applicable entitlement before the application will allow an export. Subscrio stores feature values as strings in this catalog API, so the toggle's default is written as "false".

await app.Features.CreateFeatureAsync(
    new("csv-export", "CSV export", "toggle", "false")
);
Enter fullscreen mode Exit fullscreen mode

Associate the feature with FeedbackDesk so its plans can assign export access:

await app.Products.AssociateFeatureAsync("feedbackdesk", "csv-export");
Enter fullscreen mode Exit fullscreen mode

Record which plans include exports

Create the three plans under FeedbackDesk. The lowercase strings are their catalog keys.

foreach (var plan in new[] { "free", "pro", "enterprise" })
    await app.Plans.CreatePlanAsync(new("feedbackdesk", plan, plan));
Enter fullscreen mode Exit fullscreen mode

Now translate the access table into feature values. Both paid plans grant the same feature, while Free explicitly excludes it.

await app.Plans.SetFeatureValueAsync("free", "csv-export", "false");
await app.Plans.SetFeatureValueAsync("pro", "csv-export", "true");
await app.Plans.SetFeatureValueAsync("enterprise", "csv-export", "true");
Enter fullscreen mode Exit fullscreen mode

We have now recorded why the Enterprise customer should be able to export. The next step is to connect a customer to that plan so Subscrio can resolve their access.

Connect each customer to a plan

FeedbackDesk sells subscriptions to teams. In this sample, each team is a Subscrio customer; the product manager is a user acting for that customer. We use three customers to check the full rule:

Customer key Plan Expected export access
startup-team Free Denied
growth-team Pro Allowed
enterprise-team Enterprise Allowed

A Subscrio subscription refers to a billing cycle belonging to a plan. Create one monthly cycle for each plan before assigning subscriptions. These records describe the subscription periods; creating them does not collect payment.

foreach (var plan in new[] { "free", "pro", "enterprise" })
    await app.BillingCycles.CreateBillingCycleAsync(
        new(plan, plan + "-monthly", "Monthly", "months", DurationValue: 1)
    );
Enter fullscreen mode Exit fullscreen mode

For example, enterprise-monthly belongs to the Enterprise plan. Now register the three customers:

foreach (var customer in new[] { "startup-team", "growth-team", "enterprise-team" })
    await app.Customers.CreateCustomerAsync(new(customer));
Enter fullscreen mode Exit fullscreen mode

Give each customer a subscription to the appropriate cycle. Each constructor below receives a subscription key, the customer key, and the billing-cycle key.

await app.Subscriptions.CreateSubscriptionAsync(
    new("startup-team-plan", "startup-team", "free-monthly")
);
await app.Subscriptions.CreateSubscriptionAsync(
    new("growth-team-plan", "growth-team", "pro-monthly")
);
await app.Subscriptions.CreateSubscriptionAsync(
    new("enterprise-team-plan", "enterprise-team", "enterprise-monthly")
);
Enter fullscreen mode Exit fullscreen mode

Subscrio can now follow enterprise-team's subscription to the Enterprise plan and find its csv-export value of "true".

Ask about export access when the customer exports

Return to the product manager's failed request. The application already knows which customer they act for. It can pass that customer key to the export function instead of passing a plan name.

In ExportFeedbackCsv, IsEnabledForCustomerAsync asks Subscrio whether csv-export is enabled for that customer within feedbackdesk. It returns a Boolean. The function allows the export when the answer is true and returns a denial marker when it is false.

async Task<string> ExportFeedbackCsv(string customerKey)
{
    bool canExport = await app.FeatureChecker.IsEnabledForCustomerAsync(
        customerKey,
        "feedbackdesk",
        "csv-export"
    );

    if (!canExport)
        return "export_not_included";

    return "feedback,votes\nDark mode,12";
}
Enter fullscreen mode Exit fullscreen mode

ExportFeedbackCsv is our application code; IsEnabledForCustomerAsync is the Subscrio API. The CSV is a fixed example row: a request for dark mode with 12 votes. A real export would query the customer's feedback and write those rows after the access check. Likewise, the application would turn export_not_included into its chosen response or upgrade message.

The application remains responsible for authenticating the user and establishing which customer they may act for. A customer key supplied by an untrusted request is not proof of that relationship.

The export function now contains no plan names. Pro and Enterprise reach the same operation because both plans grant csv-export.

Check the customer's experience after the change

Call the export function for each of our three customers:

string freeResult = await ExportFeedbackCsv("startup-team");
string proResult = await ExportFeedbackCsv("growth-team");
string enterpriseResult = await ExportFeedbackCsv("enterprise-team");
Enter fullscreen mode Exit fullscreen mode

The program checks that Free receives export_not_included and both paid customers receive the CSV. These are ordinary C# checks in the sample; a mismatch throws an error and fails the run.

string expectedCsv = "feedback,votes\nDark mode,12";

if (freeResult != "export_not_included")
    throw new InvalidOperationException("Free must not include CSV export.");

if (proResult != expectedCsv)
    throw new InvalidOperationException("Pro must receive the feedback CSV.");

if (enterpriseResult != expectedCsv)
    throw new InvalidOperationException("Enterprise must receive the feedback CSV.");
Enter fullscreen mode Exit fullscreen mode

After those checks pass, the program prints the following confirmation:

Free customer: export denied, as expected.
Pro customer: feedback CSV returned, as expected.
Enterprise customer: feedback CSV returned, as expected.
Enter fullscreen mode Exit fullscreen mode

The Enterprise product manager can now download the feedback for their meeting. The Free customer is still denied, so fixing Enterprise has not accidentally made exports available to every customer.

Run the complete example

The FeedbackDesk sample includes Program.cs, database setup, and the checks above. Use Windows with the .NET SDK and SQL Server Express LocalDB installed.

git clone https://github.com/subscrio/samples.git
cd samples/examples/replace-hardcoded-plan-checks-csharp
sqllocaldb start MSSQLLocalDB
dotnet restore --locked-mode
dotnet run --no-restore
Enter fullscreen mode Exit fullscreen mode

The program first prints the existing plan-name check's denial for Enterprise, then builds the catalog and verifies the corrected exports. It uses Windows integrated authentication, creates a uniquely named disposable database, and removes that database when it finishes.

If FeedbackDesk introduces another plan with CSV export, assign that plan's csv-export value in the catalog. The export function can keep asking the same customer-level question. Keep the Free, Pro, and Enterprise checks when extending the catalog so the access promised by each plan remains covered.


This article was prepared with AI assistance.

Top comments (1)

Collapse
 
kevinpruett023_kevinpruet profile image
Lee •

Hi. Your post, website, github all are great!
I wanna have meaningful conversation about collaboration with you.
I believe we can achieve something big by our collaborating.
How about discussing about collaboration via a meeting?