Blog

How to enforce monthly usage quotas in C# with Subscrio

A structural beam beside a full 250-validation allowance and a stopped next job.

BeamCheck validates structural models before engineers submit them for review. Its Studio subscription includes 250 validations each calendar month. An allowance of 250 is only useful if the application counts accepted jobs and refuses the next one, including when a caller retries an earlier request.

In this guide we will enforce a monthly usage quota in C#, report each validation with a stable job key, and inspect the usage that Subscrio returns.

This hard usage limit resets at the start of each UTC calendar month, regardless of the subscription billing date. Stable job keys make usage reporting idempotent: retrying an accepted job does not count it again.

Define the customer promise

Rule Studio subscription
Allowance 250 validations
Reset Start of each UTC calendar month
Quantity One unit per accepted validation
At the limit Reject a new usage report

Run the example

The complete BeamCheck 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/monthly-usage-quotas-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. ExpectError<T> is its assertion helper: it requires the requested exception and prints true when that check passes.

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

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

await app.Products.CreateProductAsync(new("beamcheck", "BeamCheck"));

Define the entitlement

The validations feature records accepted validation jobs. A customer without the Studio entitlement has an allowance of zero. The monthly count configuration measures one job per unit and rejects reports above the allowance.

await app.Features.CreateFeatureAsync(
    new(
        "validations",
        "validations",
        "metered",
        "0",
        MeteredConfig: new("monthly", "hard", "count", "customer")
    )
);

Associate the feature

Make this feature available to BeamCheck’s plans.

await app.Products.AssociateFeatureAsync(
    "beamcheck",
    "validations",
    new FeatureResolutionOptions(SubscriptionRule: "most_generous")
);

Create the plan

The standard key identifies the Studio offer. Its metered feature value supplies the monthly limit; accepted jobs will be counted separately.

await app.Plans.CreatePlanAsync(new("beamcheck", "standard", "Studio"));

Set the included value

Studio supplies 250 validations for each UTC calendar month.

await app.Plans.SetFeatureValueAsync("standard", "validations", "250");

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 BeamCheck purchaser. Creating this record identifies the recipient; the subscription will assign their plan.

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

Give the customer a subscription

The Studio plan allows the customer to submit 250 validations each UTC month. The agreement subscription selects standard-cycle, which belongs to that plan. Usage reports will resolve this allowance for the customer.

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

Read the monthly allowance

GetUsageAsync describes the current bucket and whether another validation would fit. It does not reserve a validation. The hard limit is enforced by ReportUsageAsync.

var initial = await app.Metering.GetUsageAsync(
    "customer",
    "beamcheck",
    "validations"
);
Check("Monthly limit", initial.Limit, 250L);
Check("Initially consumed", initial.Consumed, 0L);
Check("Initially remaining", initial.Remaining, 250L);

Output:

Monthly limit: 250
Initially consumed: 0
Initially remaining: 250

Record usage against the monthly quota

Each completed admission uses one unit because this is a count meter. The job key belongs to the validation request and must survive retries. Admit the job by reporting usage before starting the expensive validation.

for (var job = 1; job <= 250; job++)
    await app.Metering.ReportUsageAsync(
        "customer",
        "beamcheck",
        "validations",
        1,
        new UsageReportOptions($"validation-{job}")
    );
var full = await app.Metering.GetUsageAsync("customer", "beamcheck", "validations");
Check("Accepted validations", full.Consumed, 250L);
Check("Remaining validations", full.Remaining, 0L);

Output:

Accepted validations: 250
Remaining validations: 0

The meter admission and the external validation are separate operations. This sample verifies the allowance; a job runner still needs to track which admitted jobs have actually run.

Retry a recorded job

Replaying the same quantity with the same key returns the recorded result. It does not spend a second validation.

await app.Metering.ReportUsageAsync(
    "customer",
    "beamcheck",
    "validations",
    1,
    new UsageReportOptions("validation-250")
);
Check(
    "Consumed after retry",
    (
        await app.Metering.GetUsageAsync("customer", "beamcheck", "validations")
    ).Consumed,
    250L
);

Output:

Consumed after retry: 250

Reject usage above the hard limit

A new job has a new key. Once the allowance is exhausted, the hard meter rejects its usage report.

await ExpectError<UsageLimitExceededException>(
    () =>
        app.Metering.ReportUsageAsync(
            "customer",
            "beamcheck",
            "validations",
            1,
            new UsageReportOptions("validation-251")
        ),
    "Next validation rejected"
);

Output:

Next validation rejected: true

Verify the calendar-month quota reset

A monthly meter uses UTC calendar boundaries. Moving the test clock to the first instant of the next month selects a fresh usage bucket.

var now = clock.UtcNow;
clock.UtcNow = new DateTime(
    now.Year,
    now.Month,
    1,
    0,
    0,
    0,
    DateTimeKind.Utc
).AddMonths(1);
var nextMonth = await app.Metering.GetUsageAsync(
    "customer",
    "beamcheck",
    "validations"
);
Check("Next month consumed", nextMonth.Consumed, 0L);
Check("Next month remaining", nextMonth.Remaining, 250L);

Output:

Next month consumed: 0
Next month remaining: 250

Use the result in your application

The allowance now answers two separate questions: how much the customer has used, and whether the next validation can be admitted. The 251st new job fails, an existing job key does not spend twice, and the next calendar month starts with a fresh bucket. Keep the stable job key when connecting this admission step to the real validator.

Run the complete BeamCheck example to reproduce the checks.

For a related TypeScript example, see billing-period metering in TypeScript. A related next step is monthly credit grants 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