Browse Source

Refactored Localization Aggregation to use InMemory cache with abstraction

pull/235/head
Galip Tolga Erdem 3 years ago
parent
commit
8e8b1dd7cd
  1. 93
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/AggregateRemoteServiceBase.cs
  2. 34
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/AggregateServiceBase.cs
  3. 41
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/CachedServiceBase.cs
  4. 12
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/IAggregateRemoteService.cs
  5. 9
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/ICachedServiceBase.cs
  6. 8
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/IRequestInput.cs
  7. 12
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ILocalizationAggregation.cs
  8. 30
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/CachedLocalizationService.cs
  9. 11
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/ILocalizationAggregation.cs
  10. 6
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/ILocalizationRemoteService.cs
  11. 10
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/IRemoteLocalizationService.cs
  12. 72
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationAggregation.cs
  13. 9
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationCachedService.cs
  14. 15
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationRemoteService.cs
  15. 15
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationRequest.cs
  16. 83
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/RemoteLocalizationService.cs
  17. 105
      gateways/web/src/EShopOnAbp.WebGateway/Aggregations/LocalizationAggregation.cs

93
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/AggregateRemoteServiceBase.cs

@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace EShopOnAbp.WebGateway.Aggregations.Base;
public abstract class AggregateRemoteServiceBase<TDto> : IAggregateRemoteService<TDto>
{
private readonly ILogger<AggregateRemoteServiceBase<TDto>> _logger;
protected JsonSerializerOptions JsonSerializerOptions { get; }
protected AggregateRemoteServiceBase(ILogger<AggregateRemoteServiceBase<TDto>> logger)
{
_logger = logger;
JsonSerializerOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
public async Task<Dictionary<string, TDto>> GetMultipleAsync(
Dictionary<string, string> serviceNameWithUrlDictionary)
{
Dictionary<string, Task<TDto>> completedTasks = new Dictionary<string, Task<TDto>>();
Dictionary<string, Task<TDto>> runningTasks = new Dictionary<string, Task<TDto>>();
Dictionary<string, TDto> completedResult = new Dictionary<string, TDto>();
using (HttpClient httpClient = new HttpClient())
{
foreach (var service in serviceNameWithUrlDictionary)
{
Task<TDto> requestTask =
MakeRequestAsync<TDto>(httpClient, service.Value);
runningTasks.Add(service.Key, requestTask);
}
while (runningTasks.Count > 0)
{
KeyValuePair<string, Task<TDto>> completedTask = await WaitForAnyTaskAsync(runningTasks);
runningTasks.Remove(completedTask.Key);
try
{
TDto result = await completedTask.Value;
completedTasks.Add(completedTask.Key, completedTask.Value);
completedResult.Add(completedTask.Key, result);
_logger.LogInformation($"Localization Key: {completedTask.Key}, Value: {result}");
}
catch (Exception ex)
{
_logger.LogInformation($"Error for the {completedTask.Key}: {ex.Message}");
}
}
}
return completedResult;
}
public async Task<T> MakeRequestAsync<T>(HttpClient httpClient, string url)
{
try
{
HttpResponseMessage response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(content, JsonSerializerOptions);
}
catch (Exception e)
{
_logger.LogInformation($"Error making request to {url}: {e.Message}");
throw;
}
}
public async Task<KeyValuePair<TKey, Task<TValue>>> WaitForAnyTaskAsync<TKey, TValue>(
Dictionary<TKey, Task<TValue>> tasks)
{
var completedTask = Task.WhenAny(tasks.Values);
var result = await completedTask;
var completedTaskPair = tasks.First(kv => kv.Value == result);
return completedTaskPair;
}
}

34
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/AggregateServiceBase.cs

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace EShopOnAbp.WebGateway.Aggregations.Base;
public abstract class AggregateServiceBase<TDto>
{
private readonly IAggregateRemoteService<TDto> _remoteService;
public AggregateServiceBase(IAggregateRemoteService<TDto> remoteService)
{
_remoteService = remoteService;
}
public virtual async Task<Dictionary<string, TDto>> GetMultipleFromRemoteAsync(List<string> missingKeys,
Dictionary<string, string> endpoints)
{
return await _remoteService
.GetMultipleAsync(endpoints
.Where(kv => missingKeys.Contains(kv.Key))
.ToDictionary(k => k.Key, v => v.Value));
}
public List<string> GetMissingServiceKeys(
IDictionary<string, TDto> serviceNamesWithData,
Dictionary<string, string> serviceNamesWithUrls)
{
List<string> missingKeysInCache = serviceNamesWithUrls.Keys.Except(serviceNamesWithData.Keys).ToList();
List<string> missingKeysInUrls = serviceNamesWithData.Keys.Except(serviceNamesWithUrls.Keys).ToList();
return missingKeysInCache.Concat(missingKeysInUrls).ToList();
}
}

41
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/CachedServiceBase.cs

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Caching.Memory;
namespace EShopOnAbp.WebGateway.Aggregations.Base;
public abstract class CachedServiceBase<TCacheValue> : ICachedServiceBase<TCacheValue>
{
private readonly IMemoryCache _cache;
protected MemoryCacheEntryOptions CacheEntryOptions { get; } = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24),
SlidingExpiration = TimeSpan.FromHours(4)
};
protected CachedServiceBase(IMemoryCache cache)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
}
public void Add(string serviceName, TCacheValue data)
{
_cache.Set(serviceName, data, CacheEntryOptions);
}
public IDictionary<string, TCacheValue> GetManyAsync(IEnumerable<string> serviceNames)
{
var result = new Dictionary<string, TCacheValue>();
foreach (var serviceName in serviceNames)
{
if (_cache.TryGetValue(serviceName, out TCacheValue data))
{
result.Add(serviceName, data);
}
}
return result;
}
}

12
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/IAggregateRemoteService.cs

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
namespace EShopOnAbp.WebGateway.Aggregations.Base;
public interface IAggregateRemoteService<TDto>
{
Task<Dictionary<string, TDto>> GetMultipleAsync(Dictionary<string, string> serviceNameWithUrlDictionary);
Task<T> MakeRequestAsync<T>(HttpClient httpClient, string url);
Task<KeyValuePair<TKey, Task<TValue>>> WaitForAnyTaskAsync<TKey, TValue>(Dictionary<TKey, Task<TValue>> tasks);
}

9
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/ICachedServiceBase.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace EShopOnAbp.WebGateway.Aggregations.Base;
public interface ICachedServiceBase<TValue>
{
void Add(string serviceName, TValue data);
IDictionary<string, TValue> GetManyAsync(IEnumerable<string> serviceNames);
}

8
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/IRequestInput.cs

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace EShopOnAbp.WebGateway.Aggregations.Base;
public interface IRequestInput
{
Dictionary<string, string> Endpoints { get; }
}

12
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ILocalizationAggregation.cs

@ -1,12 +0,0 @@
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.DependencyInjection;
namespace EShopOnAbp.WebGateway.Aggregations;
public interface ILocalizationAggregation : ITransientDependency
{
public string LocalizationRouteName { get; }
public string LocalizationEndpoint { get; }
public Task<ApplicationLocalizationDto> GetLocalizationAsync(LocalizationRequest input);
}

30
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/CachedLocalizationService.cs

@ -1,30 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.DependencyInjection;
namespace EShopOnAbp.WebGateway.Aggregations.Localization;
public class CachedLocalizationService : ISingletonDependency
{
protected static ConcurrentDictionary<string, ApplicationLocalizationDto> LocalizationDictionary { get; private set; }
public CachedLocalizationService()
{
LocalizationDictionary = new ConcurrentDictionary<string, ApplicationLocalizationDto>(StringComparer.OrdinalIgnoreCase);
}
public void AddOrUpdate(string serviceNameWithCulture, ApplicationLocalizationDto localizationInfo)
{
LocalizationDictionary.AddOrUpdate(serviceNameWithCulture, localizationInfo, (key, value) => localizationInfo);
}
public IDictionary<string, ApplicationLocalizationDto> GetLocalizationsFromCacheAsync(string[] serviceNamesWithCulture)
{
return LocalizationDictionary
.Where(kv => serviceNamesWithCulture.Contains(kv.Key))
.ToDictionary();
}
}

11
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/ILocalizationAggregation.cs

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
namespace EShopOnAbp.WebGateway.Aggregations.Localization;
public interface ILocalizationAggregation
{
string RouteName { get; }
string Endpoint { get; }
Task<ApplicationLocalizationDto> GetAsync(LocalizationRequest input);
}

6
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/ILocalizationRemoteService.cs

@ -0,0 +1,6 @@
using EShopOnAbp.WebGateway.Aggregations.Base;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
namespace EShopOnAbp.WebGateway.Aggregations.Localization;
public interface ILocalizationRemoteService : IAggregateRemoteService<ApplicationLocalizationDto>;

10
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/IRemoteLocalizationService.cs

@ -1,10 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
namespace EShopOnAbp.WebGateway.Aggregations.Localization;
public interface IRemoteLocalizationService
{
Task<Dictionary<string, ApplicationLocalizationDto>> GetMultipleLocalizationsAsync(Dictionary<string, string> serviceNameWithUrlDictionary);
}

72
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationAggregation.cs

@ -0,0 +1,72 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EShopOnAbp.WebGateway.Aggregations.Base;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.DependencyInjection;
namespace EShopOnAbp.WebGateway.Aggregations.Localization;
public class LocalizationAggregation : AggregateServiceBase<ApplicationLocalizationDto>, ILocalizationAggregation,
ITransientDependency
{
public string RouteName => "EshopOnAbpLocalization";
public string Endpoint => "api/abp/application-localization";
protected LocalizationCachedService LocalizationCachedService { get; }
public LocalizationAggregation(
LocalizationCachedService localizationCachedService,
ILocalizationRemoteService localizationRemoteService)
: base(localizationRemoteService)
{
LocalizationCachedService = localizationCachedService;
}
public async Task<ApplicationLocalizationDto> GetAsync(LocalizationRequest input)
{
// Check the cache service
var cachedLocalization = LocalizationCachedService
.GetManyAsync(input.Endpoints.Keys.ToArray());
// Compare cache with input service list
var missingLocalizationKeys = GetMissingServiceKeys(cachedLocalization, input.Endpoints);
if (missingLocalizationKeys.Count != 0)
{
// Make request to remote localization service to get missing localizations
var remoteLocalizationResults =
await GetMultipleFromRemoteAsync(missingLocalizationKeys, input.Endpoints);
// Update localization cache
foreach (var result in remoteLocalizationResults)
{
LocalizationCachedService.Add(result.Key, result.Value);
}
cachedLocalization = LocalizationCachedService
.GetManyAsync(input.Endpoints.Keys.ToArray());
}
//merge result
var mergedResult = MergeLocalizationData(cachedLocalization);
//return result
return mergedResult;
}
private static ApplicationLocalizationDto MergeLocalizationData(
IDictionary<string, ApplicationLocalizationDto> resourceDictionary)
{
var localizationDto = new ApplicationLocalizationDto();
foreach (var localization in resourceDictionary)
{
foreach (var resource in localization.Value.Resources)
{
localizationDto.Resources.TryAdd(resource.Key, resource.Value);
}
}
return localizationDto;
}
}

9
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationCachedService.cs

@ -0,0 +1,9 @@
using EShopOnAbp.WebGateway.Aggregations.Base;
using Microsoft.Extensions.Caching.Memory;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.DependencyInjection;
namespace EShopOnAbp.WebGateway.Aggregations.Localization;
public class LocalizationCachedService(IMemoryCache localizationCache)
: CachedServiceBase<ApplicationLocalizationDto>(localizationCache), ISingletonDependency;

15
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationRemoteService.cs

@ -0,0 +1,15 @@
using EShopOnAbp.WebGateway.Aggregations.Base;
using Microsoft.Extensions.Logging;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.DependencyInjection;
namespace EShopOnAbp.WebGateway.Aggregations.Localization;
public class LocalizationRemoteService : AggregateRemoteServiceBase<ApplicationLocalizationDto>,
ILocalizationRemoteService, ITransientDependency
{
public LocalizationRemoteService(ILogger<AggregateRemoteServiceBase<ApplicationLocalizationDto>> logger) :
base(logger)
{
}
}

15
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationRequest.cs

@ -0,0 +1,15 @@
using System.Collections.Generic;
using EShopOnAbp.WebGateway.Aggregations.Base;
namespace EShopOnAbp.WebGateway.Aggregations.Localization;
public class LocalizationRequest : IRequestInput
{
public Dictionary<string, string> Endpoints { get; } = new();
public string CultureName { get; set; }
public LocalizationRequest(string cultureName)
{
CultureName = cultureName;
}
}

83
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/RemoteLocalizationService.cs

@ -1,83 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.DependencyInjection;
namespace EShopOnAbp.WebGateway.Aggregations.Localization;
public class RemoteLocalizationService : IRemoteLocalizationService, ITransientDependency
{
private readonly ILogger<RemoteLocalizationService> _logger;
public RemoteLocalizationService(ILogger<RemoteLocalizationService> logger)
{
_logger = logger;
}
public async Task<Dictionary<string, ApplicationLocalizationDto>> GetMultipleLocalizationsAsync(Dictionary<string, string> serviceNameWithUrlDictionary)
{
Dictionary<string, Task<ApplicationLocalizationDto>> completedTasks = new Dictionary<string, Task<ApplicationLocalizationDto>>();
Dictionary<string, Task<ApplicationLocalizationDto>> runningTasks = new Dictionary<string, Task<ApplicationLocalizationDto>>();
Dictionary<string, ApplicationLocalizationDto> completedResult = new Dictionary<string, ApplicationLocalizationDto>();
using (HttpClient httpClient = new HttpClient())
{
foreach (var service in serviceNameWithUrlDictionary)
{
Task<ApplicationLocalizationDto> requestTask = MakeRequestAsync<ApplicationLocalizationDto>(httpClient, service.Value);
runningTasks.Add(service.Key, requestTask);
}
while (runningTasks.Count > 0)
{
KeyValuePair<string, Task<ApplicationLocalizationDto>> completedTask = await WaitForAnyTaskAsync(runningTasks);
runningTasks.Remove(completedTask.Key);
try
{
ApplicationLocalizationDto result = await completedTask.Value;
completedTasks.Add(completedTask.Key, completedTask.Value);
completedResult.Add(completedTask.Key, result);
_logger.LogInformation($"Localization Key: {completedTask.Key}, Value: {result}");
}
catch (Exception ex)
{
_logger.LogInformation($"Error for the {completedTask.Key}: {ex.Message}");
}
}
}
return completedResult;
}
private async Task<KeyValuePair<string, Task<ApplicationLocalizationDto>>> WaitForAnyTaskAsync(Dictionary<string, Task<ApplicationLocalizationDto>> tasks)
{
Task<Task<ApplicationLocalizationDto>> completedTask = Task.WhenAny(tasks.Values);
Task<ApplicationLocalizationDto> result = await completedTask;
KeyValuePair<string, Task<ApplicationLocalizationDto>> completedTaskPair = tasks.First(kv => kv.Value == result);
return completedTaskPair;
}
private async Task<T> MakeRequestAsync<T>(HttpClient httpClient, string url)
{
HttpResponseMessage response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
return JsonSerializer.Deserialize<T>(content, options);
}
}

105
gateways/web/src/EShopOnAbp.WebGateway/Aggregations/LocalizationAggregation.cs

@ -1,105 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EShopOnAbp.WebGateway.Aggregations.Localization;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
namespace EShopOnAbp.WebGateway.Aggregations;
public class LocalizationAggregation : ILocalizationAggregation
{
//Yarp localization route name
public string LocalizationRouteName { get; } = "EshopOnAbpLocalization";
public string LocalizationEndpoint { get; } = "api/abp/application-localization";
private readonly CachedLocalizationService _cachedLocalizationService;
private readonly IRemoteLocalizationService _remoteLocalizationService;
public LocalizationAggregation(CachedLocalizationService cachedLocalizationService,
IRemoteLocalizationService remoteLocalizationService)
{
_cachedLocalizationService = cachedLocalizationService;
_remoteLocalizationService = remoteLocalizationService;
}
public async Task<ApplicationLocalizationDto> GetLocalizationAsync(LocalizationRequest input)
{
// Check the cache service
var cachedLocalization = _cachedLocalizationService
.GetLocalizationsFromCacheAsync(input.LocalizationEndpoints.Keys.ToArray());
// Compare cache with input service list
var missingLocalizationKeys = GetMissingLocalizations(cachedLocalization, input.LocalizationEndpoints);
if (missingLocalizationKeys.Count != 0)
{
// Make request to remote localization service to get missing localizations
var remoteLocalizationResults =
await GetLocalizationsFromRemoteAsync(missingLocalizationKeys, input.LocalizationEndpoints);
// Update localization cache
foreach (var result in remoteLocalizationResults)
{
_cachedLocalizationService.AddOrUpdate(result.Key, result.Value);
}
cachedLocalization = _cachedLocalizationService
.GetLocalizationsFromCacheAsync(input.LocalizationEndpoints.Keys.ToArray());
}
//merge result
ApplicationLocalizationDto mergedResult = MergeLocalizations(cachedLocalization);
//return result
return mergedResult;
}
private async Task<Dictionary<string, ApplicationLocalizationDto>> GetLocalizationsFromRemoteAsync(
List<string> missingLocalizationKeys, Dictionary<string, string> localizationEndpoints)
{
return await _remoteLocalizationService
.GetMultipleLocalizationsAsync(localizationEndpoints
.Where(kv => missingLocalizationKeys.Contains(kv.Key))
.ToDictionary(k => k.Key, v => v.Value));
}
private List<string> GetMissingLocalizations(
IDictionary<string, ApplicationLocalizationDto> serviceNameWithLocalization,
Dictionary<string, string> serviceNameWithUrls)
{
List<string> missingKeysInCache = serviceNameWithUrls.Keys.Except(serviceNameWithLocalization.Keys).ToList();
List<string> missingKeysInUrls = serviceNameWithLocalization.Keys.Except(serviceNameWithUrls.Keys).ToList();
return missingKeysInCache.Concat(missingKeysInUrls).ToList();
}
private ApplicationLocalizationDto MergeLocalizations(
IDictionary<string, ApplicationLocalizationDto> localizationResults)
{
var localizationDto = new ApplicationLocalizationDto();
foreach (var localization in localizationResults)
{
foreach (var resource in localization.Value.Resources)
{
localizationDto.Resources.TryAdd(resource.Key, resource.Value);
}
}
return localizationDto;
}
}
public class LocalizationRequest
{
// Localization clusterName - localization endpoint pair
public Dictionary<string, string> LocalizationEndpoints { get; }
public string CultureName { get; set; }
public LocalizationRequest(string cultureName)
{
CultureName = cultureName;
LocalizationEndpoints = new Dictionary<string, string>();
}
}
Loading…
Cancel
Save