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

  1. Only call SetValidity with a date later than the current ValidUpto — check before calling.
  2. In seeders, make expiry dates relative to UtcNow instead of hard-coded.
  3. Add a separate explicit downgrade/shorten method if backdating is ever legitimately required.
  4. 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

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


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)