fullstackhero/dotnet-starter-kit · error · InvalidOperationException
Subscription cannot be backdated.
Error message
Subscription cannot be backdated.
What it means
AppTenantInfo.SetValidity extends a tenant's subscription validity to the new expiry date, but only if the date is later than the current ValidUpto. Passing an earlier (or equal) date throws InvalidOperationException, preventing accidental shortening of a paid subscription. Note the comparison is ValidUpto < normalized — an equal date also throws.
Solutions
- Only call SetValidity with a date later than the current ValidUpto — check before calling.
- In seeders, make expiry dates relative to UtcNow instead of hard-coded.
- Add a separate explicit downgrade/shorten method if backdating is ever legitimately required.
- Guard the caller: skip SetValidity when normalized <= tenant.ValidUpto.
Example fix
// before
subscription.SetValidity(DateTime.Parse("2026-01-01"));
// after
var newExpiry = DateTime.UtcNow.AddYears(1);
if (newExpiry > tenant.ValidUpto)
tenant.SetValidity(newExpiry); Defensive patterns
Strategy: validation
Validate before calling
if (newExpiry <= tenant.ValidUpto)
throw new ArgumentException("New expiry must be later than the current subscription expiry.");
tenant.SetValidity(newExpiry); Type guard
bool CanExtend(AppTenantInfo t, DateTime validTill) => t.ValidUpto < validTill;
Try / catch
try {
tenant.SetValidity(newExpiry);
} catch (InvalidOperationException ex) when (ex.Message == "Subscription cannot be backdated.") {
logger.LogWarning("Skipped backdated expiry {Expiry} for tenant {TenantId}", newExpiry, tenant.Id);
} Prevention
- Compute expiry dates relative to UtcNow in seeders and tests — never hard-code.
- Only call SetValidity for extensions; handle renewals-before-expiry differently.
- In admin UIs, disable submitting an expiry earlier than the current one.
When it happens
Trigger: Calling SetValidity(validTill) with a validTill less than or equal to the tenant's current ValidUpto, e.g. during EnsureDemoTenantsExistAsync seeding with a stale/hard-coded expiry date after it was already extended.
Common situations: Seeding demo tenants on a database where the subscription was already extended; re-running seeders with fixed dates; clock/config mistakes passing dates in the past; admin UI allowing operators to enter an earlier expiry.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Invalid Tenant
- This tenant's subscription has expired. Please renew to…
- Storage quota exceeded
- ConnectionString can't be null.
- Cross-tenant audit summary requires…
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/ddf3fcc65169ddc0.
Report an issue: GitHub.
Appendix: source
Thrown at src/BuildingBlocks/Shared/Multitenancy/AppTenantInfo.cs:58
public bool IsActive { get; set; }
public DateTime ValidUpto { get; set; }
public string? Issuer { get; set; }
/// <summary>Plan name used to resolve quota defaults (e.g. "free", "pro"). Null falls back to <c>QuotaOptions.DefaultPlan</c>.</summary>
public string? Plan { get; set; }
/// <summary>Per-tenant quota overrides. Serialized as JSON by the tenant store; empty by default.</summary>
public Dictionary<QuotaResource, long> QuotaLimits { get; set; } = new();
public void AddValidity(int months) =>
ValidUpto = ValidUpto.AddMonths(months);
public void SetValidity(in DateTime validTill)
{
var normalized = validTill;
ValidUpto = ValidUpto < normalized
? normalized
: throw new InvalidOperationException("Subscription cannot be backdated.");
}
public void Activate()
{
if (Id == MultitenancyConstants.Root.Id)
{
throw new InvalidOperationException("Invalid Tenant");
}
IsActive = true;
}
public void Deactivate()
{
if (Id == MultitenancyConstants.Root.Id)
{
throw new InvalidOperationException("Invalid Tenant");
}View on GitHub (pinned to 3f2959e683)