diff --git a/etc/k8s/eshoponabp/charts/gateway-web/templates/gateway-web-configmap.yaml b/etc/k8s/eshoponabp/charts/gateway-web/templates/gateway-web-configmap.yaml index 9aa5cffd..e62f6a48 100644 --- a/etc/k8s/eshoponabp/charts/gateway-web/templates/gateway-web-configmap.yaml +++ b/etc/k8s/eshoponabp/charts/gateway-web/templates/gateway-web-configmap.yaml @@ -13,6 +13,18 @@ data: "Path": "/api/abp/{**catch-all}" } }, + "EshopOnAbpLocalization": { + "ClusterId": "Administration", + "Match": { + "Path": "/api/abp/application-localization" + } + }, + "EshopOnAbpApplicationConfiguration": { + "ClusterId": "Administration", + "Match": { + "Path": "/api/abp/application-configuration" + } + }, "Identity Service": { "ClusterId": "Identity", "Match": { diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationAggregation.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationAggregation.cs new file mode 100644 index 00000000..54352085 --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationAggregation.cs @@ -0,0 +1,94 @@ +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.ApplicationConfiguration; + +public class AppConfigurationAggregation : AggregateServiceBase, + IAppConfigurationAggregation, ITransientDependency +{ + public string AppConfigRouteName => "EshopOnAbpApplicationConfiguration"; + public string AppConfigEndpoint => "api/abp/application-configuration"; + + protected IAppConfigurationRemoteService AppConfigurationRemoteService { get; } + + public AppConfigurationAggregation( + IAppConfigurationRemoteService appConfigurationRemoteService) : base( + appConfigurationRemoteService) + { + AppConfigurationRemoteService = appConfigurationRemoteService; + } + + public async Task GetAppConfigurationAsync(AppConfigurationRequest input) + { + var remoteAppConfigurationResults = + await AppConfigurationRemoteService.GetMultipleAsync(input.Endpoints); + + //merge only application configuration settings data + var mergedResult = MergeAppConfigurationSettingsData(remoteAppConfigurationResults); + + //return result + return mergedResult; + } + + private static ApplicationConfigurationDto MergeAppConfigurationSettingsData( + IDictionary appConfigurations) + { + var appConfigurationDto = CreateInitialAppConfigDto(appConfigurations); + + foreach (var (_, appConfig) in appConfigurations) + { + foreach (var resource in appConfig.Setting.Values) + { + appConfigurationDto.Setting.Values.TryAdd(resource.Key, resource.Value); + } + } + + return appConfigurationDto; + } + + /// + /// Checks "Administration" clusterId to set the initial data from the AdministrationService. + /// Otherwise uses the first available service for the initial application configuration data + /// + /// + /// + private static ApplicationConfigurationDto CreateInitialAppConfigDto( + IDictionary appConfigurations + ) + { + if (appConfigurations.Count == 0) + { + return new ApplicationConfigurationDto(); + } + + if (appConfigurations.TryGetValue("Administration_AppConfig", out var administrationServiceData)) + { + return MapServiceData(administrationServiceData); + } + + return MapServiceData(appConfigurations.First().Value); + } + + private static ApplicationConfigurationDto MapServiceData(ApplicationConfigurationDto appConfiguration) + { + return new ApplicationConfigurationDto + { + Localization = appConfiguration.Localization, + Auth = appConfiguration.Auth, + Clock = appConfiguration.Clock, + Setting = appConfiguration.Setting, + Features = appConfiguration.Features, + Timing = appConfiguration.Timing, + CurrentTenant = appConfiguration.CurrentTenant, + CurrentUser = appConfiguration.CurrentUser, + ExtraProperties = appConfiguration.ExtraProperties, + GlobalFeatures = appConfiguration.GlobalFeatures, + MultiTenancy = appConfiguration.MultiTenancy, + ObjectExtensions = appConfiguration.ObjectExtensions + }; + } +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationCachedService.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationCachedService.cs new file mode 100644 index 00000000..b51b15af --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationCachedService.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.ApplicationConfiguration; + +public class AppConfigurationCachedService(IMemoryCache applicationConfigurationCache) + : CachedServiceBase(applicationConfigurationCache), ISingletonDependency; \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationRemoteService.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationRemoteService.cs new file mode 100644 index 00000000..14ee74fb --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationRemoteService.cs @@ -0,0 +1,15 @@ +using EShopOnAbp.WebGateway.Aggregations.Base; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace EShopOnAbp.WebGateway.Aggregations.ApplicationConfiguration; + +public class AppConfigurationRemoteService( + IHttpContextAccessor httpContextAccessor, + IJsonSerializer jsonSerializer, + ILogger> logger) + : AggregateRemoteServiceBase(httpContextAccessor, jsonSerializer, logger), + IAppConfigurationRemoteService, ITransientDependency; \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationRequest.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationRequest.cs new file mode 100644 index 00000000..10b23ec0 --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/AppConfigurationRequest.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using EShopOnAbp.WebGateway.Aggregations.Base; + +namespace EShopOnAbp.WebGateway.Aggregations.ApplicationConfiguration; + +public class AppConfigurationRequest : IRequestInput +{ + public Dictionary Endpoints { get; } = new(); +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/IAppConfigurationAggregation.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/IAppConfigurationAggregation.cs new file mode 100644 index 00000000..cd53d8a8 --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/IAppConfigurationAggregation.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +namespace EShopOnAbp.WebGateway.Aggregations.ApplicationConfiguration; + +public interface IAppConfigurationAggregation +{ + string AppConfigRouteName { get; } + string AppConfigEndpoint { get; } + Task GetAppConfigurationAsync(AppConfigurationRequest input); +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/IAppConfigurationRemoteService.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/IAppConfigurationRemoteService.cs new file mode 100644 index 00000000..52987842 --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ApplicationConfiguration/IAppConfigurationRemoteService.cs @@ -0,0 +1,6 @@ +using EShopOnAbp.WebGateway.Aggregations.Base; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +namespace EShopOnAbp.WebGateway.Aggregations.ApplicationConfiguration; + +public interface IAppConfigurationRemoteService : IAggregateRemoteService; \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/AggregateRemoteServiceBase.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/AggregateRemoteServiceBase.cs new file mode 100644 index 00000000..f0e3a799 --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/AggregateRemoteServiceBase.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Volo.Abp.Json; + +namespace EShopOnAbp.WebGateway.Aggregations.Base; + +public abstract class AggregateRemoteServiceBase : IAggregateRemoteService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger> _logger; + protected IJsonSerializer JsonSerializer { get; } + + protected AggregateRemoteServiceBase(IHttpContextAccessor httpContextAccessor, IJsonSerializer jsonSerializer, + ILogger> logger) + { + _httpContextAccessor = httpContextAccessor; + JsonSerializer = jsonSerializer; + _logger = logger; + } + + public async Task> GetMultipleAsync( + Dictionary serviceNameWithUrlDictionary) + { + Dictionary> completedTasks = new Dictionary>(); + Dictionary> runningTasks = new Dictionary>(); + Dictionary completedResult = new Dictionary(); + + using (HttpClient httpClient = CreateHttpClient()) + { + foreach (var service in serviceNameWithUrlDictionary) + { + Task requestTask = + MakeRequestAsync(httpClient, service.Value); + runningTasks.Add(service.Key, requestTask); + } + + while (runningTasks.Count > 0) + { + KeyValuePair> 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: {0}, Value: {1}", completedTask.Key, result); + } + catch (Exception ex) + { + _logger.LogInformation("Error for the {0}: {1}", completedTask.Key, ex.Message); + } + } + } + + return completedResult; + } + + private HttpClient CreateHttpClient() + { + var httpClient = new HttpClient(); + + var headers = _httpContextAccessor.HttpContext?.Request.Headers; + if (headers != null) + { + foreach (var header in headers) + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value.ToArray()); + } + } + + return httpClient; + } + + public async Task MakeRequestAsync(HttpClient httpClient, string url) + { + try + { + HttpResponseMessage response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(content); + } + catch (Exception e) + { + _logger.LogInformation("Error making request to {0}: {1}", url, e.Message); + throw; + } + } + + public async Task>> WaitForAnyTaskAsync( + Dictionary> tasks) + { + var completedTask = Task.WhenAny(tasks.Values); + var result = await completedTask; + + var completedTaskPair = tasks.First(kv => kv.Value == result); + + return completedTaskPair; + } +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/AggregateServiceBase.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/AggregateServiceBase.cs new file mode 100644 index 00000000..c850b4a7 --- /dev/null +++ b/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 +{ + private readonly IAggregateRemoteService _remoteService; + + public AggregateServiceBase(IAggregateRemoteService remoteService) + { + _remoteService = remoteService; + } + + public virtual async Task> GetMultipleFromRemoteAsync(List missingKeys, + Dictionary endpoints) + { + return await _remoteService + .GetMultipleAsync(endpoints + .Where(kv => missingKeys.Contains(kv.Key)) + .ToDictionary(k => k.Key, v => v.Value)); + } + + public List GetMissingServiceKeys( + IDictionary serviceNamesWithData, + Dictionary serviceNamesWithUrls) + { + List missingKeysInCache = serviceNamesWithUrls.Keys.Except(serviceNamesWithData.Keys).ToList(); + List missingKeysInUrls = serviceNamesWithData.Keys.Except(serviceNamesWithUrls.Keys).ToList(); + + return missingKeysInCache.Concat(missingKeysInUrls).ToList(); + } +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/CachedServiceBase.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/CachedServiceBase.cs new file mode 100644 index 00000000..904988be --- /dev/null +++ b/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 : ICachedServiceBase +{ + 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 GetManyAsync(IEnumerable serviceNames) + { + var result = new Dictionary(); + + foreach (var serviceName in serviceNames) + { + if (_cache.TryGetValue(serviceName, out TCacheValue data)) + { + result.Add(serviceName, data); + } + } + + return result; + } +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/IAggregateRemoteService.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/IAggregateRemoteService.cs new file mode 100644 index 00000000..55cfb11f --- /dev/null +++ b/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 +{ + Task> GetMultipleAsync(Dictionary serviceNameWithUrlDictionary); + Task MakeRequestAsync(HttpClient httpClient, string url); + Task>> WaitForAnyTaskAsync(Dictionary> tasks); +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/ICachedServiceBase.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/ICachedServiceBase.cs new file mode 100644 index 00000000..8e5b0caa --- /dev/null +++ b/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 +{ + void Add(string serviceName, TValue data); + IDictionary GetManyAsync(IEnumerable serviceNames); +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/IRequestInput.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Base/IRequestInput.cs new file mode 100644 index 00000000..d9640741 --- /dev/null +++ b/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 Endpoints { get; } +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/ILocalizationAggregation.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/ILocalizationAggregation.cs new file mode 100644 index 00000000..4e827531 --- /dev/null +++ b/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 LocalizationRouteName { get; } + string LocalizationEndpoint { get; } + Task GetLocalizationAsync(LocalizationRequest input); +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/ILocalizationRemoteService.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/ILocalizationRemoteService.cs new file mode 100644 index 00000000..22d58d4e --- /dev/null +++ b/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; \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationAggregation.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationAggregation.cs new file mode 100644 index 00000000..7e31add8 --- /dev/null +++ b/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, ILocalizationAggregation, + ITransientDependency +{ + public string LocalizationRouteName => "EshopOnAbpLocalization"; + public string LocalizationEndpoint => "api/abp/application-localization"; + protected LocalizationCachedService LocalizationCachedService { get; } + + public LocalizationAggregation( + LocalizationCachedService localizationCachedService, + ILocalizationRemoteService localizationRemoteService) + : base(localizationRemoteService) + { + LocalizationCachedService = localizationCachedService; + } + + public async Task GetLocalizationAsync(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 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; + } +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationCachedService.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationCachedService.cs new file mode 100644 index 00000000..3e0c084a --- /dev/null +++ b/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(localizationCache), ISingletonDependency; \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationRemoteService.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationRemoteService.cs new file mode 100644 index 00000000..55db8888 --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationRemoteService.cs @@ -0,0 +1,20 @@ +using EShopOnAbp.WebGateway.Aggregations.Base; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace EShopOnAbp.WebGateway.Aggregations.Localization; + +public class LocalizationRemoteService : AggregateRemoteServiceBase, + ILocalizationRemoteService, ITransientDependency +{ + public LocalizationRemoteService( + IHttpContextAccessor httpContextAccessor, + IJsonSerializer jsonSerializer, + ILogger> logger) + : base(httpContextAccessor, jsonSerializer, logger) + { + } +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationRequest.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/Localization/LocalizationRequest.cs new file mode 100644 index 00000000..509bc195 --- /dev/null +++ b/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 Endpoints { get; } = new(); + public string CultureName { get; set; } + + public LocalizationRequest(string cultureName) + { + CultureName = cultureName; + } +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/EShopOnAbpWebGatewayModule.cs b/gateways/web/src/EShopOnAbp.WebGateway/EShopOnAbpWebGatewayModule.cs index 3033581e..01c51d2a 100644 --- a/gateways/web/src/EShopOnAbp.WebGateway/EShopOnAbpWebGatewayModule.cs +++ b/gateways/web/src/EShopOnAbp.WebGateway/EShopOnAbpWebGatewayModule.cs @@ -53,6 +53,8 @@ public class EShopOnAbpWebGatewayModule : AbpModule .AllowCredentials(); }); }); + + context.Services.AddMemoryCache(); } public override void OnApplicationInitialization(ApplicationInitializationContext context) @@ -66,6 +68,8 @@ public class EShopOnAbpWebGatewayModule : AbpModule } app.UseCorrelationId(); + app.UseCors(); + app.UseAbpRequestLocalization(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); @@ -76,6 +80,9 @@ public class EShopOnAbpWebGatewayModule : AbpModule // Regex for "", "/" and "" (whitespace) .AddRedirect("^(|\\|\\s+)$", "/swagger")); - app.UseEndpoints(endpoints => { endpoints.MapReverseProxy(); }); + app.UseEndpoints(endpoints => + { + endpoints.MapReverseProxyWithLocalization(); + }); } } \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/ReverseProxyBuilderExtensions.cs b/gateways/web/src/EShopOnAbp.WebGateway/ReverseProxyBuilderExtensions.cs new file mode 100644 index 00000000..3dd29485 --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/ReverseProxyBuilderExtensions.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EShopOnAbp.WebGateway.Aggregations.ApplicationConfiguration; +using EShopOnAbp.WebGateway.Aggregations.Localization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Yarp.ReverseProxy.Configuration; + +namespace EShopOnAbp.WebGateway; + +public static class ReverseProxyBuilderExtensions +{ + public static ReverseProxyConventionBuilder MapReverseProxyWithLocalization(this IEndpointRouteBuilder endpoints) + { + return endpoints.MapReverseProxy(proxyBuilder => + { + proxyBuilder.Use(async (context, next) => + { + var endpoint = context.GetEndpoint(); + + var localizationAggregation = context.RequestServices + .GetRequiredService(); + + var appConfigurationAggregation = context.RequestServices + .GetRequiredService(); + + // The "/api/abp/application-localization" endpoint + if (localizationAggregation.LocalizationRouteName == endpoint?.DisplayName) + { + var localizationRequestInput = + CreateLocalizationRequestInput(context, localizationAggregation.LocalizationEndpoint); + + var result = await localizationAggregation.GetLocalizationAsync(localizationRequestInput); + await context.Response.WriteAsJsonAsync(result); + return; + } + + // The "/api/abp/application-configuration" endpoint + if (appConfigurationAggregation.AppConfigRouteName == endpoint?.DisplayName) + { + var appConfigurationRequestInput = + CreateAppConfigurationRequestInput(context, appConfigurationAggregation.AppConfigEndpoint); + + var result = + await appConfigurationAggregation.GetAppConfigurationAsync(appConfigurationRequestInput); + await context.Response.WriteAsJsonAsync(result); + return; + } + + await next(); + }); + + proxyBuilder.UseLoadBalancing(); + }); + } + + private static AppConfigurationRequest CreateAppConfigurationRequestInput(HttpContext context, + string appConfigurationPath) + { + var proxyConfig = context.RequestServices.GetRequiredService(); + + var input = new AppConfigurationRequest(); + string path = $"{appConfigurationPath}?includeLocalizationResources=false"; + + var clusterList = GetClusters(proxyConfig); + foreach (var cluster in clusterList) + { + var hostUrl = new Uri(cluster.Value.Address) + $"{path}"; + // CacheKey/Endpoint dictionary key -> ex: ("Administration_AppConfig") + input.Endpoints.Add($"{cluster.Key}_AppConfig", hostUrl); + } + + return input; + } + + private static LocalizationRequest CreateLocalizationRequestInput(HttpContext context, string localizationPath) + { + var proxyConfig = context.RequestServices.GetRequiredService(); + + context.Request.Query.TryGetValue("CultureName", out var cultureName); + + var input = new LocalizationRequest(cultureName); + string path = $"{localizationPath}?cultureName={cultureName}&onlyDynamics=false"; + + var clusterList = GetClusters(proxyConfig); + foreach (var cluster in clusterList) + { + var hostUrl = new Uri(cluster.Value.Address) + $"{path}"; + // Endpoint dictionary key -> ex: ("Administration_en") + input.Endpoints.Add($"{cluster.Key}_{cultureName}", hostUrl); + } + + return input; + } + + private static Dictionary GetClusters(IProxyConfigProvider proxyConfig) + { + var yarpConfig = proxyConfig.GetConfig(); + + var routedClusters = yarpConfig.Clusters + .SelectMany(t => t.Destinations, + (clusterId, destination) => new { clusterId.ClusterId, destination.Value }); + + return routedClusters + .GroupBy(q => q.Value.Address) + .Select(t => t.First()) + .Distinct() + .ToDictionary(k => k.ClusterId, v => v.Value); + } +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/yarp.json b/gateways/web/src/EShopOnAbp.WebGateway/yarp.json index f77a2db6..9a0b39a4 100644 --- a/gateways/web/src/EShopOnAbp.WebGateway/yarp.json +++ b/gateways/web/src/EShopOnAbp.WebGateway/yarp.json @@ -7,6 +7,18 @@ "Path": "/api/abp/{**catch-all}" } }, + "EshopOnAbpLocalization": { + "ClusterId": "Administration", + "Match": { + "Path": "/api/abp/application-localization" + } + }, + "EshopOnAbpApplicationConfiguration": { + "ClusterId": "Administration", + "Match": { + "Path": "/api/abp/application-configuration" + } + }, "Identity Service": { "ClusterId": "Identity", "Match": { diff --git a/tye.yaml b/tye.yaml index de0f4014..ff389ce8 100644 --- a/tye.yaml +++ b/tye.yaml @@ -1,14 +1,5 @@ name: EShopOnAbp services: -# - name: auth-server -# project: apps/auth-server/src/EShopOnAbp.AuthServer/EShopOnAbp.AuthServer.csproj -# bindings: -# - protocol: https -# port: 44330 -# env: -# - Kestrel__Certificates__Default__Path=../../../../etc/dev-cert/localhost.pfx -# - Kestrel__Certificates__Default__Password=8b6039b6-c67a-448b-977b-0ce6d3fcfd49 - - name: identity-service project: services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/EShopOnAbp.IdentityService.HttpApi.Host.csproj bindings: