From 54e9c7962d96687e439cc37d57aefb030d446fff Mon Sep 17 00:00:00 2001 From: Seany <17074267@qq.com> Date: Sun, 14 Dec 2025 02:43:40 +0800 Subject: [PATCH] add jobs --- .../Services/JsonStringLocalizer.cs | 82 ++++++++++ .../Services/LocalizationProvider.cs | 77 +-------- .../Services/WasmLocalizationProvider.cs | 65 +++++++- Atomx.Admin/Atomx.Admin/Atomx.Admin.csproj | 1 + .../Controllers/LocaleResourceController.cs | 34 ++-- .../Middlewares/RequestCultureMiddleware.cs | 6 +- Atomx.Admin/Atomx.Admin/Program.cs | 4 + .../Services/LocalizationFileService.cs | 147 ------------------ Atomx.Core/Jos/LocalizationJob.cs | 71 +++++---- ...JobsService.cs => BackgroundJobService.cs} | 11 +- 10 files changed, 220 insertions(+), 278 deletions(-) create mode 100644 Atomx.Admin/Atomx.Admin.Client/Services/JsonStringLocalizer.cs delete mode 100644 Atomx.Admin/Atomx.Admin/Services/LocalizationFileService.cs rename Atomx.Core/Services/{BackgroundJobsService.cs => BackgroundJobService.cs} (78%) diff --git a/Atomx.Admin/Atomx.Admin.Client/Services/JsonStringLocalizer.cs b/Atomx.Admin/Atomx.Admin.Client/Services/JsonStringLocalizer.cs new file mode 100644 index 0000000..ae91bdf --- /dev/null +++ b/Atomx.Admin/Atomx.Admin.Client/Services/JsonStringLocalizer.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.Localization; +using System.Globalization; + +namespace Atomx.Admin.Client.Services +{ + /// + /// 基于 ILocalizationProvider 的 IStringLocalizer 实现: + /// 使用 JSON 文件中的键值,未找到返回 key 本身。 + /// 名称改为 JsonStringLocalizer 避免与框架的 StringLocalizer 冲突。 + /// + public class JsonStringLocalizer : IStringLocalizer + { + private readonly ILocalizationProvider _provider; + + public JsonStringLocalizer(ILocalizationProvider provider) + { + _provider = provider; + } + + public LocalizedString this[string name] + { + get + { + var value = _provider.GetString(name); + if (value == null) + { + // Avoid synchronous blocking during server prerender. Start background load and return key. + try + { + _ = _provider.LoadCultureAsync(_provider.CurrentCulture); + } + catch { } + } + + var result = value ?? name; + return new LocalizedString(name, result, resourceNotFound: result == name); + } + } + + public LocalizedString this[string name, params object[] arguments] + { + get + { + var fmt = _provider.GetString(name); + if (fmt == null) + { + try + { + _ = _provider.LoadCultureAsync(_provider.CurrentCulture); + } + catch { } + } + + var format = fmt ?? name; + var value = string.Format(format, arguments); + return new LocalizedString(name, value, resourceNotFound: format == name); + } + } + + public IEnumerable GetAllStrings(bool includeParentCultures) + { + var list = new List(); + var providerType = _provider.GetType(); + var currentProp = providerType.GetProperty("CurrentCulture"); + var culture = currentProp?.GetValue(_provider) as string ?? string.Empty; + var cacheField = providerType.GetField("_cache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (!string.IsNullOrEmpty(culture) && cacheField?.GetValue(_provider) is Dictionary> cache && cache.TryGetValue(culture, out var dict)) + { + foreach (var kv in dict) + { + list.Add(new LocalizedString(kv.Key, kv.Value, resourceNotFound: false)); + } + } + return list; + } + + public IStringLocalizer WithCulture(CultureInfo culture) + { + return this; + } + } +} diff --git a/Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs b/Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs index 060daa4..1a3701c 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs +++ b/Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs @@ -505,80 +505,5 @@ namespace Atomx.Admin.Client.Services } } - /// - /// ILocalizationProvider IStringLocalizer ʵ֣ - /// ʹ JSON ļеļֵδҵ key - /// ƸΪ JsonStringLocalizer ܵ StringLocalizer ͻ - /// - public class JsonStringLocalizer : IStringLocalizer - { - private readonly ILocalizationProvider _provider; - - public JsonStringLocalizer(ILocalizationProvider provider) - { - _provider = provider; - } - - public LocalizedString this[string name] - { - get - { - var value = _provider.GetString(name); - if (value == null) - { - // Avoid synchronous blocking during server prerender. Start background load and return key. - try - { - _ = _provider.LoadCultureAsync(_provider.CurrentCulture); - } - catch { } - } - - var result = value ?? name; - return new LocalizedString(name, result, resourceNotFound: result == name); - } - } - - public LocalizedString this[string name, params object[] arguments] - { - get - { - var fmt = _provider.GetString(name); - if (fmt == null) - { - try - { - _ = _provider.LoadCultureAsync(_provider.CurrentCulture); - } - catch { } - } - - var format = fmt ?? name; - var value = string.Format(format, arguments); - return new LocalizedString(name, value, resourceNotFound: format == name); - } - } - - public IEnumerable GetAllStrings(bool includeParentCultures) - { - var list = new List(); - var providerType = _provider.GetType(); - var currentProp = providerType.GetProperty("CurrentCulture"); - var culture = currentProp?.GetValue(_provider) as string ?? string.Empty; - var cacheField = providerType.GetField("_cache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - if (!string.IsNullOrEmpty(culture) && cacheField?.GetValue(_provider) is Dictionary> cache && cache.TryGetValue(culture, out var dict)) - { - foreach (var kv in dict) - { - list.Add(new LocalizedString(kv.Key, kv.Value, resourceNotFound: false)); - } - } - return list; - } - - public IStringLocalizer WithCulture(CultureInfo culture) - { - return this; - } - } + } diff --git a/Atomx.Admin/Atomx.Admin.Client/Services/WasmLocalizationProvider.cs b/Atomx.Admin/Atomx.Admin.Client/Services/WasmLocalizationProvider.cs index ea4e113..cd45f04 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Services/WasmLocalizationProvider.cs +++ b/Atomx.Admin/Atomx.Admin.Client/Services/WasmLocalizationProvider.cs @@ -13,6 +13,7 @@ namespace Atomx.Admin.Client.Services private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly ILocalStorageService _localStorage; + private readonly ILocalizationService _localizationService; private static readonly ConcurrentDictionary> _cache = new(); private readonly HashSet _loadingCultures = new(); private string _currentCulture = "zh-Hans"; @@ -21,12 +22,13 @@ namespace Atomx.Admin.Client.Services private const string LocalizationStorageKey = "Localization_{0}"; private const string LocalizationVersionKey = "LocalizationVersion_{0}"; - public WasmLocalizationProvider(IJSRuntime jsRuntime, HttpClient httpClient, ILogger logger, ILocalStorageService localStorage) + public WasmLocalizationProvider(IJSRuntime jsRuntime, HttpClient httpClient, ILogger logger, ILocalStorageService localStorage, ILocalizationService localizationService) { _jsRuntime = jsRuntime; _httpClient = httpClient; _logger = logger; _localStorage = localStorage; + _localizationService = localizationService; } public string CurrentCulture => _currentCulture; @@ -51,14 +53,43 @@ namespace Atomx.Admin.Client.Services if (_isInitialized) return; await LoadCultureAsync(_currentCulture); + + // ensure thread cultures and notify localization service + try + { + var ci = new CultureInfo(_currentCulture); + CultureInfo.DefaultThreadCurrentCulture = ci; + CultureInfo.DefaultThreadCurrentUICulture = ci; + _localizationService.SetLanguage(ci); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to set culture after initialize: {Culture}", _currentCulture); + } + _isInitialized = true; LanguageChanged?.Invoke(this, _currentCulture); } public async Task SetCultureAsync(string cultureShortOrFull) { - _currentCulture = MapToFullCulture(cultureShortOrFull); + var full = MapToFullCulture(cultureShortOrFull); + _currentCulture = full; + await LoadCultureAsync(_currentCulture); + + try + { + var ci = new CultureInfo(_currentCulture); + CultureInfo.DefaultThreadCurrentCulture = ci; + CultureInfo.DefaultThreadCurrentUICulture = ci; + _localizationService.SetLanguage(ci); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to set culture in SetCultureAsync: {Culture}", _currentCulture); + } + LanguageChanged?.Invoke(this, _currentCulture); } @@ -101,6 +132,20 @@ namespace Atomx.Admin.Client.Services if (cachedVersion == serverVersion && cachedData != null) { _logger.LogInformation("Localization data for {Culture} is up-to-date.", cultureFull); + + // ensure thread cultures and notify localization service when using cached data + try + { + var ci = new CultureInfo(cultureFull); + CultureInfo.DefaultThreadCurrentCulture = ci; + CultureInfo.DefaultThreadCurrentUICulture = ci; + _localizationService.SetLanguage(ci); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to set culture after loading from cache: {Culture}", cultureFull); + } + return; } @@ -115,6 +160,19 @@ namespace Atomx.Admin.Client.Services await _localStorage.SetItemAsync(localDataKey, dict); _logger.LogInformation("Loaded localization file for {Culture} from server and updated local storage.", cultureFull); + + // ensure thread cultures and notify localization service after fetching + try + { + var ci = new CultureInfo(cultureFull); + CultureInfo.DefaultThreadCurrentCulture = ci; + CultureInfo.DefaultThreadCurrentUICulture = ci; + _localizationService.SetLanguage(ci); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to set culture after fetching: {Culture}", cultureFull); + } } catch (Exception ex) { @@ -127,6 +185,9 @@ namespace Atomx.Admin.Client.Services _loadingCultures.Remove(cultureFull); } } + + // Notify listeners that the culture has been loaded + LanguageChanged?.Invoke(this, cultureFull); } private string MapToFullCulture(string culture) diff --git a/Atomx.Admin/Atomx.Admin/Atomx.Admin.csproj b/Atomx.Admin/Atomx.Admin/Atomx.Admin.csproj index 75396aa..61efd74 100644 --- a/Atomx.Admin/Atomx.Admin/Atomx.Admin.csproj +++ b/Atomx.Admin/Atomx.Admin/Atomx.Admin.csproj @@ -18,6 +18,7 @@ + diff --git a/Atomx.Admin/Atomx.Admin/Controllers/LocaleResourceController.cs b/Atomx.Admin/Atomx.Admin/Controllers/LocaleResourceController.cs index d9765a2..e14a0eb 100644 --- a/Atomx.Admin/Atomx.Admin/Controllers/LocaleResourceController.cs +++ b/Atomx.Admin/Atomx.Admin/Controllers/LocaleResourceController.cs @@ -3,12 +3,16 @@ using Atomx.Admin.Client.Validators; using Atomx.Admin.Services; using Atomx.Common.Entities; using Atomx.Common.Models; +using Atomx.Core.Jos; using Atomx.Data; using Atomx.Data.CacheServices; using Atomx.Data.Services; +using Atomx.Utils.Json; using Atomx.Utils.Models; using MapsterMapper; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; namespace Atomx.Admin.Controllers { @@ -23,6 +27,8 @@ namespace Atomx.Admin.Controllers private readonly IMapper _mapper; private readonly JwtSetting _jwtSettings; private readonly ICacheService _cacheService; + private readonly IBackgroundJobService _backgroundService; + private readonly IWebHostEnvironment _environment; /// @@ -35,7 +41,7 @@ namespace Atomx.Admin.Controllers /// /// /// - public LocaleResourceController(ILogger logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, JwtSetting jwtSettings, ICacheService cacheService) + public LocaleResourceController(ILogger logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, JwtSetting jwtSettings, ICacheService cacheService, IBackgroundJobService backgroundJobService, IWebHostEnvironment environment) { _logger = logger; _idCreator = idCreator; @@ -44,6 +50,8 @@ namespace Atomx.Admin.Controllers _mapper = mapper; _jwtSettings = jwtSettings; _cacheService = cacheService; + _backgroundService = backgroundJobService; + _environment = environment; } /// @@ -198,6 +206,11 @@ namespace Atomx.Admin.Controllers } //异步更新对应的json文件 + var wwwroot = _environment.WebRootPath; + var dic = new Dictionary(); + dic.Add(data.Name,data.Value); + + _backgroundService.UpdateLocalizationFile(wwwroot, model.Culture, dic.ToJson()); result = result.IsSuccess(true); @@ -210,29 +223,16 @@ namespace Atomx.Admin.Controllers /// /// [HttpGet("version/{culture}")] + [AllowAnonymous] public async Task GetVersion(string culture) { - var result = new ApiResult(); + var result = string.Empty; var data = await _cacheService.GetLanguageByCulture(culture); if (data != null) { - result = result.IsSuccess(data.ResourceVersion); + result = data.ResourceVersion; } return new JsonResult(result); } - - /// - /// 获取文化语言数据 - /// - /// - /// - [HttpGet("resources/{culture}")] - public IActionResult GetLocaleResources(string culture) - { - var result = new ApiResult(); - - - return new JsonResult(result); - } } } diff --git a/Atomx.Admin/Atomx.Admin/Middlewares/RequestCultureMiddleware.cs b/Atomx.Admin/Atomx.Admin/Middlewares/RequestCultureMiddleware.cs index 127dd0e..39eba59 100644 --- a/Atomx.Admin/Atomx.Admin/Middlewares/RequestCultureMiddleware.cs +++ b/Atomx.Admin/Atomx.Admin/Middlewares/RequestCultureMiddleware.cs @@ -53,7 +53,7 @@ namespace Atomx.Admin.Middlewares logger?.LogWarning(ex, "Failed to set thread culture to {Culture}", cultureName); } - // Attempt to synchronously load localization for server-side rendering + // 尝试同步加载服务器端渲染的本地化内容 try { var providerObj = context.RequestServices.GetService(typeof(Atomx.Admin.Client.Services.ILocalizationProvider)); @@ -61,10 +61,10 @@ namespace Atomx.Admin.Middlewares { logger?.LogDebug("ILocalizationProvider not registered in RequestServices"); } - else if (providerObj is Atomx.Admin.Client.Services.LocalizationProvider provider) + else if (providerObj is Atomx.Admin.Services.ServerLocalizationProvider provider) { logger?.LogDebug("Calling SetCultureForServer on LocalizationProvider with {Culture}", cultureName); - provider.SetCultureForServer(cultureName); + _= provider.SetCultureAsync(cultureName); logger?.LogInformation("LocalizationProvider.CurrentCulture after SetCultureForServer: {Culture}", provider.CurrentCulture); } else diff --git a/Atomx.Admin/Atomx.Admin/Program.cs b/Atomx.Admin/Atomx.Admin/Program.cs index def4087..65b8a85 100644 --- a/Atomx.Admin/Atomx.Admin/Program.cs +++ b/Atomx.Admin/Atomx.Admin/Program.cs @@ -7,6 +7,7 @@ using Atomx.Admin.Models; using Atomx.Admin.Services; using Atomx.Admin.Utils; using Atomx.Common.Models; +using Atomx.Core.Jos; using Atomx.Data; using Atomx.Data.Services; using Atomx.Utils.Json.Converts; @@ -180,6 +181,9 @@ builder.Services.Configure(builder.Configuration.GetSection(" // ע FluentValidation ֤ builder.Services.AddValidatorsFromAssembly(typeof(Atomx.Admin.Client.Validators.LoginModelValidator).Assembly); +// Register IBackgroundJobService and its implementation +builder.Services.AddScoped(); + var app = builder.Build(); app.AddDataMigrate(); diff --git a/Atomx.Admin/Atomx.Admin/Services/LocalizationFileService.cs b/Atomx.Admin/Atomx.Admin/Services/LocalizationFileService.cs deleted file mode 100644 index 7208dae..0000000 --- a/Atomx.Admin/Atomx.Admin/Services/LocalizationFileService.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Atomx.Common.Models; -using System.Text.Json; - -namespace Atomx.Admin.Services -{ - public interface ILocalizationFileService - { - Task> GetTranslationsAsync(string culture); - Task SaveTranslationsAsync(string version, string culture, Dictionary translations); - Task FileExistsAsync(string culture); - } - - public class LocalizationFileService : ILocalizationFileService - { - private readonly IWebHostEnvironment _environment; - private readonly ILogger _logger; - private readonly string _resourcesPath; - - public LocalizationFileService( - IWebHostEnvironment environment, - ILogger logger) - { - _environment = environment; - _logger = logger; - _resourcesPath = Path.Combine(_environment.ContentRootPath, "Resources"); - - EnsureResourcesDirectoryExists(); - } - - /// - /// 读取指定文化语言的译文 - /// - /// - /// - public async Task> GetTranslationsAsync(string culture) - { - var filePath = GetFilePath(culture); - - if (!File.Exists(filePath)) - { - _logger.LogWarning("Localization file not found for culture: {Culture}", culture); - return new Dictionary(); - } - - try - { - var json = await File.ReadAllTextAsync(filePath); - var fileData = JsonSerializer.Deserialize(json, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); - - return fileData?.Translations ?? new Dictionary(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading localization file for culture: {Culture}", culture); - return new Dictionary(); - } - } - - /// - /// 保存本地化译文 - /// - /// - /// - /// - /// - public async Task SaveTranslationsAsync(string version, string culture, Dictionary translations) - { - try - { - var fileData = new LocalizationFile - { - ResourceVersion = culture, - Translations = translations - }; - - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; - - var json = JsonSerializer.Serialize(fileData, options); - var filePath = GetFilePath(culture); - - await File.WriteAllTextAsync(filePath, json); - - _logger.LogInformation("Saved localization file for culture: {Culture} with {Count} translations", - culture, translations.Count); - - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving localization file for culture: {Culture}", culture); - return false; - } - } - - /// - /// 判断文化语言文件是否存在 - /// - /// - /// - public Task FileExistsAsync(string culture) - { - var filePath = GetFilePath(culture); - return Task.FromResult(File.Exists(filePath)); - } - - /// - /// 获取文件最后修改时间 - /// - /// - /// - public DateTime GetFileLastModified(string culture) - { - var filePath = GetFilePath(culture); - return File.Exists(filePath) ? File.GetLastWriteTimeUtc(filePath) : DateTime.MinValue; - } - - /// - /// 获取文件路径 - /// - /// - /// - private string GetFilePath(string culture) - { - var fileName = $"{culture}.json"; - return Path.Combine(_resourcesPath, fileName); - } - - /// - /// 判断存放资源文件的文件夹是否存在,不存在则创建 - /// - private void EnsureResourcesDirectoryExists() - { - if (!Directory.Exists(_resourcesPath)) - { - Directory.CreateDirectory(_resourcesPath); - _logger.LogInformation("Created Resources directory: {Path}", _resourcesPath); - } - } - } -} diff --git a/Atomx.Core/Jos/LocalizationJob.cs b/Atomx.Core/Jos/LocalizationJob.cs index 44faa4e..b2a16c3 100644 --- a/Atomx.Core/Jos/LocalizationJob.cs +++ b/Atomx.Core/Jos/LocalizationJob.cs @@ -1,6 +1,9 @@ -using Atomx.Utils.Json; +using Atomx.Data; +using Atomx.Data.CacheServices; +using Atomx.Utils.Json; using Hangfire; using Microsoft.Extensions.Logging; +using System.Security.Cryptography; using System.Text.Json; namespace Atomx.Core.Jos @@ -12,9 +15,13 @@ namespace Atomx.Core.Jos public class LocalizationJob { readonly ILogger _logger; - public LocalizationJob(ILogger logger) + readonly DataContext _dbContext; + readonly ICacheService _cacheService; + public LocalizationJob(ILogger logger, DataContext dataContext, ICacheService cacheService) { _logger = logger; + _dbContext = dataContext; + _cacheService = cacheService; } /// @@ -30,46 +37,58 @@ namespace Atomx.Core.Jos var fileName = $"{culture}.json"; var filePath = Path.Combine(path, fileName); - if (!Directory.Exists(filePath)) + if (!Directory.Exists(path)) { - Directory.CreateDirectory(filePath); - _logger.LogInformation("Created Resources directory: {Path}", filePath); + Directory.CreateDirectory(path); + _logger.LogInformation("Created Resources directory: {Path}", path); } - var json = await File.ReadAllTextAsync(filePath); - var fileData = JsonSerializer.Deserialize>(json, new JsonSerializerOptions + var fileData = new Dictionary(); + if (File.Exists(filePath)) { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); - if (fileData == null) - { - fileData = new Dictionary(); + var json = await File.ReadAllTextAsync(filePath); + fileData = JsonSerializer.Deserialize>(json, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }) ?? new Dictionary(); } + foreach (var item in translations) { - if (fileData.ContainsKey(item.Key)) - { - fileData[item.Key] = item.Value; - } - else - { - fileData.Add(item.Key, item.Value); - } + fileData[item.Key] = item.Value; } + var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }; - json = JsonSerializer.Serialize(fileData, options); - await File.WriteAllTextAsync(filePath, json); + var updatedJson = JsonSerializer.Serialize(fileData, options); + await File.WriteAllTextAsync(filePath, updatedJson); + + // 更新文件后,更新数据库中的资源版本 + string fileHash; + using (var sha256 = SHA256.Create()) + using (var stream = File.OpenRead(filePath)) + { + var hashBytes = sha256.ComputeHash(stream); + fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } + var language = _dbContext.Languages.FirstOrDefault(l => l.Culture == culture); + if (language != null) + { + language.UpdateTime = DateTime.UtcNow; + language.ResourceVersion = fileHash; + await _dbContext.SaveChangesAsync(); + await _cacheService.GetLanguageById(language.Id, language); + } - - _logger.LogInformation("Saved localization file for culture: {Culture} with {Count} translations", - culture, translations.Count); + _logger.LogInformation("Saved localization file for culture: {Culture} with {Count} translations. File hash: {Hash}", + culture, translations.Count, fileHash); } - catch(Exception ex) { + catch (Exception ex) + { _logger.LogError(ex, "Error saving localization file for culture: {Culture}", culture); } } diff --git a/Atomx.Core/Services/BackgroundJobsService.cs b/Atomx.Core/Services/BackgroundJobService.cs similarity index 78% rename from Atomx.Core/Services/BackgroundJobsService.cs rename to Atomx.Core/Services/BackgroundJobService.cs index c3d317d..dd94c63 100644 --- a/Atomx.Core/Services/BackgroundJobsService.cs +++ b/Atomx.Core/Services/BackgroundJobService.cs @@ -1,12 +1,9 @@ using Hangfire; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Text; namespace Atomx.Core.Jos { - public partial interface IBackgroundJobsService + public partial interface IBackgroundJobService { /// /// 更新本地化文件 @@ -19,12 +16,12 @@ namespace Atomx.Core.Jos string SendSMSVerificationCode(string phoneNumber, string code, TimeSpan validDuration); } - public partial class BackgroundJobsService : IBackgroundJobsService + public partial class BackgroundJobService : IBackgroundJobService { readonly IBackgroundJobClient _backgroundJobClient; readonly IRecurringJobManager _recurringJobManager; - readonly ILogger _logger; - public BackgroundJobsService(IBackgroundJobClient backgroundJobClient, IRecurringJobManager recurringJobManager, ILogger logger) + readonly ILogger _logger; + public BackgroundJobService(IBackgroundJobClient backgroundJobClient, IRecurringJobManager recurringJobManager, ILogger logger) { _backgroundJobClient = backgroundJobClient; _recurringJobManager = recurringJobManager;