using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; namespace Umbraco.Core.Services.Implement { public class RelationService : ScopeRepositoryService, IRelationService { private readonly IEntityService _entityService; private readonly IRelationRepository _relationRepository; private readonly IRelationTypeRepository _relationTypeRepository; private readonly IAuditRepository _auditRepository; public RelationService(IScopeProvider uowProvider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IEntityService entityService, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, IAuditRepository auditRepository) : base(uowProvider, logger, eventMessagesFactory) { _relationRepository = relationRepository; _relationTypeRepository = relationTypeRepository; _auditRepository = auditRepository; _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); } /// public IRelation GetById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _relationRepository.Get(id); } } /// public IRelationType GetRelationTypeById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _relationTypeRepository.Get(id); } } /// public IRelationType GetRelationTypeById(Guid id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _relationTypeRepository.Get(id); } } /// public IRelationType GetRelationTypeByAlias(string alias) => GetRelationType(alias); /// public IEnumerable GetAllRelations(params int[] ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _relationRepository.GetMany(ids); } } /// public IEnumerable GetAllRelationsByRelationType(IRelationType relationType) { return GetAllRelationsByRelationType(relationType.Id); } /// public IEnumerable GetAllRelationsByRelationType(int relationTypeId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.RelationTypeId == relationTypeId); return _relationRepository.Get(query); } } /// public IEnumerable GetAllRelationTypes(params int[] ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _relationTypeRepository.GetMany(ids); } } /// public IEnumerable GetByParentId(int id) => GetByParentId(id, null); /// public IEnumerable GetByParentId(int id, string relationTypeAlias) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { if (relationTypeAlias.IsNullOrWhiteSpace()) { var qry1 = Query().Where(x => x.ParentId == id); return _relationRepository.Get(qry1); } var relationType = GetRelationType(relationTypeAlias); if (relationType == null) return Enumerable.Empty(); var qry2 = Query().Where(x => x.ParentId == id && x.RelationTypeId == relationType.Id); return _relationRepository.Get(qry2); } } /// public IEnumerable GetByParent(IUmbracoEntity parent) => GetByParentId(parent.Id); /// public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) => GetByParentId(parent.Id, relationTypeAlias); /// public IEnumerable GetByChildId(int id) => GetByChildId(id, null); /// public IEnumerable GetByChildId(int id, string relationTypeAlias) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { if (relationTypeAlias.IsNullOrWhiteSpace()) { var qry1 = Query().Where(x => x.ChildId == id); return _relationRepository.Get(qry1); } var relationType = GetRelationType(relationTypeAlias); if (relationType == null) return Enumerable.Empty(); var qry2 = Query().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id); return _relationRepository.Get(qry2); } } /// public IEnumerable GetByChild(IUmbracoEntity child) => GetByChildId(child.Id); /// public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) => GetByChildId(child.Id, relationTypeAlias); /// public IEnumerable GetByParentOrChildId(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.ChildId == id || x.ParentId == id); return _relationRepository.Get(query); } } public IEnumerable GetByParentOrChildId(int id, string relationTypeAlias) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var relationType = GetRelationType(relationTypeAlias); if (relationType == null) return Enumerable.Empty(); var query = Query().Where(x => (x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id); return _relationRepository.Get(query); } } /// public IEnumerable GetByRelationTypeName(string relationTypeName) { List relationTypeIds; using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { //This is a silly query - but i guess it's needed in case someone has more than one relation with the same Name (not alias), odd. var query = Query().Where(x => x.Name == relationTypeName); var relationTypes = _relationTypeRepository.Get(query); relationTypeIds = relationTypes.Select(x => x.Id).ToList(); } return relationTypeIds.Count == 0 ? Enumerable.Empty() : GetRelationsByListOfTypeIds(relationTypeIds); } /// public IEnumerable GetByRelationTypeAlias(string relationTypeAlias) { var relationType = GetRelationType(relationTypeAlias); return relationType == null ? Enumerable.Empty() : GetRelationsByListOfTypeIds(new[] { relationType.Id }); } /// public IEnumerable GetByRelationTypeId(int relationTypeId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.RelationTypeId == relationTypeId); return _relationRepository.Get(query); } } /// public IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering ordering = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.RelationTypeId == relationTypeId); return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering); } } /// public IUmbracoEntity GetChildEntityFromRelation(IRelation relation) { var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); return _entityService.Get(relation.ChildId, objectType); } /// public IUmbracoEntity GetParentEntityFromRelation(IRelation relation) { var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); return _entityService.Get(relation.ParentId, objectType); } /// public Tuple GetEntitiesFromRelation(IRelation relation) { var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); var child = _entityService.Get(relation.ChildId, childObjectType); var parent = _entityService.Get(relation.ParentId, parentObjectType); return new Tuple(parent, child); } /// public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations) { // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll // method to lookup batches of entities for each parent object type foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ChildObjectType))) { var objectType = groupedRelations.Key; var ids = groupedRelations.Select(x => x.ChildId).ToArray(); foreach (var e in _entityService.GetAll(objectType, ids)) yield return e; } } /// public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations) { // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll // method to lookup batches of entities for each parent object type foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ParentObjectType))) { var objectType = groupedRelations.Key; var ids = groupedRelations.Select(x => x.ParentId).ToArray(); foreach (var e in _entityService.GetAll(objectType, ids)) yield return e; } } /// public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); } } /// public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); } } /// public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) { //TODO: Argh! N+1 foreach (var relation in relations) { var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); var child = _entityService.Get(relation.ChildId, childObjectType); var parent = _entityService.Get(relation.ParentId, parentObjectType); yield return new Tuple(parent, child); } } /// public IRelation Relate(int parentId, int childId, IRelationType relationType) { // Ensure that the RelationType has an identity before using it to relate two entities if (relationType.HasIdentity == false) Save(relationType); //TODO: We don't check if this exists first, it will throw some sort of data integrity exception if it already exists, is that ok? var relation = new Relation(parentId, childId, relationType); using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(relation); if (scope.Events.DispatchCancelable(SavingRelation, this, saveEventArgs)) { scope.Complete(); return relation; // TODO: returning sth that does not exist here?! } _relationRepository.Save(relation); saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedRelation, this, saveEventArgs); scope.Complete(); return relation; } } /// public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) { return Relate(parent.Id, child.Id, relationType); } /// public IRelation Relate(int parentId, int childId, string relationTypeAlias) { var relationType = GetRelationTypeByAlias(relationTypeAlias); if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); return Relate(parentId, childId, relationType); } /// public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) { var relationType = GetRelationTypeByAlias(relationTypeAlias); if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); return Relate(parent.Id, child.Id, relationType); } /// public bool HasRelations(IRelationType relationType) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.RelationTypeId == relationType.Id); return _relationRepository.Get(query).Any(); } } /// public bool IsRelated(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.ParentId == id || x.ChildId == id); return _relationRepository.Get(query).Any(); } } /// public bool AreRelated(int parentId, int childId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId); return _relationRepository.Get(query).Any(); } } /// public bool AreRelated(int parentId, int childId, string relationTypeAlias) { var relType = GetRelationTypeByAlias(relationTypeAlias); if (relType == null) return false; return AreRelated(parentId, childId, relType); } /// public bool AreRelated(int parentId, int childId, IRelationType relationType) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId && x.RelationTypeId == relationType.Id); return _relationRepository.Get(query).Any(); } } /// public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child) { return AreRelated(parent.Id, child.Id); } /// public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) { return AreRelated(parent.Id, child.Id, relationTypeAlias); } /// public void Save(IRelation relation) { using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(relation); if (scope.Events.DispatchCancelable(SavingRelation, this, saveEventArgs)) { scope.Complete(); return; } _relationRepository.Save(relation); scope.Complete(); saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedRelation, this, saveEventArgs); } } public void Save(IEnumerable relations) { using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(relations); if (scope.Events.DispatchCancelable(SavingRelation, this, saveEventArgs)) { scope.Complete(); return; } _relationRepository.Save(relations); scope.Complete(); saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedRelation, this, saveEventArgs); } } /// public void Save(IRelationType relationType) { using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(relationType); if (scope.Events.DispatchCancelable(SavingRelationType, this, saveEventArgs)) { scope.Complete(); return; } _relationTypeRepository.Save(relationType); Audit(AuditType.Save, Constants.Security.SuperUserId, relationType.Id, $"Saved relation type: {relationType.Name}"); scope.Complete(); saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedRelationType, this, saveEventArgs); } } /// public void Delete(IRelation relation) { using (var scope = ScopeProvider.CreateScope()) { var deleteEventArgs = new DeleteEventArgs(relation); if (scope.Events.DispatchCancelable(DeletingRelation, this, deleteEventArgs)) { scope.Complete(); return; } _relationRepository.Delete(relation); scope.Complete(); deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedRelation, this, deleteEventArgs); } } /// public void Delete(IRelationType relationType) { using (var scope = ScopeProvider.CreateScope()) { var deleteEventArgs = new DeleteEventArgs(relationType); if (scope.Events.DispatchCancelable(DeletingRelationType, this, deleteEventArgs)) { scope.Complete(); return; } _relationTypeRepository.Delete(relationType); scope.Complete(); deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedRelationType, this, deleteEventArgs); } } /// public void DeleteRelationsOfType(IRelationType relationType) { var relations = new List(); using (var scope = ScopeProvider.CreateScope()) { var query = Query().Where(x => x.RelationTypeId == relationType.Id); relations.AddRange(_relationRepository.Get(query).ToList()); //TODO: N+1, we should be able to do this in a single call foreach (var relation in relations) _relationRepository.Delete(relation); scope.Complete(); scope.Events.Dispatch(DeletedRelation, this, new DeleteEventArgs(relations, false)); } } #region Private Methods private IRelationType GetRelationType(string relationTypeAlias) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.Alias == relationTypeAlias); return _relationTypeRepository.Get(query).FirstOrDefault(); } } private IEnumerable GetRelationsByListOfTypeIds(IEnumerable relationTypeIds) { var relations = new List(); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { foreach (var relationTypeId in relationTypeIds) { var id = relationTypeId; var query = Query().Where(x => x.RelationTypeId == id); relations.AddRange(_relationRepository.Get(query)); } } return relations; } private void Audit(AuditType type, int userId, int objectId, string message = null) { _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.RelationType), message)); } #endregion #region Events Handlers /// /// Occurs before Deleting a Relation /// public static event TypedEventHandler> DeletingRelation; /// /// Occurs after a Relation is Deleted /// public static event TypedEventHandler> DeletedRelation; /// /// Occurs before Saving a Relation /// public static event TypedEventHandler> SavingRelation; /// /// Occurs after a Relation is Saved /// public static event TypedEventHandler> SavedRelation; /// /// Occurs before Deleting a RelationType /// public static event TypedEventHandler> DeletingRelationType; /// /// Occurs after a RelationType is Deleted /// public static event TypedEventHandler> DeletedRelationType; /// /// Occurs before Saving a RelationType /// public static event TypedEventHandler> SavingRelationType; /// /// Occurs after a RelationType is Saved /// public static event TypedEventHandler> SavedRelationType; #endregion } }