diff --git a/framework/Volo.Abp.slnx b/framework/Volo.Abp.slnx index 3d74af9f95..1302600c09 100644 --- a/framework/Volo.Abp.slnx +++ b/framework/Volo.Abp.slnx @@ -181,6 +181,7 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs index e3d3c12370..27856fe7e4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using Microsoft.AspNetCore.Http; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; @@ -84,20 +85,43 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu private async Task GetRemoteConfigurationAsync() { - var config = await ApplicationConfigurationAppService.GetAsync( + var cultureName = CultureInfo.CurrentUICulture.Name; + + var configTask = ApplicationConfigurationAppService.GetAsync( new ApplicationConfigurationRequestOptions { IncludeLocalizationResources = false } ); - var localizationDto = await ApplicationLocalizationClientProxy.GetAsync( - new ApplicationLocalizationRequestDto { - CultureName = config.Localization.CurrentCulture.Name, + var localizationTask = ApplicationLocalizationClientProxy.GetAsync( + new ApplicationLocalizationRequestDto + { + CultureName = cultureName, OnlyDynamics = true } ); + var config = await configTask; + + ApplicationLocalizationDto localizationDto; + // In most cases, the culture matches and we can reuse the concurrent request. + // If not, we discard it and make a new request with the correct culture. + if (string.Equals(config.Localization.CurrentCulture.Name, cultureName, StringComparison.OrdinalIgnoreCase)) + { + localizationDto = await localizationTask; + } + else + { + localizationDto = await ApplicationLocalizationClientProxy.GetAsync( + new ApplicationLocalizationRequestDto + { + CultureName = config.Localization.CurrentCulture.Name, + OnlyDynamics = true + } + ); + } + config.Localization.Resources = localizationDto.Resources; return config; diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo.Abp.AspNetCore.Mvc.Client.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo.Abp.AspNetCore.Mvc.Client.Tests.csproj new file mode 100644 index 0000000000..d1190652a3 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo.Abp.AspNetCore.Mvc.Client.Tests.csproj @@ -0,0 +1,17 @@ + + + + + + net10.0 + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientTestBase.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientTestBase.cs new file mode 100644 index 0000000000..96e82fe105 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientTestBase.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Testing; + +namespace Volo.Abp.AspNetCore.Mvc.Client; + +public abstract class AbpAspNetCoreMvcClientTestBase : AbpIntegratedTest +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientTestModule.cs new file mode 100644 index 0000000000..04e9f856a2 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientTestModule.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Mvc.Client; + +[DependsOn( + typeof(AbpAspNetCoreMvcClientModule), + typeof(AbpAutofacModule) +)] +public class AbpAspNetCoreMvcClientTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddHttpContextAccessor(); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient_Tests.cs new file mode 100644 index 0000000000..ed20593873 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Client.Tests/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient_Tests.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using NSubstitute; +using Shouldly; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ClientProxies; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Client; + +public class MvcCachedApplicationConfigurationClient_Tests : AbpAspNetCoreMvcClientTestBase +{ + private AbpApplicationConfigurationClientProxy _configProxy; + private AbpApplicationLocalizationClientProxy _localizationProxy; + + private readonly ICachedApplicationConfigurationClient _applicationConfigurationClient; + + public MvcCachedApplicationConfigurationClient_Tests() + { + _applicationConfigurationClient = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + _configProxy = Substitute.For(); + _localizationProxy = Substitute.For(); + + services.Replace(ServiceDescriptor.Transient(_ => _configProxy)); + services.Replace(ServiceDescriptor.Transient(_ => _localizationProxy)); + } + + [Fact] + public async Task Should_Use_Concurrent_Requests_When_Culture_Matches() + { + var cultureName = "en"; + + _configProxy.GetAsync(Arg.Any()).Returns(CreateConfigDto(cultureName)); + + var expectedResources = new Dictionary + { + ["TestResource"] = new() + }; + + _localizationProxy.GetAsync(Arg.Any()).Returns(new ApplicationLocalizationDto { Resources = expectedResources }); + + var result = await _applicationConfigurationClient.GetAsync(); + + result.Localization.Resources.ShouldBe(expectedResources); + + await _configProxy.Received(1).GetAsync(Arg.Is(x => x.IncludeLocalizationResources == false)); + await _localizationProxy.Received(1).GetAsync(Arg.Is(x => x.CultureName == cultureName && x.OnlyDynamics == true)); + } + + [Fact] + public async Task Should_Refetch_Localization_When_Culture_Differs() + { + var currentCulture = "en"; + var serverCulture = "tr"; + + _configProxy.GetAsync(Arg.Any()).Returns(CreateConfigDto(serverCulture)); + + var wrongResources = new Dictionary(); + var correctResources = new Dictionary + { + ["TestResource"] = new() + }; + + _localizationProxy.GetAsync(Arg.Is(x => x.CultureName == currentCulture)).Returns(new ApplicationLocalizationDto { Resources = wrongResources }); + _localizationProxy.GetAsync(Arg.Is(x => x.CultureName == serverCulture)).Returns(new ApplicationLocalizationDto { Resources = correctResources }); + + var result = await _applicationConfigurationClient.GetAsync(); + + result.Localization.Resources.ShouldBe(correctResources); + + await _localizationProxy.Received(1).GetAsync(Arg.Is(x => x.CultureName == serverCulture)); + } + + private static ApplicationConfigurationDto CreateConfigDto(string cultureName) + { + return new ApplicationConfigurationDto + { + Localization = + { + CurrentCulture = new CurrentCultureDto { Name = cultureName } + } + }; + } +}