using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; /// /// Represents a repository for doing CRUD operations for /// internal class DictionaryRepository : EntityRepositoryBase, IDictionaryRepository { private readonly ILoggerFactory _loggerFactory; private readonly ILanguageRepository _languageRepository; public DictionaryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILoggerFactory loggerFactory, ILanguageRepository languageRepository) : base(scopeAccessor, cache, logger) { _loggerFactory = loggerFactory; _languageRepository = languageRepository; } public IDictionaryItem? Get(Guid uniqueId) { var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); return uniqueIdRepo.Get(uniqueId); } public IEnumerable GetMany(params Guid[] uniqueIds) { var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); return uniqueIdRepo.GetMany(uniqueIds); } public IDictionaryItem? Get(string key) { var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); return keyRepo.Get(key); } public IEnumerable GetManyByKeys(string[] keys) { var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); return keyRepo.GetMany(keys); } public Dictionary GetDictionaryItemKeyMap() { var columns = new[] { "key", "id" }.Select(x => (object)SqlSyntax.GetQuotedColumnName(x)).ToArray(); Sql sql = Sql().Select(columns).From(); return Database.Fetch(sql).ToDictionary(x => x.Key, x => x.Id); } public IEnumerable GetDictionaryItemDescendants(Guid? parentId, string? filter = null) { IDictionary languageIsoCodeById = GetLanguagesById(); // This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive // lookup to get descendants. Currently this is the most efficient way to do it Func>> getItemsFromParents = guids => { return guids.InGroupsOf(Constants.Sql.MaxParameterCount) .Select(group => { Sql sql = GetBaseQuery(false) .Where(x => x.Parent != null) .WhereIn(x => x.Parent, group); if (filter.IsNullOrWhiteSpace() is false) { sql.Where(x => x.Key.StartsWith(filter)); } sql.OrderBy(x => x.UniqueId); return Database .FetchOneToMany(x => x.LanguageTextDtos, sql) .Select(dto => ConvertFromDto(dto, languageIsoCodeById)); }); }; if (!parentId.HasValue) { Sql sql = GetBaseQuery(false) .Where(x => x.PrimaryKey > 0); if (filter.IsNullOrWhiteSpace() is false) { sql.Where(x => x.Key.StartsWith(filter)); } return Database .FetchOneToMany(x => x.LanguageTextDtos, sql) .Select(dto => ConvertFromDto(dto, languageIsoCodeById)) .OrderBy(DictionaryItemOrdering); } return getItemsFromParents(new[] { parentId.Value }) .SelectRecursive(items => getItemsFromParents(items.Select(x => x.Key).ToArray())).SelectMany(items => items) .OrderBy(DictionaryItemOrdering); // we're loading all descendants into memory, sometimes recursively... so we have to order them in memory too string DictionaryItemOrdering(IDictionaryItem item) => item.ItemKey; } protected override IRepositoryCachePolicy CreateCachePolicy() { var options = new RepositoryCachePolicyOptions { // allow zero to be cached GetAllCacheAllowZeroCount = true, }; return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); } private IDictionaryItem ConvertFromDto(DictionaryDto dto, IDictionary languagesById) { IDictionaryItem entity = DictionaryItemFactory.BuildEntity(dto); entity.Translations = dto.LanguageTextDtos.EmptyNull() .Where(x => x.LanguageId > 0) .Select(x => languagesById.TryGetValue(x.LanguageId, out ILanguage? language) ? DictionaryTranslationFactory.BuildEntity(x, dto.UniqueId, language) : null) .WhereNotNull() .ToList(); return entity; } #region Overrides of RepositoryBase protected override IDictionaryItem? PerformGet(int id) { Sql sql = GetBaseQuery(false) .Where(GetBaseWhereClause(), new { id }) .OrderBy(x => x.UniqueId); DictionaryDto? dto = Database .FetchOneToMany(x => x.LanguageTextDtos, sql) .FirstOrDefault(); if (dto == null) { return null; } IDictionaryItem entity = ConvertFromDto(dto, GetLanguagesById()); // reset dirty initial properties (U4-1946) ((EntityBase)entity).ResetDirtyProperties(false); return entity; } private IEnumerable GetRootDictionaryItems() { IQuery query = Query().Where(x => x.ParentId == null); return Get(query); } private class DictionaryItemKeyIdDto { public string Key { get; } = null!; public Guid Id { get; set; } } private class DictionaryByUniqueIdRepository : SimpleGetRepository { private readonly DictionaryRepository _dictionaryRepository; private readonly IDictionary _languagesById; public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) { _dictionaryRepository = dictionaryRepository; _languagesById = dictionaryRepository.GetLanguagesById(); } protected override IEnumerable PerformFetch(Sql sql) => Database .FetchOneToMany(x => x.LanguageTextDtos, sql); protected override Sql GetBaseQuery(bool isCount) => _dictionaryRepository.GetBaseQuery(isCount); protected override string GetBaseWhereClause() => "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " = @id"; protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) => _dictionaryRepository.ConvertFromDto(dto, _languagesById); protected override object GetBaseWhereClauseArguments(Guid id) => new { id }; protected override string GetWhereInClauseForGetAll() => "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; protected override IRepositoryCachePolicy CreateCachePolicy() { var options = new RepositoryCachePolicyOptions { // allow zero to be cached GetAllCacheAllowZeroCount = true, }; return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); } protected override IEnumerable PerformGetAll(params Guid[]? ids) { Sql sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); if (ids?.Any() ?? false) { sql.WhereIn(x => x.UniqueId, ids); } return Database .FetchOneToMany(x => x.LanguageTextDtos, sql) .Select(ConvertToEntity); } } private class DictionaryByKeyRepository : SimpleGetRepository { private readonly DictionaryRepository _dictionaryRepository; private readonly IDictionary _languagesById; public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) { _dictionaryRepository = dictionaryRepository; _languagesById = dictionaryRepository.GetLanguagesById(); } protected override IEnumerable PerformFetch(Sql sql) => Database .FetchOneToMany(x => x.LanguageTextDtos, sql); protected override Sql GetBaseQuery(bool isCount) => _dictionaryRepository.GetBaseQuery(isCount); protected override string GetBaseWhereClause() => "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " = @id"; protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) => _dictionaryRepository.ConvertFromDto(dto, _languagesById); protected override object GetBaseWhereClauseArguments(string? id) => new { id }; protected override string GetWhereInClauseForGetAll() => "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; protected override IRepositoryCachePolicy CreateCachePolicy() { var options = new RepositoryCachePolicyOptions { // allow zero to be cached GetAllCacheAllowZeroCount = true, }; return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); } protected override IEnumerable PerformGetAll(params string[]? ids) { Sql sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); if (ids?.Any() ?? false) { sql.WhereIn(x => x.Key, ids); } return Database .FetchOneToMany(x => x.LanguageTextDtos, sql) .Select(ConvertToEntity); } } protected override IEnumerable PerformGetAll(params int[]? ids) { Sql sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); if (ids?.Any() ?? false) { sql.WhereIn(x => x.PrimaryKey, ids); } IDictionary languageIsoCodeById = GetLanguagesById(); return Database .FetchOneToMany(x => x.LanguageTextDtos, sql) .Select(dto => ConvertFromDto(dto, languageIsoCodeById)); } protected override IEnumerable PerformGetByQuery(IQuery query) { Sql sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); Sql sql = translator.Translate(); sql.OrderBy(x => x.UniqueId); IDictionary languageIsoCodeById = GetLanguagesById(); return Database .FetchOneToMany(x => x.LanguageTextDtos, sql) .Select(dto => ConvertFromDto(dto, languageIsoCodeById)); } #endregion #region Overrides of EntityRepositoryBase protected override Sql GetBaseQuery(bool isCount) { Sql sql = Sql(); if (isCount) { sql.SelectCount() .From(); } else { sql.SelectAll() .From() .LeftJoin() .On(left => left.UniqueId, right => right.UniqueId); } return sql; } protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.DictionaryEntry}.pk = @id"; protected override IEnumerable GetDeleteClauses() => new List(); #endregion #region Unit of Work Implementation protected override void PersistNewItem(IDictionaryItem entity) { var dictionaryItem = (DictionaryItem)entity; dictionaryItem.AddingEntity(); foreach (IDictionaryTranslation translation in dictionaryItem.Translations) { translation.Value = translation.Value.ToValidXmlString(); } DictionaryDto dto = DictionaryItemFactory.BuildDto(dictionaryItem); var id = Convert.ToInt32(Database.Insert(dto)); dictionaryItem.Id = id; IDictionary languagesByIsoCode = GetLanguagesByIsoCode(); foreach (IDictionaryTranslation translation in dictionaryItem.Translations) { LanguageTextDto textDto = DictionaryTranslationFactory.BuildDto(translation, dictionaryItem.Key, languagesByIsoCode); translation.Id = Convert.ToInt32(Database.Insert(textDto)); translation.Key = dictionaryItem.Key; } dictionaryItem.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IDictionaryItem entity) { entity.UpdatingEntity(); foreach (IDictionaryTranslation translation in entity.Translations) { translation.Value = translation.Value.ToValidXmlString(); } DictionaryDto dto = DictionaryItemFactory.BuildDto(entity); Database.Update(dto); IDictionary languagesByIsoCode = GetLanguagesByIsoCode(); foreach (IDictionaryTranslation translation in entity.Translations) { LanguageTextDto textDto = DictionaryTranslationFactory.BuildDto(translation, entity.Key, languagesByIsoCode); if (translation.HasIdentity) { Database.Update(textDto); } else { translation.Id = Convert.ToInt32(Database.Insert(textDto)); translation.Key = entity.Key; } } entity.ResetDirtyProperties(); // Clear the cache entries that exist by uniqueid/item key IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); } protected override void PersistDeletedItem(IDictionaryItem entity) { RecursiveDelete(entity.Key); Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key }); Database.Delete("WHERE id = @Id", new { Id = entity.Key }); // Clear the cache entries that exist by uniqueid/item key IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); entity.DeleteDate = DateTime.Now; } private void RecursiveDelete(Guid parentId) { List? list = Database.Fetch("WHERE parent = @ParentId", new { ParentId = parentId }); foreach (DictionaryDto? dto in list) { RecursiveDelete(dto.UniqueId); Database.Delete("WHERE UniqueId = @Id", new { Id = dto.UniqueId }); Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); // Clear the cache entries that exist by uniqueid/item key IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); } } private IDictionary GetLanguagesById() => _languageRepository .GetMany() .ToDictionary(language => language.Id); private IDictionary GetLanguagesByIsoCode() => _languageRepository .GetMany() .ToDictionary(language => language.IsoCode); #endregion }