From 202bbcc18cf0d03ee984901e8f7b766cdd724c31 Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Mon, 29 Jan 2024 22:48:15 -0500 Subject: [PATCH] Added localization aggregation --- .../Aggregations/ILocalizationAggregation.cs | 11 ++ .../Aggregations/LocalizationAggregation.cs | 152 ++++++++++++++++++ .../EShopOnAbpWebGatewayModule.cs | 8 +- .../ReverseProxyBuilderExtensions.cs | 88 ++++++++++ .../web/src/EShopOnAbp.WebGateway/yarp.json | 6 + 5 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ILocalizationAggregation.cs create mode 100644 gateways/web/src/EShopOnAbp.WebGateway/Aggregations/LocalizationAggregation.cs create mode 100644 gateways/web/src/EShopOnAbp.WebGateway/ReverseProxyBuilderExtensions.cs diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ILocalizationAggregation.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ILocalizationAggregation.cs new file mode 100644 index 00000000..e2295f63 --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/ILocalizationAggregation.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.WebGateway.Aggregations; + +public interface ILocalizationAggregation : ITransientDependency +{ + public string LocalizationEndpoint { get; } + public Task GetLocalizationAsync(LocalizationRequest input); +} \ No newline at end of file diff --git a/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/LocalizationAggregation.cs b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/LocalizationAggregation.cs new file mode 100644 index 00000000..0530a3d3 --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/Aggregations/LocalizationAggregation.cs @@ -0,0 +1,152 @@ +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.Caching; + +namespace EShopOnAbp.WebGateway.Aggregations; + +public class LocalizationAggregation : ILocalizationAggregation +{ + //Yarp localization route name + public string LocalizationEndpoint { get; } = "EshopOnAbpLocalization"; + private readonly ILogger _logger; + private readonly IDistributedCache _localizationResponseCache; + + public LocalizationAggregation(ILogger logger, IDistributedCache localizationResponseCache) + { + _logger = logger; + _localizationResponseCache = localizationResponseCache; + } + + public async Task GetLocalizationAsync(LocalizationRequest input) + { + //Temp list + Dictionary services = new Dictionary() + { + { "Administration", "https://localhost:44353/api/abp/application-localization?CultureName=en" }, + { "Catalog", "https://localhost:44354/api/abp/application-localization?CultureName=en" } + }; + + // Check the cache + + + + Dictionary localizationResults = await GetCompletedLocalizationResultsAsync(services); + ApplicationLocalizationDto mergedResult = MergeLocalizations(localizationResults); + + return mergedResult; + } + + private ApplicationLocalizationDto MergeLocalizations(Dictionary localizationResults) + { + var localizationDto = new ApplicationLocalizationDto(); + foreach (var localizationResult in localizationResults) + { + localizationDto.Resources.AddIfNotContains(localizationResult.Value.Resources); + } + + return localizationDto; + } + + private async Task> GetCompletedLocalizationResultsAsync(Dictionary services) + { + Dictionary> completedTasks = new Dictionary>(); + Dictionary> runningTasks = new Dictionary>(); + Dictionary completedResult = new Dictionary(); + + using (HttpClient httpClient = new HttpClient()) + { + foreach (var service in services) + { + 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 + { + ApplicationLocalizationDto result = await completedTask.Value; + + completedTasks.Add(completedTask.Key, completedTask.Value); + completedResult.Add(completedTask.Key, result); + + _logger.LogInformation($"Key: {completedTask.Key}, Value: {result}"); + } + catch (Exception ex) + { + _logger.LogInformation($"Error for the {completedTask.Key}: {ex.Message}"); + } + } + } + + return completedResult; + } + + private async Task>> WaitForAnyTaskAsync( + Dictionary> tasks) + { + Task> completedTask = Task.WhenAny(tasks.Values); + Task result = await completedTask; + + // Find the key corresponding to the completed task + KeyValuePair> + completedTaskPair = tasks.First(kv => kv.Value == result); + + return completedTaskPair; + } + + private async Task MakeRequestAsync(HttpClient httpClient, string url) + { + try + { + HttpResponseMessage response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + string content = await response.Content.ReadAsStringAsync(); + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + return JsonSerializer.Deserialize(content, options); + } + catch (Exception ex) + { + // Log or handle the exception, and return a default value or throw it again if needed + Console.WriteLine($"Error making request to {url}: {ex.Message}"); + throw; + } + } +} + +public class LocalizationRequest +{ + // Localization clusterName - localization endpoint pair + public Dictionary LocalizationEndpoints { get; } + public string CultureName { get; set; } + public bool? OnlyDynamics { get; set; } + + public LocalizationRequest(string cultureName) + { + CultureName = cultureName; + LocalizationEndpoints = new Dictionary(); + } +} + +public class LocalizationResponse +{ + // Localization url - localization resource pair + public Dictionary Localization { get; } + public string CultureName { get; set; } + public bool? OnlyDynamics { get; set; } + public string ServiceName { get; set; } +} \ 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..96c9a855 100644 --- a/gateways/web/src/EShopOnAbp.WebGateway/EShopOnAbpWebGatewayModule.cs +++ b/gateways/web/src/EShopOnAbp.WebGateway/EShopOnAbpWebGatewayModule.cs @@ -7,7 +7,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Linq; +using EShopOnAbp.WebGateway.Aggregations; +using Microsoft.AspNetCore.Http; using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; using Volo.Abp.Modularity; namespace EShopOnAbp.WebGateway; @@ -76,6 +79,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..d53f4a9f --- /dev/null +++ b/gateways/web/src/EShopOnAbp.WebGateway/ReverseProxyBuilderExtensions.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using EShopOnAbp.WebGateway.Aggregations; +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 string LocalizationEndpoint = "api/abp/application-localization"; + + 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(); + if (localizationAggregation.LocalizationEndpoint == endpoint?.DisplayName) + { + LocalizationRequest input = CreateLocalizationRequestInput(context); + + var result = await localizationAggregation.GetLocalizationAsync(input); + await context.Response.WriteAsync(JsonSerializer.Serialize(result, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + })); + return; + } + + await next(); + }); + + proxyBuilder.UseLoadBalancing(); + }); + } + + private static LocalizationRequest CreateLocalizationRequestInput(HttpContext context) + { + var proxyConfig = context.RequestServices.GetRequiredService(); + + context.Request.Query.TryGetValue("CultureName", out var cultureName); + context.Request.Query.TryGetValue("OnlyDynamics", out var onlyDynamics); + + var input = new LocalizationRequest(cultureName); + StringBuilder stringBuilder = new StringBuilder(LocalizationEndpoint + $"?CultureName={cultureName}"); + + if (!string.IsNullOrEmpty(onlyDynamics)) + { + input.OnlyDynamics = bool.Parse(onlyDynamics); + stringBuilder.Append($"&OnlyDynamics={onlyDynamics}"); + } + + var clusterList = GetClusters(proxyConfig); + foreach (var cluster in clusterList) + { + var hostUrl = new Uri(cluster.Value.Address) + $"{stringBuilder}"; + input.LocalizationEndpoints.Add(cluster.Key, 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..28a452eb 100644 --- a/gateways/web/src/EShopOnAbp.WebGateway/yarp.json +++ b/gateways/web/src/EShopOnAbp.WebGateway/yarp.json @@ -7,6 +7,12 @@ "Path": "/api/abp/{**catch-all}" } }, + "EshopOnAbpLocalization": { + "ClusterId": "Administration", + "Match": { + "Path": "/api/abp/application-localization" + } + }, "Identity Service": { "ClusterId": "Identity", "Match": {