using System.Linq.Expressions; using System.Reflection; using Nop.Core; using Nop.Core.Caching; using Nop.Core.Domain.Localization; using Nop.Data; namespace Nop.Services.Localization; /// /// Provides information about localizable entities /// public partial class LocalizedEntityService : ILocalizedEntityService { #region Fields protected readonly IRepository _localizedPropertyRepository; protected readonly IStaticCacheManager _staticCacheManager; protected readonly LocalizationSettings _localizationSettings; #endregion #region Ctor public LocalizedEntityService(IRepository localizedPropertyRepository, IStaticCacheManager staticCacheManager, LocalizationSettings localizationSettings) { _localizedPropertyRepository = localizedPropertyRepository; _staticCacheManager = staticCacheManager; _localizationSettings = localizationSettings; } #endregion #region Utilities /// /// Gets localized properties /// /// Entity identifier /// Locale key group /// /// A task that represents the asynchronous operation /// The task result contains the localized properties /// protected virtual async Task> GetLocalizedPropertiesAsync(int entityId, string localeKeyGroup) { if (entityId == 0 || string.IsNullOrEmpty(localeKeyGroup)) return new List(); var query = from lp in _localizedPropertyRepository.Table orderby lp.Id where lp.EntityId == entityId && lp.LocaleKeyGroup == localeKeyGroup select lp; var props = await query.ToListAsync(); return props; } /// /// Gets all cached localized properties /// /// /// A task that represents the asynchronous operation /// The task result contains the cached localized properties /// protected virtual async Task> GetAllLocalizedPropertiesAsync() { return await _localizedPropertyRepository.GetAllAsync(query => { return from lp in query select lp; }, cache => default); } /// /// Gets all cached localized properties by language /// /// /// A task that represents the asynchronous operation /// The task result contains the uncached localized properties /// protected virtual async Task> GetAllLocalizedPropertiesAsync(int languageId) { // do not cache here return await _localizedPropertyRepository.GetAllAsync(query => query.Where(lp => lp.LanguageId == languageId)); } /// /// Deletes a localized property /// /// Localized property /// A task that represents the asynchronous operation protected virtual async Task DeleteLocalizedPropertyAsync(LocalizedProperty localizedProperty) { await _localizedPropertyRepository.DeleteAsync(localizedProperty); } /// /// Inserts a localized property /// /// Localized property /// A task that represents the asynchronous operation protected virtual async Task InsertLocalizedPropertyAsync(LocalizedProperty localizedProperty) { await _localizedPropertyRepository.InsertAsync(localizedProperty); } /// /// Updates the localized property /// /// Localized property /// A task that represents the asynchronous operation protected virtual async Task UpdateLocalizedPropertyAsync(LocalizedProperty localizedProperty) { await _localizedPropertyRepository.UpdateAsync(localizedProperty); } #endregion #region Methods /// /// Find localized properties /// /// Entity identifier /// Locale key group /// Locale key /// /// A task that represents the asynchronous operation /// The task result contains the found localized properties /// public virtual async Task> GetEntityLocalizedPropertiesAsync(int entityId, string localeKeyGroup, string localeKey) { var key = _staticCacheManager.PrepareKeyForDefaultCache(NopLocalizationDefaults.LocalizedPropertiesCacheKey, entityId, localeKeyGroup, localeKey); return await _staticCacheManager.GetAsync(key, async () => { var source = _localizationSettings.LoadAllLocalizedPropertiesOnStartup //load all records (we know they are cached) ? (await GetAllLocalizedPropertiesAsync()).AsQueryable() //gradual loading : _localizedPropertyRepository.Table; var query = from lp in source where lp.EntityId == entityId && lp.LocaleKeyGroup == localeKeyGroup && lp.LocaleKey == localeKey select lp; return await query.ToListAsync(); }); } /// /// Find localized value /// /// Language identifier /// Entity identifier /// Locale key group /// Locale key /// /// A task that represents the asynchronous operation /// The task result contains the found localized value /// public virtual async Task GetLocalizedValueAsync(int languageId, int entityId, string localeKeyGroup, string localeKey) { if (_localizationSettings.LoadAllLocalizedPropertiesOnStartup) { //value tuples aren't json-serializable by default, so we use a string key static string formatKey(string keyGroup, string key, int id) => $"{keyGroup}:{key}:{id}"; var localizedValues = await _staticCacheManager.GetAsync( _staticCacheManager.PrepareKeyForDefaultCache(NopLocalizationDefaults.LocalizedPropertyLookupCacheKey, languageId), async () => (await GetAllLocalizedPropertiesAsync(languageId)) .GroupBy(p => formatKey(p.LocaleKeyGroup, p.LocaleKey, p.EntityId)) .ToDictionary(g => g.Key, g => g.First().LocaleValue)); return localizedValues.TryGetValue(formatKey(localeKeyGroup, localeKey, entityId), out var localeValue) ? localeValue : string.Empty; } //gradual loading var key = _staticCacheManager.PrepareKeyForDefaultCache(NopLocalizationDefaults.LocalizedPropertyCacheKey , languageId, entityId, localeKeyGroup, localeKey); return await _staticCacheManager.GetAsync(key, async () => { var query = from lp in _localizedPropertyRepository.Table where lp.LanguageId == languageId && lp.EntityId == entityId && lp.LocaleKeyGroup == localeKeyGroup && lp.LocaleKey == localeKey select lp.LocaleValue; //little hack here. nulls aren't cacheable so set it to "" return await query.FirstOrDefaultAsync() ?? string.Empty; }); } /// /// Save localized value /// /// Type /// Entity /// Key selector /// Locale value /// Language ID /// A task that represents the asynchronous operation public virtual async Task SaveLocalizedValueAsync(T entity, Expression> keySelector, string localeValue, int languageId) where T : BaseEntity, ILocalizedEntity { await SaveLocalizedValueAsync(entity, keySelector, localeValue, languageId); } /// /// Save localized value /// /// Type /// Property type /// Entity /// Key selector /// Locale value /// Language ID /// A task that represents the asynchronous operation public virtual async Task SaveLocalizedValueAsync(T entity, Expression> keySelector, TPropType localeValue, int languageId) where T : BaseEntity, ILocalizedEntity { ArgumentNullException.ThrowIfNull(entity); if (languageId == 0) throw new ArgumentOutOfRangeException(nameof(languageId), "Language ID should not be 0"); if (keySelector.Body is not MemberExpression member) { throw new ArgumentException(string.Format( "Expression '{0}' refers to a method, not a property.", keySelector)); } var propInfo = member.Member as PropertyInfo ?? throw new ArgumentException(string.Format( "Expression '{0}' refers to a field, not a property.", keySelector)); //load localized value (check whether it's a cacheable entity. In such cases we load its original entity type) var localeKeyGroup = entity.GetType().Name; var localeKey = propInfo.Name; var props = await GetLocalizedPropertiesAsync(entity.Id, localeKeyGroup); var prop = props.FirstOrDefault(lp => lp.LanguageId == languageId && lp.LocaleKey.Equals(localeKey, StringComparison.InvariantCultureIgnoreCase)); //should be culture invariant var localeValueStr = CommonHelper.To(localeValue); if (prop != null) { if (string.IsNullOrWhiteSpace(localeValueStr)) { //delete await DeleteLocalizedPropertyAsync(prop); } else { //update prop.LocaleValue = localeValueStr; await UpdateLocalizedPropertyAsync(prop); } } else { if (string.IsNullOrWhiteSpace(localeValueStr)) return; //insert prop = new LocalizedProperty { EntityId = entity.Id, LanguageId = languageId, LocaleKey = localeKey, LocaleKeyGroup = localeKeyGroup, LocaleValue = localeValueStr }; await InsertLocalizedPropertyAsync(prop); } } #endregion }