4 Answer(s)
-
0
In the ABP SaaS module, features assigned via an edition are linked to the tenant's subscription. When an edition (and its subscription) expires, the expected behavior is that the features provided by that edition should no longer be available to the tenant.
However, by default, the ABP SaaS module does not automatically invalidate or remove the features from the tenant when the edition expires. The features remain accessible unless you implement custom logic to handle feature invalidation upon edition expiration.
To achieve the behavior where features become invalid after the edition expires, you need to implement a mechanism (such as a background job or event handler) that checks for expired subscriptions and updates or removes the tenant's feature values accordingly.
The ABP SaaS module provides extensibility points where you can hook into subscription/edition changes and enforce your own business rules for feature availability.
For more details, refer to the official documentation and related support discussions:
Sources:
- ABP SaaS Module Documentation
- Feature value is disabled at tenant level when it's assigned to an edition
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
-
0
Yes, the subscription has expired.
-
0
If the feature cannot fail, what is the purpose of the expiration?
-
0
Issue Summary
Thank you for reporting this issue. We have investigated and confirmed that edition features continue to work even after the edition subscription expires. This is indeed a bug in the ABP Framework's feature management system.
What's Happening?
When you set an edition with an expiration date for a tenant:
- The
EditionEndDateUtcis correctly stored in the database - The edition expiration date is tracked properly
- However, features associated with that edition remain accessible after the expiration date
This affects subscription-based applications where features should be disabled when a subscription expires.
Root Cause
The issue is in the ABP Framework's
EditionFeatureValueProviderclass, which is responsible for retrieving feature values for tenants based on their edition. Currently, this provider:- ✅ Correctly retrieves the tenant's
EditionId - ❌ Does NOT check if the edition has expired (
EditionEndDateUtc) - ❌ Returns features from expired editions
The SaaS module has the necessary data (
EditionEndDateUtc) and logic (GetActiveEditionId()method) to handle expiration, but the feature system doesn't use this information.
Impact
Affected Scenarios
- Subscription Expiration: Tenants can continue using premium features after their subscription expires
- Trial Periods: Trial editions with time limits don't properly restrict access after expiration
- Payment Integration: When payment subscriptions expire or are canceled, features remain active
- Manual Edition Management: Setting an expiration date doesn't automatically disable features
What Still Works Correctly
- ✅ Edition claims in user tokens are correctly set (expired editions don't appear in claims)
- ✅ Payment subscription events properly update the expiration dates
- ✅ Tenant activation states work as expected
- ✅ Database queries for expired editions work correctly
Immediate Workarounds
While we work on a permanent fix, here are some workarounds you can implement a workaround.
Workaround : Handle Subscription Events
When a subscription is canceled or updated, you can handle these events:
using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus.Distributed; using Volo.Payment.Subscription; public class CustomSubscriptionEventHandler : IDistributedEventHandler<SubscriptionCanceledEto>, IDistributedEventHandler<SubscriptionUpdatedEto>, ITransientDependency { private readonly IDistributedCache<TenantConfigurationCacheItem> _cache; private readonly ILogger<CustomSubscriptionEventHandler> _logger; private readonly IEmailSender _emailSender; public CustomSubscriptionEventHandler( IDistributedCache<TenantConfigurationCacheItem> cache, ILogger<CustomSubscriptionEventHandler> logger, IEmailSender emailSender) { _cache = cache; _logger = logger; _emailSender = emailSender; } public async Task HandleEventAsync(SubscriptionCanceledEto eventData) { var tenantId = Guid.Parse(eventData.ExtraProperties[TenantConsts.TenantIdParameterName]?.ToString()); _logger.LogWarning($"Subscription canceled for tenant {tenantId}"); // *** Set the EditionId of the tenant to null if needed: *** // var tenant = await TenantRepository.FindAsync(tenantId, includeDetails: false); // tenant.EditionId = null; // await TenantRepository.UpdateAsync(tenant); // Clear tenant configuration cache to force feature re-check await InvalidateTenantCacheAsync(tenantId); // Send notification email await _emailSender.SendAsync( to: await GetTenantAdminEmailAsync(tenantId), subject: "Subscription Canceled", body: "Your subscription has been canceled. You have access until the end of your billing period." ); } public async Task HandleEventAsync(SubscriptionUpdatedEto eventData) { var tenantId = Guid.Parse(eventData.ExtraProperties[TenantConsts.TenantIdParameterName]?.ToString()); // Check if subscription expired (PeriodEndDate is in the past) if (eventData.PeriodEndDate.HasValue && eventData.PeriodEndDate < DateTime.UtcNow) { _logger.LogWarning($"Subscription expired for tenant {tenantId}"); // *** Set the EditionId of the tenant to null if needed: *** // var tenant = await TenantRepository.FindAsync(tenantId, includeDetails: false); // tenant.EditionId = null; // await TenantRepository.UpdateAsync(tenant); // Clear cache to force feature re-check await InvalidateTenantCacheAsync(tenantId); // Send expiration notification await _emailSender.SendAsync( to: await GetTenantAdminEmailAsync(tenantId), subject: "Subscription Expired", body: "Your subscription has expired. Please renew to continue using premium features." ); } } private async Task InvalidateTenantCacheAsync(Guid tenantId) { // Clear tenant configuration cache await _cache.RemoveAsync( TenantConfigurationCacheItem.CalculateCacheKey(tenantId, null), considerUow: true ); } private async Task<string> GetTenantAdminEmailAsync(Guid tenantId) { // Implement logic to get tenant admin email return "admin@tenant.com"; } }
Testing Your Workaround
To verify your workaround is working:
Test Case 1: Expired Edition
- Create a test tenant with an edition that has features
- Set the tenant's
EditionEndDateUtcto a past date:var tenant = await TenantRepository.GetAsync(tenantId); tenant.EditionEndDateUtc = DateTime.UtcNow.AddDays(-1); await TenantRepository.UpdateAsync(tenant); - Try to access a feature from that edition
- ✅ Expected: Access should be denied
- ❌ Without fix: Access is still granted
Test Case 2: Active Edition
- Use a tenant with an edition expiring in the future
- Access features from that edition
- ✅ Expected: Access should be granted
Test Case 3: Edition Renewal
- Start with a tenant with an expired edition (blocked access)
- Renew the subscription:
var tenant = await TenantRepository.GetAsync(tenantId); tenant.EditionEndDateUtc = DateTime.UtcNow.AddMonths(1); await TenantRepository.UpdateAsync(tenant); - Access features again
- ✅ Expected: Access should now be granted
Migration Guide for Existing Data
If you have existing tenants with expired editions that should be blocked:
Step 1: Identify Affected Tenants
var expiredTenants = await TenantRepository.GetListAsync( expirationDateMax: DateTime.UtcNow ); foreach (var tenant in expiredTenants) { if (tenant.EditionId != null && tenant.GetActiveEditionId() == null) { Logger.LogInformation($"Tenant {tenant.Name} has expired edition {tenant.EditionId}"); } }Step 2: Decide on Action
For each expired tenant, you can:
Option A: Clear Edition (if they shouldn't have any edition features)
tenant.EditionId = null; tenant.EditionEndDateUtc = null; await TenantRepository.UpdateAsync(tenant);Option B: Set to Free/Trial Edition
var freeEditionId = await GetFreeEditionIdAsync(); tenant.EditionId = freeEditionId; tenant.EditionEndDateUtc = null; // Perpetual free edition await TenantRepository.UpdateAsync(tenant);Option C: Extend Expiration (give them grace period)
tenant.EditionEndDateUtc = DateTime.UtcNow.AddDays(7); // 7 day grace await TenantRepository.UpdateAsync(tenant);Step 3: Notify Affected Users
foreach (var tenant in expiredTenants) { await EmailService.SendAsync( to: await GetTenantAdminEmailAsync(tenant.Id), subject: "Subscription Expired", body: $"Your subscription has expired. Please renew to continue using premium features." ); }Updates and Progress
We'll update this document as we make progress on the fix. You can track:
- This support thread for updates
- ABP GitHub repository for code changes
- Release notes for the version containing the fix
We understand this impacts subscription-based applications, and we're prioritizing a fix. In the meantime, please implement one of the workarounds provided above.
If you need further assistance implementing any of these workarounds, please don't hesitate to reach out.
- The