From 26df42318011da93c3544867ab6212214b49f5be Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 25 Sep 2026 03:04:37 +0200 Subject: [PATCH] Refactored cache services --- .../Abstractions/Services/IProjectService.cs | 2 +- .../Governance/GovernanceCacheService.cs | 108 +++++- .../Services/Projects/ProjectCacheService.cs | 324 +++++++++++++++--- .../Services/Projects/ProjectService.cs | 2 +- .../Services/Shared/IdentityService.cs | 1 + .../Services/Users/UserService.cs | 8 + 6 files changed, 379 insertions(+), 66 deletions(-) diff --git a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs index 4f6e73e..1883fb5 100644 --- a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs @@ -153,5 +153,5 @@ public interface IProjectService Task> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default); [Log] - Task> GetSiteVisitAsync(string SiteVisitIdHash, CancellationToken cancellationToken = default); + Task> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs index 9f630d8..2061276 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs @@ -215,25 +215,101 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut return Result.Ok(cached.FromJson()); })); - public Task> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetTenantAsync(tenantIdHash, cancellationToken); - public Task> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cacheKey = HashService.ToMd5Hash($"gettenant-{tenantIdHash}").Value; - public Task> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); - public Task> GetTenantsAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetTenantAsync(tenantIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetTenantOrganisationAsync(tenantOrganisationIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"gettenantorganisation-{tenantOrganisationIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetTenantOrganisationAsync(tenantOrganisationIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetTenantOrganisationsAsync(cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"gettenantorganisations").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetTenantOrganisationsAsync(cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetTenantsAsync(CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetTenantsAsync(cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"gettenants").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetTenantsAsync(cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) => await service.UpdateOrganisationAsync(request, cancellationToken); diff --git a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs index 4c952e3..d927741 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs @@ -86,45 +86,197 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach return Result.Ok(cached.FromJson()); })); - public Task> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetBeneficiariesAsync(projectIdHash, cancellationToken); - public Task> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cacheKey = HashService.ToMd5Hash($"getbeneficiaries-{projectIdHash}").Value; - public Task> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); - public Task> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetBeneficiariesAsync(projectIdHash, cancellationToken); - public Task> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (result.IsFailed) return result; - public Task> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); - public Task> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return result.Value; + } - public Task> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetBeneficiaryAsync(beneficiaryIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getbeneficiary-{beneficiaryIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetBeneficiaryAsync(beneficiaryIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetChangeRequestAsync(changeRequestIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getchangerequest-{changeRequestIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetChangeRequestAsync(changeRequestIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetChangeRequestsAsync(projectIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getchangerequests-{projectIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetChangeRequestsAsync(projectIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetComplianceItemAsync(complianceItemIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getcomplianceitem-{complianceItemIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetComplianceItemAsync(complianceItemIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetComplianceItemsAsync(projectIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getcomplianceitems-{projectIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetComplianceItemsAsync(projectIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetIssueAsync(issueIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getissue-{issueIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetIssueAsync(issueIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetIssuesAsync(projectIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getissues-{projectIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetIssuesAsync(projectIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); public async Task> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => @@ -174,15 +326,53 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach return Result.Ok(cached.FromJson()); })); - public Task> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetOutcomeAsync(outcomeIdHash, cancellationToken); - public Task> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cacheKey = HashService.ToMd5Hash($"getoutcome-{outcomeIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetOutcomeAsync(outcomeIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetOutcomesAsync(beneficiaryIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getoutcomes-{beneficiaryIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetOutcomesAsync(beneficiaryIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); public async Task> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => @@ -376,15 +566,53 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach return Result.Ok(cached.FromJson()); })); - public Task> GetSiteVisitAsync(string SiteVisitIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetSiteVisitAsync(siteVisitIdHash, cancellationToken); - public Task> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var cacheKey = HashService.ToMd5Hash($"getsitevisit-{siteVisitIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetSiteVisitAsync(siteVisitIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetSiteVisitsAsync(projectIdHash, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getsitevisits-{projectIdHash}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetSiteVisitsAsync(projectIdHash, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); public async Task UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdateBeneficiaryItemAsync(request, userId, cancellationToken); diff --git a/PostFundManagement.Application/Services/Projects/ProjectService.cs b/PostFundManagement.Application/Services/Projects/ProjectService.cs index f7fef6d..d2abeb5 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectService.cs @@ -498,7 +498,7 @@ public sealed class ProjectService(IDbContextFactory conte throw new NotImplementedException(); } - public Task> GetSiteVisitAsync(string SiteVisitIdHash, CancellationToken cancellationToken = default) + public Task> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/PostFundManagement.Application/Services/Shared/IdentityService.cs b/PostFundManagement.Application/Services/Shared/IdentityService.cs index cfe216d..7c668ad 100644 --- a/PostFundManagement.Application/Services/Shared/IdentityService.cs +++ b/PostFundManagement.Application/Services/Shared/IdentityService.cs @@ -12,6 +12,7 @@ public sealed class IdentityService(IDbContextFactory cont { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: Ensure TenantUser status is checked var userExistsAndActive = await context.Users .AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken); diff --git a/PostFundManagement.Application/Services/Users/UserService.cs b/PostFundManagement.Application/Services/Users/UserService.cs index e76caf9..a7ca325 100644 --- a/PostFundManagement.Application/Services/Users/UserService.cs +++ b/PostFundManagement.Application/Services/Users/UserService.cs @@ -14,8 +14,11 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: refactor to use TenantUser var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); + // TODO: refactor to use TenantUser + // TODO: Also add to TenantUser if (user is null) context.Users.Add(new Domain.Entities.User { @@ -40,8 +43,10 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: refactor to use TenantUser var query = context.Users.AsNoTracking(); + // TODO: refactor to use TenantUser if (request.Status.HasValue) query = query.Where(u => u.Status == request.Status.Value); @@ -59,6 +64,7 @@ public sealed class UserService(IDbContextFactory contextF else query = query.OrderBy(u => u.Id); + // TODO: refactor to use TenantUser var users = await query.Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize).ToListAsync(cancellationToken); @@ -70,6 +76,7 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: Update TenantUser status as well var changedTotal = await context.Users .Where(f => f.Id == userId) .ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken); @@ -82,6 +89,7 @@ public sealed class UserService(IDbContextFactory contextF { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + // TODO: refactor to use TenantUser var user = await context.Users.AsNoTracking() .FirstOrDefaultAsync(f => f.Id == userId, cancellationToken);