How to enforce monthly usage quotas in C# with Subscrio
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#.