using System.Linq.Expressions; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services; public class EntityService : RepositoryService, IEntityService { private readonly IEntityRepository _entityRepository; private readonly IIdKeyMap _idKeyMap; private readonly Dictionary _objectTypes; private IQuery? _queryRootEntity; public EntityService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IIdKeyMap idKeyMap, IEntityRepository entityRepository) : base(provider, loggerFactory, eventMessagesFactory) { _idKeyMap = idKeyMap; _entityRepository = entityRepository; _objectTypes = new Dictionary { { typeof(IDataType).FullName!, UmbracoObjectTypes.DataType }, { typeof(IContent).FullName!, UmbracoObjectTypes.Document }, { typeof(IContentType).FullName!, UmbracoObjectTypes.DocumentType }, { typeof(IMedia).FullName!, UmbracoObjectTypes.Media }, { typeof(IMediaType).FullName!, UmbracoObjectTypes.MediaType }, { typeof(IMember).FullName!, UmbracoObjectTypes.Member }, { typeof(IMemberType).FullName!, UmbracoObjectTypes.MemberType }, { typeof(IMemberGroup).FullName!, UmbracoObjectTypes.MemberGroup }, { typeof(ITemplate).FullName!, UmbracoObjectTypes.Template }, }; } #region Static Queries // lazy-constructed because when the ctor runs, the query factory may not be ready private IQuery QueryRootEntity => _queryRootEntity ??= Query() .Where(x => x.ParentId == -1); #endregion /// public IEntitySlim? Get(int id) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Get(id); } } /// public IEntitySlim? Get(Guid key) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Get(key); } } /// public virtual IEntitySlim? Get(int id, UmbracoObjectTypes objectType) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Get(id, objectType.GetGuid()); } } /// public IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Get(key, objectType.GetGuid()); } } /// public virtual IEntitySlim? Get(int id) where T : IUmbracoEntity { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Get(id); } } /// public virtual IEntitySlim? Get(Guid key) where T : IUmbracoEntity { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Get(key); } } /// public bool Exists(int id) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Exists(id); } } /// public bool Exists(Guid key) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Exists(key); } } public bool Exists(IEnumerable keys) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Exists(keys); } } /// public bool Exists(Guid key, UmbracoObjectTypes objectType) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Exists(key, objectType.GetGuid()); } } /// public bool Exists(int id, UmbracoObjectTypes objectType) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.Exists(id, objectType.GetGuid()); } } /// public virtual IEnumerable GetAll() where T : IUmbracoEntity => GetAll(Array.Empty()); /// public virtual IEnumerable GetAll(params int[] ids) where T : IUmbracoEntity { Type entityType = typeof(T); UmbracoObjectTypes objectType = GetObjectType(entityType); Guid objectTypeId = objectType.GetGuid(); using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAll(objectTypeId, ids); } } /// public virtual IEnumerable GetAll(UmbracoObjectTypes objectType) => GetAll(objectType, Array.Empty()); /// public virtual IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids) { Type? entityType = objectType.GetClrType(); if (entityType == null) { throw new NotSupportedException($"Type \"{objectType}\" is not supported here."); } GetObjectType(entityType); using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAll(objectType.GetGuid(), ids); } } /// public virtual IEnumerable GetAll(Guid objectType) => GetAll(objectType, Array.Empty()); /// public virtual IEnumerable GetAll(Guid objectType, params int[] ids) { Type? entityType = ObjectTypes.GetClrType(objectType); GetObjectType(entityType); using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAll(objectType, ids); } } /// public virtual IEnumerable GetAll(params Guid[] keys) where T : IUmbracoEntity { Type entityType = typeof(T); UmbracoObjectTypes objectType = GetObjectType(entityType); Guid objectTypeId = objectType.GetGuid(); using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAll(objectTypeId, keys); } } /// public IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys) { Type? entityType = objectType.GetClrType(); GetObjectType(entityType); using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAll(objectType.GetGuid(), keys); } } /// public virtual IEnumerable GetAll(Guid objectType, params Guid[] keys) { Type? entityType = ObjectTypes.GetClrType(objectType); GetObjectType(entityType); using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAll(objectType, keys); } } /// public virtual IEnumerable GetRootEntities(UmbracoObjectTypes objectType) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetByQuery(QueryRootEntity, objectType.GetGuid()); } } /// public virtual IEntitySlim? GetParent(int id) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IEntitySlim? entity = _entityRepository.Get(id); if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) { return null; } return _entityRepository.Get(entity.ParentId); } } /// public virtual IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IEntitySlim? entity = _entityRepository.Get(id); if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) { return null; } return _entityRepository.Get(entity.ParentId, objectType.GetGuid()); } } /// public virtual IEnumerable GetChildren(int parentId) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery query = Query().Where(x => x.ParentId == parentId); return _entityRepository.GetByQuery(query); } } /// public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes objectType) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery query = Query().Where(x => x.ParentId == parentId); return _entityRepository.GetByQuery(query, objectType.GetGuid()); } } public IEnumerable GetChildren(Guid? key, UmbracoObjectTypes objectType) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); if (ResolveKey(key, objectType, out int parentId) is false) { return Enumerable.Empty(); } IEnumerable children = GetChildren(parentId, objectType); return children; } /// public virtual IEnumerable GetDescendants(int id) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IEntitySlim? entity = _entityRepository.Get(id); var pathMatch = entity?.Path + ","; IQuery query = Query() .Where(x => x.Path.StartsWith(pathMatch) && x.Id != id); return _entityRepository.GetByQuery(query); } } /// public virtual IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IEntitySlim? entity = _entityRepository.Get(id); if (entity is null) { return Enumerable.Empty(); } IQuery query = Query() .Where(x => x.Path.StartsWith(entity.Path) && x.Id != id); return _entityRepository.GetByQuery(query, objectType.GetGuid()); } } /// public IEnumerable GetPagedChildren( int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null) => GetPagedChildren(id, objectType, pageIndex, pageSize, false, filter, ordering, out totalRecords); public IEnumerable GetPagedChildren( Guid? key, UmbracoObjectTypes objectType, int skip, int take, out long totalRecords, IQuery? filter = null, Ordering? ordering = null) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); if (ResolveKey(key, objectType, out int parentId) is false) { totalRecords = 0; return Enumerable.Empty(); } PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); IEnumerable children = GetPagedChildren( parentId, objectType, pageNumber, pageSize, out totalRecords, filter, ordering); scope.Complete(); return children; } /// public IEnumerable GetPagedTrashedChildren( int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null) => GetPagedChildren(id, objectType, pageIndex, pageSize, true, filter, ordering, out totalRecords); IEnumerable GetPagedTrashedChildren( Guid? key, UmbracoObjectTypes objectType, int skip, int take, out long totalRecords, IQuery? filter = null, Ordering? ordering = null) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); if (ResolveKey(key, objectType, out int parentId) is false) { totalRecords = 0; return Enumerable.Empty(); } PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); IEnumerable children = GetPagedChildren( parentId, objectType, pageNumber, pageSize, true, filter, ordering, out totalRecords); scope.Complete(); return children; } /// public IEnumerable GetPagedDescendants( int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { Guid objectTypeGuid = objectType.GetGuid(); IQuery query = Query(); if (id != Constants.System.Root) { // lookup the path so we can use it in the prefix query below TreeEntityPath[] paths = _entityRepository.GetAllPaths(objectTypeGuid, id).ToArray(); if (paths.Length == 0) { totalRecords = 0; return Enumerable.Empty(); } var path = paths[0].Path; query.Where(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar)); } return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering); } } /// public IEnumerable GetPagedDescendants( IEnumerable ids, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null) { totalRecords = 0; var idsA = ids.ToArray(); if (idsA.Length == 0) { return Enumerable.Empty(); } using (ScopeProvider.CreateCoreScope(autoComplete: true)) { Guid objectTypeGuid = objectType.GetGuid(); IQuery query = Query(); if (idsA.All(x => x != Constants.System.Root)) { TreeEntityPath[] paths = _entityRepository.GetAllPaths(objectTypeGuid, idsA).ToArray(); if (paths.Length == 0) { totalRecords = 0; return Enumerable.Empty(); } var clauses = new List>>(); foreach (var id in idsA) { // if the id is root then don't add any clauses if (id == Constants.System.Root) { continue; } TreeEntityPath? entityPath = paths.FirstOrDefault(x => x.Id == id); if (entityPath == null) { continue; } var path = entityPath.Path; var qid = id; clauses.Add(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar) || x.Path.SqlEndsWith("," + qid, TextColumnType.NVarchar)); } query.WhereAny(clauses); } return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering); } } /// public IEnumerable GetPagedDescendants( UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null, bool includeTrashed = true) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery query = Query(); if (includeTrashed == false) { query.Where(x => x.Trashed == false); } return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering); } } /// public virtual UmbracoObjectTypes GetObjectType(int id) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetObjectType(id); } } /// public virtual UmbracoObjectTypes GetObjectType(Guid key) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetObjectType(key); } } /// public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity) => entity is IEntitySlim light ? ObjectTypes.GetUmbracoObjectType(light.NodeObjectType) : GetObjectType(entity.Id); /// public virtual Type? GetEntityType(int id) { UmbracoObjectTypes objectType = GetObjectType(id); return objectType.GetClrType(); } /// public Attempt GetId(Guid key, UmbracoObjectTypes objectType) => _idKeyMap.GetIdForKey(key, objectType); /// public Attempt GetId(Udi udi) => _idKeyMap.GetIdForUdi(udi); /// public Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType) => _idKeyMap.GetKeyForId(id, umbracoObjectType); /// public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids) { Type? entityType = objectType.GetClrType(); GetObjectType(entityType); using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAllPaths(objectType.GetGuid(), ids); } } /// public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys) { Type? entityType = objectType.GetClrType(); GetObjectType(entityType); using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAllPaths(objectType.GetGuid(), keys); } } /// public int ReserveId(Guid key) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.ReserveId(key); } } public int CountChildren( int id, UmbracoObjectTypes objectType, IQuery? filter = null) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == false); return _entityRepository.CountByQuery(query, objectType.GetGuid(), filter); } } public int CountChildren(Guid? key, UmbracoObjectTypes objectType, IQuery? filter = null) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); if (ResolveKey(key, objectType, out var parentId) is false) { return 0; } var count = CountChildren(parentId, objectType, filter); scope.Complete(); return count; } private bool ResolveKey(Guid? key, UmbracoObjectTypes objectType, out int id) { // We have to explicitly check for "root key" since this value is null, and GetId does not accept null. if (key == Constants.System.RootKey) { id = Constants.System.Root; return true; } Attempt parentIdAttempt = GetId(key!.Value, objectType); id = parentIdAttempt.Result; return parentIdAttempt.Success; } // gets the object type, throws if not supported private UmbracoObjectTypes GetObjectType(Type? type) { if (type?.FullName == null || !_objectTypes.TryGetValue(type.FullName, out UmbracoObjectTypes objType)) { throw new NotSupportedException($"Type \"{type?.FullName ?? ""}\" is not supported here."); } return objType; } private IEnumerable GetPagedChildren( int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, bool trashed, IQuery? filter, Ordering? ordering, out long totalRecords) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == trashed); if (pageSize == 0) { totalRecords = _entityRepository.CountByQuery(query, objectType.GetGuid(), filter); return Enumerable.Empty(); } return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering); } } }