diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/IUsageGate.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/IUsageGate.cs index d729b687c..f91dc1e2b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/IUsageGate.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/IUsageGate.cs @@ -28,10 +28,10 @@ public interface IUsageGate Task DeleteAssetsUsageAsync( CancellationToken ct = default); - Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, + Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, bool canCache, CancellationToken ct = default); - Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, + Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, bool canCache, CancellationToken ct = default); Task<(Plan Plan, string PlanId)> GetPlanForTeamAsync(ITeamEntity team, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs index ae296789a..bc92668cd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageGate.cs @@ -134,7 +134,7 @@ public sealed class UsageGate : IUsageGate, IAssetUsageTracker Guard.NotNull(app); // Resolve the plan from either the app or the assigned team. - var (plan, _, teamId) = await GetPlanForAppAsync(app, ct); + var (plan, _, teamId) = await GetPlanForAppAsync(app, true, ct); var appId = app.Id; var blocking = false; @@ -222,7 +222,7 @@ public sealed class UsageGate : IUsageGate, IAssetUsageTracker usageTracker.TrackAsync(SummaryDate, appKey, null, counters, ct) }; - var (_, _, teamId) = await GetPlanForAppAsync(appId, ct); + var (_, _, teamId) = await GetPlanForAppAsync(appId, true, ct); if (teamId != null) { @@ -235,12 +235,17 @@ public sealed class UsageGate : IUsageGate, IAssetUsageTracker await Task.WhenAll(tasks); } - public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, + public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(IAppEntity app, bool canCache, CancellationToken ct = default) { Guard.NotNull(app); - return memoryCache.GetOrCreateAsync(app, async x => + if (!canCache) + { + return GetPlanCoreAsync(app, ct); + } + + return memoryCache.GetOrCreateAsync(CacheKey(app.Id), async x => { x.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); @@ -248,10 +253,15 @@ public sealed class UsageGate : IUsageGate, IAssetUsageTracker }); } - public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, + public Task<(Plan Plan, string PlanId, DomainId? TeamId)> GetPlanForAppAsync(DomainId appId, bool canCache, CancellationToken ct = default) { - return memoryCache.GetOrCreateAsync(appId, async x => + if (!canCache) + { + return GetPlanCoreAsync(appId, ct); + } + + return memoryCache.GetOrCreateAsync(CacheKey(appId), async x => { x.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); @@ -310,4 +320,9 @@ public sealed class UsageGate : IUsageGate, IAssetUsageTracker { return $"{appId}_TeamAssets"; } + + private static string CacheKey(DomainId appId) + { + return $"{appId}_Plan"; + } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 38fcef7de..9d6885aad 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -6,7 +6,6 @@ // ========================================================================== using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Billing; @@ -48,14 +47,9 @@ public sealed class AppContributorsController : ApiController [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContributorsRead)] [ApiCosts(0)] - public IActionResult GetContributors(string app) + public async Task GetContributors(string app) { - var response = Deferred.AsyncResponse(() => - { - return GetResponseAsync(App, false); - }); - - Response.Headers[HeaderNames.ETag] = App.ToEtag(); + var response = await GetResponseAsync(App, false); return Ok(response); } @@ -145,7 +139,7 @@ public sealed class AppContributorsController : ApiController private async Task GetResponseAsync(IAppEntity app, bool invited) { - var (plan, _, _) = await usageGate.GetPlanForAppAsync(app, HttpContext.RequestAborted); + var (plan, _, _) = await usageGate.GetPlanForAppAsync(app, false, HttpContext.RequestAborted); return await ContributorsDto.FromDomainAsync(app, Resources, userResolver, plan, invited); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 38160e8b8..fc1d6592f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -469,7 +469,7 @@ public sealed class AssetsController : ApiController throw new ValidationException(error); } - var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); + var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, true, HttpContext.RequestAborted); var currentSize = await assetUsageTracker.GetTotalSizeByAppAsync(AppId, HttpContext.RequestAborted); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 7dd2a7ac0..32ed27c67 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -60,7 +60,7 @@ public sealed class AppPlansController : ApiController var (plan, link, referral) = await AsyncHelper.WhenAll( - usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted), + usageGate.GetPlanForAppAsync(App, false, HttpContext.RequestAborted), billingManager.GetPortalLinkAsync(UserId, App, HttpContext.RequestAborted), billingManager.GetReferralInfoAsync(UserId, App, HttpContext.RequestAborted)); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index d4877a38c..b6cfeac17 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -126,7 +126,7 @@ public sealed class UsagesController : ApiController var (summary, details) = await usageTracker.QueryAsync(AppId.ToString(), fromDate.Date, toDate.Date, HttpContext.RequestAborted); // Use the current app plan to show the limits to the user. - var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); + var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, false, HttpContext.RequestAborted); var response = CallsUsageDtoDto.FromDomain(plan, summary, details); @@ -185,7 +185,7 @@ public sealed class UsagesController : ApiController var size = await assetStatsRepository.GetTotalSizeByAppAsync(AppId, HttpContext.RequestAborted); // Use the current app plan to show the limits to the user. - var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, HttpContext.RequestAborted); + var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, false, HttpContext.RequestAborted); var response = new CurrentStorageDto { Size = size, MaxAllowed = plan.MaxAssetSize }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs index 02dfaa6ed..27ec8683a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageGateTests.cs @@ -82,7 +82,7 @@ public class UsageGateTests [Fact] public async Task Should_get_free_plan_for_app() { - var plan = await sut.GetPlanForAppAsync(appWithoutTeam, ct); + var plan = await sut.GetPlanForAppAsync(appWithoutTeam, false, ct); Assert.Equal((planFree, planFree.Id, null), plan); } @@ -98,7 +98,7 @@ public class UsageGateTests A.CallTo(() => team.Id) .Returns(teamId); - var plan = await sut.GetPlanForAppAsync(appWithTeam, ct); + var plan = await sut.GetPlanForAppAsync(appWithTeam, false, ct); Assert.Equal((planFree, planFree.Id, teamId), plan); } @@ -109,7 +109,7 @@ public class UsageGateTests A.CallTo(() => appWithoutTeam.Plan) .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); - var plan = await sut.GetPlanForAppAsync(appWithoutTeam, ct); + var plan = await sut.GetPlanForAppAsync(appWithoutTeam, false, ct); Assert.Equal((planPaid, planPaid.Id, null), plan); } @@ -123,7 +123,7 @@ public class UsageGateTests A.CallTo(() => appWithoutTeam.Plan) .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); - var plan = await sut.GetPlanForAppAsync(appWithoutTeam.Id, ct); + var plan = await sut.GetPlanForAppAsync(appWithoutTeam.Id, false, ct); Assert.Equal((planPaid, planPaid.Id, null), plan); } @@ -142,7 +142,7 @@ public class UsageGateTests A.CallTo(() => team.Plan) .Returns(new AssignedPlan(RefToken.User("1"), planPaid.Id)); - var plan = await sut.GetPlanForAppAsync(appWithTeam, ct); + var plan = await sut.GetPlanForAppAsync(appWithTeam, false, ct); Assert.Equal((planPaid, planPaid.Id, teamId), plan); }