diff --git a/FreeRedis.sln b/FreeRedis.sln index 1db522a..1dfdac8 100644 --- a/FreeRedis.sln +++ b/FreeRedis.sln @@ -47,6 +47,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeRedis.DistributedCache" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "console_net8_cluster_client_side_caching", "examples\console_net8_cluster_client_side_caching\console_net8_cluster_client_side_caching.csproj", "{1C4387AA-C4BD-4388-97D1-3A769439705D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuget", "nuget", "{1C5DEACA-4A4F-4C8F-999F-466B3B95CD18}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeRedis.Abp.Localization", "nuget\FreeRedis.Abp.Localization\FreeRedis.Abp.Localization.csproj", "{F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "abpapi_net8", "examples\abpapi_net8\abpapi_net8.csproj", "{E6737319-1109-4645-94CD-43ED445CD7C8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "abpapi_net8_server", "examples\abpapi_net8_server\abpapi_net8_server.csproj", "{6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "abpapi_net8_benchmark", "examples\abpapi_net8_benchmark\abpapi_net8_benchmark.csproj", "{C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -225,6 +235,54 @@ Global {1C4387AA-C4BD-4388-97D1-3A769439705D}.Release|x64.Build.0 = Release|Any CPU {1C4387AA-C4BD-4388-97D1-3A769439705D}.Release|x86.ActiveCfg = Release|Any CPU {1C4387AA-C4BD-4388-97D1-3A769439705D}.Release|x86.Build.0 = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|x64.Build.0 = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|x86.Build.0 = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|Any CPU.Build.0 = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|x64.ActiveCfg = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|x64.Build.0 = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|x86.ActiveCfg = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|x86.Build.0 = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|x64.Build.0 = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|x86.Build.0 = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|Any CPU.Build.0 = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|x64.ActiveCfg = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|x64.Build.0 = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|x86.ActiveCfg = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|x86.Build.0 = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|x64.ActiveCfg = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|x64.Build.0 = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|x86.ActiveCfg = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|x86.Build.0 = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|Any CPU.Build.0 = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|x64.ActiveCfg = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|x64.Build.0 = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|x86.ActiveCfg = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|x86.Build.0 = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|x64.Build.0 = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|x86.ActiveCfg = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|x86.Build.0 = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|Any CPU.Build.0 = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|x64.ActiveCfg = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|x64.Build.0 = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|x86.ActiveCfg = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -246,6 +304,10 @@ Global {7AC68251-83D4-423F-849C-04BD89F4BDFB} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} {B0B0B55E-4D41-4419-83FE-309F72699A3A} = {D8045351-59E4-45B9-81F5-D21CC0996344} {1C4387AA-C4BD-4388-97D1-3A769439705D} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9} = {1C5DEACA-4A4F-4C8F-999F-466B3B95CD18} + {E6737319-1109-4645-94CD-43ED445CD7C8} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B36AD060-F6AE-49C7-A310-B58B8467CE69} diff --git a/examples/abpapi_net8/ApiModule.cs b/examples/abpapi_net8/ApiModule.cs new file mode 100644 index 0000000..ad75e61 --- /dev/null +++ b/examples/abpapi_net8/ApiModule.cs @@ -0,0 +1,42 @@ +using FreeRedis; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace abpapi_net8 +{ + [DependsOn(typeof(AbpLocalizationModule))] + public class ApiModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + // 将 RedisClient 注册为单例 + RedisClient redisClient = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + LanguageRedisOptions redisOptions = new LanguageRedisOptions + { + KeyPrefix = "language", + Capacity = 20, + CheckExpired = TimeSpan.FromHours(1), + CheckNewLanguageExpired = TimeSpan.FromMinutes(1), + }; + context.Services.AddSingleton(redisClient); + context.Services.AddSingleton(redisOptions); + + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Add("en") + // 注入 Redis 多语言 + .AddFreeRedis(redisClient, redisOptions) + .AddVirtualJson("/Localization/Resources/Test"); + }); + } + } +} diff --git a/examples/abpapi_net8/Program.cs b/examples/abpapi_net8/Program.cs new file mode 100644 index 0000000..012ef3c --- /dev/null +++ b/examples/abpapi_net8/Program.cs @@ -0,0 +1,49 @@ +using FreeRedis; +using Microsoft.Extensions.Localization; +using Volo.Abp; +using Volo.Abp.Localization; + +namespace abpapi_net8 +{ + internal class Program + { + static async Task Main(string[] args) + { + RedisClient redisClient = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + + await redisClient.HMSetAsync("language:en", new Dictionary + { + { "Hello", "Hello" }, + { "World", "World"}, + }); + await redisClient.HMSetAsync("language:zh-CN", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + await redisClient.HMSetAsync("language:zh", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + + var provider = AbpApplicationFactory.Create(); + provider.Initialize(); + var localizer = provider.ServiceProvider.GetRequiredService>(); + using (CultureHelper.Use("en")) + { + if (localizer["Hello"].Value != "Hello") + { + throw new Exception(); + } + } + using (CultureHelper.Use("zh")) + { + if (localizer["Hello"].Value != "你好") + { + throw new Exception(); + } + } + } + } +} diff --git a/examples/abpapi_net8/Properties/launchSettings.json b/examples/abpapi_net8/Properties/launchSettings.json new file mode 100644 index 0000000..8113026 --- /dev/null +++ b/examples/abpapi_net8/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "abpapi_net8": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:51294;http://localhost:51295" + } + } +} \ No newline at end of file diff --git a/examples/abpapi_net8/TestResource.cs b/examples/abpapi_net8/TestResource.cs new file mode 100644 index 0000000..1d28532 --- /dev/null +++ b/examples/abpapi_net8/TestResource.cs @@ -0,0 +1,6 @@ +namespace abpapi_net8 +{ + public class TestResource + { + } +} diff --git a/examples/abpapi_net8/abpapi_net8.csproj b/examples/abpapi_net8/abpapi_net8.csproj new file mode 100644 index 0000000..e09fee0 --- /dev/null +++ b/examples/abpapi_net8/abpapi_net8.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/examples/abpapi_net8_benchmark/Ben.cs b/examples/abpapi_net8_benchmark/Ben.cs new file mode 100644 index 0000000..146a517 --- /dev/null +++ b/examples/abpapi_net8_benchmark/Ben.cs @@ -0,0 +1,48 @@ +using abpapi_net8; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Volo.Abp; +using Volo.Abp.Localization; + +namespace abpapi_net8_benchmark +{ + [SimpleJob(RuntimeMoniker.Net70)] + [SimpleJob(RuntimeMoniker.Net80)] + //[SimpleJob(RuntimeMoniker.NativeAot70)] + // GC 分析 + [MemoryDiagnoser] + // 线程体积 + [ThreadingDiagnoser] + public partial class Ben + { + private IStringLocalizer _stringLocalizer; + + [GlobalSetup] + public void Setup() + { + var application = AbpApplicationFactory.Create(); + + application.Initialize(); + + _stringLocalizer = application.ServiceProvider.GetRequiredService>(); + + var language = "zh"; + using (CultureHelper.Use(language)) + { + var value = _stringLocalizer["Hello"]; + } + } + + + [Benchmark] + public void GetValue() + { + using (CultureHelper.Use("zh")) + { + var value = _stringLocalizer["Hello"]; + } + } + } +} diff --git a/examples/abpapi_net8_benchmark/Program.cs b/examples/abpapi_net8_benchmark/Program.cs new file mode 100644 index 0000000..5e44243 --- /dev/null +++ b/examples/abpapi_net8_benchmark/Program.cs @@ -0,0 +1,14 @@ +using BenchmarkDotNet.Running; + +namespace abpapi_net8_benchmark +{ + + internal class Program + { + static void Main(string[] args) + { + var summary = BenchmarkRunner.Run(); + Console.ReadLine(); + } + } +} diff --git a/examples/abpapi_net8_benchmark/Properties/PublishProfiles/FolderProfile.pubxml b/examples/abpapi_net8_benchmark/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..df5633a --- /dev/null +++ b/examples/abpapi_net8_benchmark/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,13 @@ + + + + + Release + Any CPU + bin\Release\net8.0\publish\ + FileSystem + <_TargetId>Folder + + \ No newline at end of file diff --git a/examples/abpapi_net8_benchmark/abpapi_net8_benchmark.csproj b/examples/abpapi_net8_benchmark/abpapi_net8_benchmark.csproj new file mode 100644 index 0000000..cb2e429 --- /dev/null +++ b/examples/abpapi_net8_benchmark/abpapi_net8_benchmark.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/examples/abpapi_net8_server/AppModule.cs b/examples/abpapi_net8_server/AppModule.cs new file mode 100644 index 0000000..9473972 --- /dev/null +++ b/examples/abpapi_net8_server/AppModule.cs @@ -0,0 +1,80 @@ + +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Modularity; +using Volo.Abp; +using FreeRedis; +using Volo.Abp.Localization; +using Volo.Abp.VirtualFileSystem; + +namespace abpapi_net8_server +{ + [DependsOn(typeof(AbpAspNetCoreMvcModule), typeof(AbpLocalizationModule))] + public class AppModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + // 将 RedisClient 注册为单例 + RedisClient redisClient = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + + redisClient.HMSet("language:en", new Dictionary + { + { "Hello", "Hello" }, + { "World", "World"}, + }); + redisClient.HMSet("language:zh-CN", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + redisClient.HMSet("language:zh", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + + LanguageRedisOptions redisOptions = new LanguageRedisOptions + { + KeyPrefix = "language", + Capacity = 20, + CheckExpired = TimeSpan.FromHours(1), + CheckNewLanguageExpired = TimeSpan.FromMinutes(1), + }; + context.Services.AddSingleton(redisClient); + context.Services.AddSingleton(redisOptions); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Add("en") + // 注入 Redis 多语言 + .AddFreeRedis(redisClient, redisOptions); + }); + + context.Services.AddSwaggerGen(); + } + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); + + // Configure the HTTP request pipeline. + if (env.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseAuthorization(); + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseConfiguredEndpoints(); + } + } +} diff --git a/examples/abpapi_net8_server/Controllers/IndexController.cs b/examples/abpapi_net8_server/Controllers/IndexController.cs new file mode 100644 index 0000000..9bd0e49 --- /dev/null +++ b/examples/abpapi_net8_server/Controllers/IndexController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; + +namespace abpapi_net8_server.Controllers +{ + [ApiController] + [Route("[controller]")] + public class IndexController : ControllerBase + { + private readonly IStringLocalizer _localizer; + + public IndexController(IStringLocalizer localizer) + { + _localizer = localizer; + } + + /// + /// ȡֵַ + /// + /// + /// + [HttpGet("get")] + public string GetAsync(string name) + { + return _localizer[name]; + } + } +} diff --git a/examples/abpapi_net8_server/Program.cs b/examples/abpapi_net8_server/Program.cs new file mode 100644 index 0000000..1374135 --- /dev/null +++ b/examples/abpapi_net8_server/Program.cs @@ -0,0 +1,17 @@ +namespace abpapi_net8_server +{ + public class Program + { + public static async Task Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + await builder.AddApplicationAsync(); + + var app = builder.Build(); + + await app.InitializeApplicationAsync(); + await app.RunAsync(); + } + } +} diff --git a/examples/abpapi_net8_server/Properties/launchSettings.json b/examples/abpapi_net8_server/Properties/launchSettings.json new file mode 100644 index 0000000..f2ee310 --- /dev/null +++ b/examples/abpapi_net8_server/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3995", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5078", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/abpapi_net8_server/TestResource.cs b/examples/abpapi_net8_server/TestResource.cs new file mode 100644 index 0000000..3325ac5 --- /dev/null +++ b/examples/abpapi_net8_server/TestResource.cs @@ -0,0 +1,6 @@ +namespace abpapi_net8_server +{ + public class TestResource + { + } +} diff --git a/examples/abpapi_net8_server/abpapi_net8_server.csproj b/examples/abpapi_net8_server/abpapi_net8_server.csproj new file mode 100644 index 0000000..a99e7a7 --- /dev/null +++ b/examples/abpapi_net8_server/abpapi_net8_server.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + diff --git a/examples/abpapi_net8_server/abpapi_net8_server.http b/examples/abpapi_net8_server/abpapi_net8_server.http new file mode 100644 index 0000000..59f2a8e --- /dev/null +++ b/examples/abpapi_net8_server/abpapi_net8_server.http @@ -0,0 +1,6 @@ +@abpapi_net8_server_HostAddress = http://localhost:5078 + +GET {{abpapi_net8_server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/examples/abpapi_net8_server/appsettings.Development.json b/examples/abpapi_net8_server/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/examples/abpapi_net8_server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/abpapi_net8_server/appsettings.json b/examples/abpapi_net8_server/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/examples/abpapi_net8_server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/nuget/FreeRedis.Abp.Localization/Extensions.cs b/nuget/FreeRedis.Abp.Localization/Extensions.cs new file mode 100644 index 0000000..261d35a --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/Extensions.cs @@ -0,0 +1,46 @@ +using FreeRedis; +using FreeRedis.Abp.Localization; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.Localization +{ + /// + /// 扩展 + /// + public static class Extensions + { + // 防止每个程序集都缓存 + private static readonly Dictionary Factories = new Dictionary(); + + /// + /// 从 Redis 中添加多语言资源 + /// + /// + /// + /// RedisClient + /// 配置 + /// + public static TLocalizationResource AddFreeRedis( + [NotNull] this TLocalizationResource localizationResource, + [NotNull] RedisClient redisClient, + [NotNull] LanguageRedisOptions options) + where TLocalizationResource : LocalizationResourceBase + { + FreeRedisLocalizationResourceFactory factory; + + if(!Factories.TryGetValue(redisClient,out factory)) + { + factory = new FreeRedisLocalizationResourceFactory(redisClient, options); + Factories.Add(redisClient, factory); + } + + FreeRedisLocalizationResource resource = new FreeRedisLocalizationResource(factory); + localizationResource.Contributors.Add(resource); + return localizationResource; + } + } +} diff --git a/nuget/FreeRedis.Abp.Localization/FreeRedis.Abp.Localization.csproj b/nuget/FreeRedis.Abp.Localization/FreeRedis.Abp.Localization.csproj new file mode 100644 index 0000000..231f006 --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/FreeRedis.Abp.Localization.csproj @@ -0,0 +1,17 @@ + + + + Library + netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0; + true + + + + + + + + + + + diff --git a/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceBase.cs b/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceBase.cs new file mode 100644 index 0000000..19163ac --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceBase.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.Localization; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Localization; + +namespace FreeRedis.Abp.Localization +{ + + /// + public class FreeRedisLocalizationResource : ILocalizationResourceContributor + { + private readonly FreeRedisLocalizationResourceFactory _factory; + + /// + public FreeRedisLocalizationResource(FreeRedisLocalizationResourceFactory factory) + { + _factory = factory; + } + + /// + public bool IsDynamic => true; + + /// + public void Fill(string cultureName, Dictionary dictionary) => _factory.Fill(cultureName, dictionary); + + /// + public Task FillAsync(string cultureName, Dictionary dictionary) + => _factory.FillAsync(cultureName, dictionary); + + /// + public LocalizedString GetOrNull(string cultureName, string name) => _factory.GetOrNull(cultureName, name); + + /// + public Task> GetSupportedCulturesAsync() + => _factory.GetSupportedCulturesAsync(); + + /// + public void Initialize(LocalizationResourceInitializationContext context) + => _factory.Initialize(context); + } +} diff --git a/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceFactory.cs b/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceFactory.cs new file mode 100644 index 0000000..0ac5835 --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceFactory.cs @@ -0,0 +1,88 @@ +using Microsoft.Extensions.Localization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Localization; + +namespace FreeRedis.Abp.Localization +{ + /// + public class FreeRedisLocalizationResourceFactory : ILocalizationResourceContributor + { + private readonly RedisClient _redisClient; + private readonly LanguageRedisOptions _options; + + private volatile bool _isInit = false; + + /// + public FreeRedisLocalizationResourceFactory(RedisClient redisClient, LanguageRedisOptions options) + { + _redisClient = redisClient; + _options = options; + + redisClient.UseClientSideCaching(new ClientSideCachingOptions + { + Capacity = _options.Capacity, + KeyFilter = key => key.StartsWith(_options.KeyPrefix), + CheckExpired = (key, dt) => DateTime.Now.Subtract(dt) > options.CheckExpired + }); + } + + /// + public bool IsDynamic => true; + + // 动态语言集,此方法不会被调用 + /// + public void Fill(string cultureName, Dictionary dictionary) + { + } + + // 动态语言集,此方法不会被调用 + /// + public Task FillAsync(string cultureName, Dictionary dictionary) + { + return Task.CompletedTask; + } + + /// + public LocalizedString GetOrNull(string cultureName, string name) + { + if (string.IsNullOrEmpty(cultureName)) cultureName = _options.DefaultLanguage; + // ex: language:zh-CN + var key = _options.KeyPrefix + ":" + cultureName; + var value = _redisClient.HGet(key, name); + if (string.IsNullOrEmpty(value)) + { + return new LocalizedString(name, name, resourceNotFound: true); + } + return new LocalizedString(name, value); + } + + /// + public async Task> GetSupportedCulturesAsync() + { + List langs = new List(); + var keys = await _redisClient.KeysAsync(_options.KeyPrefix + "*"); + foreach (var item in keys) + { + langs.Add(item.Split(":").LastOrDefault()); + } + return langs; + } + + /// + public void Initialize(LocalizationResourceInitializationContext context) + { + if (_isInit) return; + _isInit = true; + var keys = _redisClient.Keys(_options.KeyPrefix + "*"); + + // 第一次先将缓存拉取到本地 + foreach (var key in keys) + { + _redisClient.HGetAll(key); + } + } + } +} diff --git a/nuget/FreeRedis.Abp.Localization/LanguageOptions.cs b/nuget/FreeRedis.Abp.Localization/LanguageOptions.cs new file mode 100644 index 0000000..04e04e0 --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/LanguageOptions.cs @@ -0,0 +1,42 @@ +using JetBrains.Annotations; +using System; +using System.ComponentModel.DataAnnotations; + +namespace Volo.Abp.Localization +{ + /// + /// Redis 多语言配置 + /// + public class LanguageRedisOptions + { + /// + /// 默认语言 + /// + [NotNull] + [Required] + public string DefaultLanguage { get; set; } = "zh"; + + /// + /// Key 前缀,如 language。 + /// 框架将会拼接成 language:zh-cn 类似的 key。 + /// + [NotNull] + [Required] + public string KeyPrefix { get; set; } + + /// + /// 缓存的 Key 数量 + /// + public int Capacity { get; set; } + + /// + /// 每个 Key 一直未被使用,则此时间后过期 + /// + public TimeSpan CheckExpired { get; set; } = TimeSpan.FromHours(1); + + /// + /// 如果在本地没有搜索到 + /// + public TimeSpan CheckNewLanguageExpired { get; set; } = TimeSpan.FromMinutes(1); + } +} diff --git a/nuget/FreeRedis.Abp.Localization/README.md b/nuget/FreeRedis.Abp.Localization/README.md new file mode 100644 index 0000000..963ed1d --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/README.md @@ -0,0 +1,78 @@ +往 Redis 填充多语言数据,使用 `{前缀}:{语言名称}` 的形式存储 Key,每个 Key 均是 Hash 类型。 + +![image-20231209085550710](images/image-20231209085550710.png) + +使用编程填充数据如下所示: +```csharp + RedisClient redisClient = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + + await redisClient.HMSetAsync("language:en", new Dictionary + { + { "Hello", "Hello" }, + { "World", "World"}, + }); + await redisClient.HMSetAsync("language:zh-CN", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + await redisClient.HMSetAsync("language:zh", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); +``` + + + +![image-20231209085649631](images/image-20231209085649631.png) + +![image-20231209085657772](images/image-20231209085657772.png) + + + + + +在 ABP 中初始化 RedisClient 和多语言配置: + +```csharp + // 将 RedisClient 注册为单例 + RedisClient cli = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + context.Services.AddSingleton(cli); + context.Services.AddSingleton(s => + { + return new LanguageRedisOptions + { + // Key 前缀 + KeyPrefix = "language", + // 缓存的 Key 数量 + Capacity = 20, + // 多长时间不使用缓存将被从本地中清除 + CheckExpired = TimeSpan.FromHours(1) + }; + }); +``` + + + +然后在按照 ABP 配置多语言时,使用 Redis 即可。 + +```csharp + // 从容器中取出单例,避免每个模块都 new 一个新的 RedisClient + var redisClient = context.Services.GetRequiredService(); + var languageRedisOptions = context.Services.GetRequiredService(); + + Configure(options => + { + options.Resources + .Add("en") + // 注入 Redis 多语言 + .AddFreeRedis(redisClient, languageRedisOptions) + .AddVirtualJson("/Localization/Resources/Test"); + }); +``` + + + +因为 ABP 多语言需要每个程序集的模块都初始化一次,因此为了避免每个程序集都做本地缓存,我们需要确保配置多语言资源提供器时,使用同一个 RedisClient 对象。 + diff --git a/nuget/FreeRedis.Abp.Localization/images/image-20231209085550710.png b/nuget/FreeRedis.Abp.Localization/images/image-20231209085550710.png new file mode 100644 index 0000000..33ae3f5 Binary files /dev/null and b/nuget/FreeRedis.Abp.Localization/images/image-20231209085550710.png differ diff --git a/nuget/FreeRedis.Abp.Localization/images/image-20231209085649631.png b/nuget/FreeRedis.Abp.Localization/images/image-20231209085649631.png new file mode 100644 index 0000000..ccc53c8 Binary files /dev/null and b/nuget/FreeRedis.Abp.Localization/images/image-20231209085649631.png differ diff --git a/nuget/FreeRedis.Abp.Localization/images/image-20231209085657772.png b/nuget/FreeRedis.Abp.Localization/images/image-20231209085657772.png new file mode 100644 index 0000000..8b7e4c2 Binary files /dev/null and b/nuget/FreeRedis.Abp.Localization/images/image-20231209085657772.png differ