Skip to content

Commit

Permalink
Moved from IMemoryCache to IDistributedCache, catering for multiple i… (
Browse files Browse the repository at this point in the history
#1115)

* Moved from IMemoryCache to IDistributedCache, catering for multiple intances of the api

* Change store and category info cache to expire on an hourly basis ensuring new stores are picked-up
  • Loading branch information
adrianwium authored Sep 27, 2024
1 parent 45d067a commit 63287f5
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Yoma.Core.Domain.Core.Interfaces
{
public interface IDistributedCacheService
{
T GetOrCreate<T>(string key, Func<T> valueProvider, TimeSpan? slidingExpiration = null, TimeSpan? absoluteExpirationRelativeToNow = null)
where T : class;

Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> valueProvider, TimeSpan? slidingExpiration = null, TimeSpan? absoluteExpirationRelativeToNow = null)
where T : class;

void Remove(string key);

Task RemoveAsync(string key);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Newtonsoft.Json;
using StackExchange.Redis;
using Yoma.Core.Domain.Core.Interfaces;

namespace Yoma.Core.Domain.Core.Services
{
public class DistributedCacheService : IDistributedCacheService
{
#region Class Variables
private readonly IDatabase _database;
#endregion

#region Constructor
public DistributedCacheService(IConnectionMultiplexer connectionMultiplexer)
{
_database = connectionMultiplexer.GetDatabase();
}
#endregion

#region Public Members
public T GetOrCreate<T>(string key, Func<T> valueProvider, TimeSpan? slidingExpiration = null, TimeSpan? absoluteExpirationRelativeToNow = null)
where T : class
{
return GetOrCreateInternalAsync(key, () => Task.FromResult(valueProvider()), slidingExpiration, absoluteExpirationRelativeToNow).Result;
}

public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> valueProvider, TimeSpan? slidingExpiration = null, TimeSpan? absoluteExpirationRelativeToNow = null)
where T : class
{
return await GetOrCreateInternalAsync(key, valueProvider, slidingExpiration, absoluteExpirationRelativeToNow);
}

public void Remove(string key)
{
RemoveInternalAsync(key).Wait();
}

public async Task RemoveAsync(string key)
{
await RemoveInternalAsync(key);
}
#endregion

#region Private Members
private async Task<T> GetOrCreateInternalAsync<T>(string key, Func<Task<T>> valueProvider, TimeSpan? slidingExpiration = null, TimeSpan? absoluteExpirationRelativeToNow = null)
where T : class
{
ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key));
key = key.Trim();

ArgumentNullException.ThrowIfNull(valueProvider, nameof(valueProvider));

if (slidingExpiration.HasValue && absoluteExpirationRelativeToNow.HasValue && slidingExpiration > absoluteExpirationRelativeToNow)
throw new InvalidOperationException("'Sliding Expiration' cannot be longer than 'Absolute Expiration Relative to Now'");

var redisValueWithExpiry = await _database.StringGetWithExpiryAsync(key);

if (redisValueWithExpiry.Value.HasValue)
{
var cachedValue = JsonConvert.DeserializeObject<T>(redisValueWithExpiry.Value!)
?? throw new InvalidOperationException($"Failed to deserialize value for key '{key}'");

if (slidingExpiration.HasValue && redisValueWithExpiry.Expiry.HasValue)
{
var newExpiration = slidingExpiration.Value < redisValueWithExpiry.Expiry.Value
? slidingExpiration.Value
: redisValueWithExpiry.Expiry.Value;

await _database.KeyExpireAsync(key, newExpiration);
}

return cachedValue;
}

var value = await valueProvider();
var serializedValue = JsonConvert.SerializeObject(value);
var expiration = absoluteExpirationRelativeToNow ?? slidingExpiration;

await _database.StringSetAsync(key, serializedValue, expiration);

return value;
}

private async Task RemoveInternalAsync(string key)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key));
key = key.Trim();

await _database.KeyDeleteAsync(key);
}
}
#endregion
}

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using FluentValidation;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Yoma.Core.Domain.Core.Helpers;
using Yoma.Core.Domain.Core.Interfaces;
using Yoma.Core.Domain.Core.Models;
using Yoma.Core.Domain.Marketplace.Interfaces;
using Yoma.Core.Domain.Marketplace.Models;
Expand All @@ -13,7 +13,7 @@ public class StoreAccessControlRuleInfoService : IStoreAccessControlRuleInfoServ
{
#region Class Variables
private readonly AppSettings _appSettings;
private readonly IMemoryCache _memoryCache;
private readonly IDistributedCacheService _distributedCacheService;
private readonly IStoreAccessControlRuleService _storeAccessControlRuleService;
private readonly IMarketplaceService _marketplaceService;
private readonly StoreAccessControlRuleRequestValidatorCreate _storeAccessControlRuleRequestValidatorCreate;
Expand All @@ -22,14 +22,14 @@ public class StoreAccessControlRuleInfoService : IStoreAccessControlRuleInfoServ

#region Constructor
public StoreAccessControlRuleInfoService(IOptions<AppSettings> appSettings,
IMemoryCache memoryCache,
IDistributedCacheService distributedCacheService,
IStoreAccessControlRuleService storeAccessControlRuleService,
IMarketplaceService marketplaceService,
StoreAccessControlRuleRequestValidatorCreate storeAccessControlRuleRequestValidatorCreate,
StoreAccessControlRuleRequestValidatorUpdate storeAccessControlRuleRequestValidatorUpdate)
{
_appSettings = appSettings.Value;
_memoryCache = memoryCache;
_distributedCacheService = distributedCacheService;
_storeAccessControlRuleService = storeAccessControlRuleService;
_marketplaceService = marketplaceService;
_storeAccessControlRuleRequestValidatorCreate = storeAccessControlRuleRequestValidatorCreate;
Expand Down Expand Up @@ -107,9 +107,6 @@ public async Task<StoreAccessControlRuleInfo> Create(StoreAccessControlRuleReque
request.RequestValidationHandled = true;
var result = await _storeAccessControlRuleService.Create(request);

_memoryCache.Remove(CacheHelper.GenerateKey<StoreSearchResults>(result.StoreCountryCodeAlpha2));
_memoryCache.Remove(CacheHelper.GenerateKey<StoreItemCategorySearchResults>(result.StoreId));

return await ToInfo(result);
}

Expand Down Expand Up @@ -147,9 +144,6 @@ public async Task<StoreAccessControlRuleInfo> Update(StoreAccessControlRuleReque
request.RequestValidationHandled = true;
var result = await _storeAccessControlRuleService.Update(request);

_memoryCache.Remove(CacheHelper.GenerateKey<StoreSearchResults>(result.StoreCountryCodeAlpha2));
_memoryCache.Remove(CacheHelper.GenerateKey<StoreItemCategorySearchResults>(result.StoreId));

return await ToInfo(result);
}

Expand Down Expand Up @@ -239,39 +233,41 @@ private async Task<StoreAccessControlRuleInfo> ToInfo(StoreAccessControlRule ite
}

/// <summary>
/// Caches the stores for store info resolution. The cache expires based on the cache settings or when a rule is created or updated. The cache applies to existing rules.
/// When a rule is created or updated, all stores become selectable, so expiring the store info cache is necessary to reflect the latest changes.
/// Caches the stores for store info resolution. The cache expires every hour based on sliding expiration settings.
/// </summary>
private async Task<List<Store>> StoresCached(string countryCodeAlpha2)
{
if (!_appSettings.CacheEnabledByCacheItemTypesAsEnum.HasFlag(Core.CacheItemType.Lookups))
return (await _marketplaceService.SearchStores(new StoreSearchFilter { CountryCodeAlpha2 = countryCodeAlpha2 })).Items;

var result = await _memoryCache.GetOrCreateAsync(CacheHelper.GenerateKey<StoreSearchResults>(countryCodeAlpha2), async entry =>
{
entry.SlidingExpiration = TimeSpan.FromHours(_appSettings.CacheSlidingExpirationInHours);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(_appSettings.CacheAbsoluteExpirationRelativeToNowInDays);
return (await _marketplaceService.SearchStores(new StoreSearchFilter { CountryCodeAlpha2 = countryCodeAlpha2 })).Items;
}) ?? throw new InvalidOperationException($"Failed to retrieve cached list of '{nameof(StoreSearchResults)}s'");
var result = await _distributedCacheService.GetOrCreateAsync(
CacheHelper.GenerateKey<StoreSearchResults>(countryCodeAlpha2),
async () => (await _marketplaceService.SearchStores(new StoreSearchFilter { CountryCodeAlpha2 = countryCodeAlpha2 })).Items,
null, // No absolute expiration
TimeSpan.FromHours(_appSettings.CacheSlidingExpirationInHours) // Sliding expiration as absolute ensures new stores/categories are picked up
) ?? throw new InvalidOperationException($"Failed to retrieve cached list of '{nameof(StoreSearchResults)}s'");

return result;
}

/// <summary>
/// Caches the store item categories for store item category resolution. The cache expires based on the cache settings or when a rule is created or updated. The cache applies to existing rules.
/// When a rule is created or updated, all store item categories become selectable, so expiring the store item category cache is necessary to reflect the latest changes.
/// Caches the categories for store item category resolution. The cache expires every hour based on sliding expiration settings
/// </summary>
private async Task<List<StoreItemCategory>> StoreItemCategoriesCached(string storeId)
{
if (!_appSettings.CacheEnabledByCacheItemTypesAsEnum.HasFlag(Core.CacheItemType.Lookups))
return (await _marketplaceService.SearchStoreItemCategories(new StoreItemCategorySearchFilter { StoreId = storeId, EvaluateStoreAccessControlRules = false })).Items;

var result = await _memoryCache.GetOrCreateAsync(CacheHelper.GenerateKey<StoreItemCategorySearchResults>(storeId), async entry =>
{
entry.SlidingExpiration = TimeSpan.FromHours(_appSettings.CacheSlidingExpirationInHours);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(_appSettings.CacheAbsoluteExpirationRelativeToNowInDays);
return (await _marketplaceService.SearchStoreItemCategories(new StoreItemCategorySearchFilter { StoreId = storeId, EvaluateStoreAccessControlRules = false })).Items;
}) ?? throw new InvalidOperationException($"Failed to retrieve cached list of '{nameof(StoreItemCategorySearchResults)}s'");
var result = await _distributedCacheService.GetOrCreateAsync(
CacheHelper.GenerateKey<StoreItemCategorySearchResults>(storeId),
async () => (await _marketplaceService.SearchStoreItemCategories(new StoreItemCategorySearchFilter
{
StoreId = storeId,
EvaluateStoreAccessControlRules = false
})).Items,
null, // No absolute expiration
TimeSpan.FromHours(_appSettings.CacheSlidingExpirationInHours) // Sliding expiration as absolute ensures new stores/categories are picked up
) ?? throw new InvalidOperationException($"Failed to retrieve cached list of '{nameof(StoreItemCategorySearchResults)}s'");

return result;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using FluentValidation;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Transactions;
Expand Down Expand Up @@ -29,7 +28,7 @@ public class StoreAccessControlRuleService : IStoreAccessControlRuleService
{
#region Class Variables
private readonly AppSettings _appSettings;
private readonly IMemoryCache _memoryCache;
private readonly IDistributedCacheService _distributedCacheService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IStoreAccessControlRuleStatusService _storeAccessControlRuleStatusService;
private readonly IOrganizationService _organizationService;
Expand All @@ -56,7 +55,7 @@ public class StoreAccessControlRuleService : IStoreAccessControlRuleService

#region Constructor
public StoreAccessControlRuleService(IOptions<AppSettings> appSettings,
IMemoryCache memoryCache,
IDistributedCacheService distributedCacheService,
IHttpContextAccessor httpContextAccessor,
IStoreAccessControlRuleStatusService storeAccessControlRuleStatusService,
IOrganizationService organizationService,
Expand All @@ -76,7 +75,7 @@ public StoreAccessControlRuleService(IOptions<AppSettings> appSettings,
IExecutionStrategyService executionStrategyService)
{
_appSettings = appSettings.Value;
_memoryCache = memoryCache;
_distributedCacheService = distributedCacheService;
_httpContextAccessor = httpContextAccessor;
_storeAccessControlRuleStatusService = storeAccessControlRuleStatusService;
_organizationService = organizationService;
Expand Down Expand Up @@ -315,7 +314,7 @@ await _executionStrategyService.ExecuteInExecutionStrategyAsync(async () =>
if (result.Opportunities?.Count == 0) result.Opportunities = null;
result.Opportunities = result.Opportunities?.OrderBy(o => o.Title).ToList();

_memoryCache.Remove(CacheHelper.GenerateKey<StoreAccessControlRule>());
_distributedCacheService.Remove(CacheHelper.GenerateKey<StoreAccessControlRule>());

return result;
}
Expand Down Expand Up @@ -410,7 +409,7 @@ await _executionStrategyService.ExecuteInExecutionStrategyAsync(async () =>
if (result.Opportunities?.Count == 0) result.Opportunities = null;
result.Opportunities = result.Opportunities?.OrderBy(o => o.Title).ToList();

_memoryCache.Remove(CacheHelper.GenerateKey<StoreAccessControlRule>());
_distributedCacheService.Remove(CacheHelper.GenerateKey<StoreAccessControlRule>());

return result;
}
Expand Down Expand Up @@ -449,7 +448,7 @@ public async Task<StoreAccessControlRule> UpdateStatus(Guid id, StoreAccessContr

result = await _storeAccessControlRuleRepistory.Update(result);

_memoryCache.Remove(CacheHelper.GenerateKey<StoreAccessControlRule>());
_distributedCacheService.Remove(CacheHelper.GenerateKey<StoreAccessControlRule>());

return result;
}
Expand Down Expand Up @@ -794,15 +793,19 @@ private async Task AssignOpportunities(List<Opportunity.Models.Opportunity>? opp
#region Private Members
private List<StoreAccessControlRule> RulesUpdatableCached()
{
if (!_appSettings.CacheEnabledByCacheItemTypesAsEnum.HasFlag(Core.CacheItemType.Lookups))
if (!_appSettings.CacheEnabledByCacheItemTypesAsEnum.HasFlag(CacheItemType.Lookups))
return Search(new StoreAccessControlRuleSearchFilter { NonPaginatedQuery = true, Statuses = [.. Statuses_Updatable] }, false).Items;

var result = _memoryCache.GetOrCreate(CacheHelper.GenerateKey<StoreAccessControlRule>(), entry =>
{
entry.SlidingExpiration = TimeSpan.FromHours(_appSettings.CacheSlidingExpirationInHours);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(_appSettings.CacheAbsoluteExpirationRelativeToNowInDays);
return Search(new StoreAccessControlRuleSearchFilter { NonPaginatedQuery = true, Statuses = [.. Statuses_Updatable] }, false).Items;
}) ?? throw new InvalidOperationException($"Failed to retrieve cached list of '{nameof(StoreAccessControlRule)}s'");
var result = _distributedCacheService.GetOrCreate(
CacheHelper.GenerateKey<StoreAccessControlRule>(),
() => Search(new StoreAccessControlRuleSearchFilter
{
NonPaginatedQuery = true,
Statuses = [.. Statuses_Updatable]
}, false).Items,
TimeSpan.FromHours(_appSettings.CacheSlidingExpirationInHours),
TimeSpan.FromDays(_appSettings.CacheAbsoluteExpirationRelativeToNowInDays)
) ?? throw new InvalidOperationException($"Failed to retrieve cached list of '{nameof(StoreAccessControlRule)}s'");

return result;
}
Expand Down
1 change: 1 addition & 0 deletions src/api/src/domain/Yoma.Core.Domain/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public static void ConfigureServices_DomainServices(this IServiceCollection serv

#region Core
services.AddScoped<IBlobService, BlobService>();
services.AddScoped<IDistributedCacheService, DistributedCacheService>();
services.AddScoped<IDistributedLockService, DistributedLockService>();
#endregion Core

Expand Down

0 comments on commit 63287f5

Please sign in to comment.