From fff3d2648f71b375a6bec6624b140cce08a3e8e2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Nov 2019 15:05:51 +1100 Subject: [PATCH 1/8] Gets entity repository to be able to return a mix of object types --- src/Umbraco.Core/Models/ContentBase.cs | 3 + src/Umbraco.Core/Models/ContentTypeBase.cs | 3 + src/Umbraco.Core/Models/DataType.cs | 3 + .../Models/Entities/ITreeEntity.cs | 4 +- .../Models/Entities/IUmbracoEntity.cs | 7 +- src/Umbraco.Core/Models/EntityContainer.cs | 2 + src/Umbraco.Core/Models/SimpleContentType.cs | 3 + .../Factories/ContentBaseFactory.cs | 2 +- .../Factories/ContentTypeFactory.cs | 1 + .../Persistence/Factories/DataTypeFactory.cs | 1 + .../Mappers/UmbracoEntityMapper.cs | 1 + .../Repositories/IEntityRepository.cs | 43 ++++- .../Implement/EntityRepository.cs | 157 +++++++++++------- .../Repositories/EntityRepositoryTest.cs | 95 +++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + src/Umbraco.Web/Editors/EntityController.cs | 3 + .../Models/Mapping/EntityMapDefinition.cs | 8 +- .../Models/Mapping/UserMapDefinition.cs | 4 +- .../Trees/ContentBlueprintTreeController.cs | 4 +- 19 files changed, 273 insertions(+), 72 deletions(-) create mode 100644 src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index fbb68194b7..2ea0fd855b 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -108,6 +108,9 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public int VersionId { get; set; } + [DataMember] + public Guid NodeObjectType { get; internal set; } + /// /// Integer Id of the default ContentType /// diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 04bcb7424a..1f3f1a5c13 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -113,6 +113,9 @@ namespace Umbraco.Core.Models OnPropertyChanged(nameof(PropertyTypes)); } + [DataMember] + public Guid NodeObjectType { get; internal set; } + /// /// The Alias of the ContentType /// diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index c237f6381c..cba53bbd10 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -65,6 +65,9 @@ namespace Umbraco.Core.Models [DataMember] public string EditorAlias => _editor.Alias; + [DataMember] + public Guid NodeObjectType { get; internal set; } + /// [DataMember] public ValueStorageType DatabaseType diff --git a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs index afa3399202..ab63e1e1d8 100644 --- a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs +++ b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs @@ -24,7 +24,7 @@ /// Sets the parent entity. /// /// Use this method to set the parent entity when the parent entity is known, but has not - /// been persistent and does not yet have an identity. The parent identifier will we retrieved + /// been persistent and does not yet have an identity. The parent identifier will be retrieved /// from the parent entity when needed. If the parent entity still does not have an entity by that /// time, an exception will be thrown by getter. void SetParent(ITreeEntity parent); @@ -53,4 +53,4 @@ /// bool Trashed { get; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs index e5f628b098..13087b5e95 100644 --- a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Models.Entities { @@ -11,5 +12,7 @@ namespace Umbraco.Core.Models.Entities /// An IUmbracoEntity can participate in notifications. /// public interface IUmbracoEntity : ITreeEntity, IRememberBeingDirty - { } + { + Guid NodeObjectType { get; } + } } diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index 70f6cbd878..71a477a9d6 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -62,6 +62,8 @@ namespace Umbraco.Core.Models /// public Guid ContainerObjectType => ObjectTypeMap[_containedObjectType]; + Guid IUmbracoEntity.NodeObjectType => ContainerObjectType; + /// /// Gets the container object type corresponding to a contained object type. /// diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs index 5c81017ec8..b4e7688eea 100644 --- a/src/Umbraco.Core/Models/SimpleContentType.cs +++ b/src/Umbraco.Core/Models/SimpleContentType.cs @@ -44,11 +44,14 @@ namespace Umbraco.Core.Models Name = contentType.Name; AllowedAsRoot = contentType.AllowedAsRoot; IsElement = contentType.IsElement; + NodeObjectType = contentType.NodeObjectType; } /// public string Alias { get; } + public Guid NodeObjectType { get; } + public int Id { get; } /// diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index 434e0393cd..25cc6bc358 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.Persistence.Factories content.VersionId = contentVersionDto.Id; content.Name = contentVersionDto.Text; - + content.NodeObjectType = nodeDto.NodeObjectType ?? Guid.Empty; content.Path = nodeDto.Path; content.Level = nodeDto.Level; content.ParentId = nodeDto.ParentId; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index 54cfee0ffa..46489aa69e 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -107,6 +107,7 @@ namespace Umbraco.Core.Persistence.Factories { entity.Id = dto.NodeDto.NodeId; entity.Key = dto.NodeDto.UniqueId; + entity.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; entity.Alias = dto.Alias; entity.Name = dto.NodeDto.Text; entity.Icon = dto.Icon; diff --git a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs index f189d38d05..5d92234ca8 100644 --- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs @@ -28,6 +28,7 @@ namespace Umbraco.Core.Persistence.Factories dataType.DisableChangeTracking(); dataType.CreateDate = dto.NodeDto.CreateDate; + dataType.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; dataType.DatabaseType = dto.DbType.EnumParse(true); dataType.Id = dto.NodeId; dataType.Key = dto.NodeDto.UniqueId; diff --git a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs index 16e2e8bcd5..96f664cc25 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence.Mappers DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(IUmbracoEntity.NodeObjectType), nameof(NodeDto.NodeObjectType)); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index 69f6ef4c5f..a6a1691347 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -15,10 +15,22 @@ namespace Umbraco.Core.Persistence.Repositories IEntitySlim Get(int id, Guid objectTypeId); IEntitySlim Get(Guid key, Guid objectTypeId); - IEnumerable GetAll(Guid objectType, params int[] ids); + IEnumerable GetAll(Guid objectType, params int[] ids); IEnumerable GetAll(Guid objectType, params Guid[] keys); + /// + /// Gets entities for a query + /// + /// + /// IEnumerable GetByQuery(IQuery query); + + /// + /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized + /// + /// + /// + /// IEnumerable GetByQuery(IQuery query, Guid objectType); UmbracoObjectTypes GetObjectType(int id); @@ -30,7 +42,36 @@ namespace Umbraco.Core.Persistence.Repositories bool Exists(int id); bool Exists(Guid key); + /// + /// Gets paged entities for a query and a subset of object types + /// + /// + /// + /// + /// + /// + /// + /// + /// A collection of mixed entity types which would be of type , , , + /// + /// + IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering); + + /// + /// Gets paged entities for a query and a specific object type + /// + /// + /// + /// + /// + /// + /// + /// + /// IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); + + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 161db543ba..2b68c8fe19 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -36,26 +36,70 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Repository - // get a page of entities + //public IEnumerable GetPagedResults(IDictionary> typesAndIds, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) + //{ + // var isContent = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); + // var isMedia = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Media); + // var isMember = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Member); + + // var sql = GetBase(isContent, isMedia, isMember, null, false) + // .WhereIn(x => x.NodeObjectType, typesAndIds.Keys); + + // ordering = ordering ?? Ordering.ByDefault(); + + // sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); + + // if (!ordering.IsEmpty) + // { + // // apply ordering + // ApplyOrdering(ref sql, ordering); + // } + + // // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently + // //no matter what we always must have node id ordered at the end + // sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); + + // // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names + // var pageIndexToFetch = pageIndex + 1; + // IEnumerable dtos; + // var page = Database.Page(pageIndexToFetch, pageSize, sql); + // dtos = page.Items; + // totalRecords = page.TotalItems; + + // var entities = dtos.Select(BuildEntity).ToArray(); + + // BuildVariants(entities.OfType()); + + // return entities; + //} + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { - var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectType == Constants.ObjectTypes.Media; - var isMember = objectType == Constants.ObjectTypes.Member; + query = query.Where(x => x.NodeObjectType == objectType); + return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering); + } + + // get a page of entities + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering) + { + var isContent = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); + var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); + var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); var sql = GetBaseWhere(isContent, isMedia, isMember, false, x => { if (filter == null) return; foreach (var filterClause in filter.GetWhereClauses()) x.Where(filterClause.Item1, filterClause.Item2); - }, objectType); + }, objectTypes); ordering = ordering ?? Ordering.ByDefault(); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); + sql = AddGroupBy(true, true, true, sql, ordering.IsEmpty); if (!ordering.IsEmpty) { @@ -70,35 +114,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names var pageIndexToFetch = pageIndex + 1; IEnumerable dtos; - if(isContent) - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } - else if (isMedia) - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } - else if (isMember) - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } - else - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; - var entities = dtos.Select(x => BuildEntity(isContent, isMedia, isMember, x)).ToArray(); + var entities = dtos.Select(BuildEntity).ToArray(); - if (isContent) - BuildVariants(entities.Cast()); + BuildVariants(entities.OfType()); return entities; } @@ -107,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, key); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(false, false, false, dto); + return dto == null ? null : BuildEntity(dto); } @@ -116,7 +138,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } @@ -127,7 +149,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dto == null) return null; - var entity = BuildEntity(false, isMedia, isMember, dto); + var entity = BuildEntity(dto); return entity; } @@ -146,7 +168,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, id); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(false, false, false, dto); + return dto == null ? null : BuildEntity(dto); } public IEntitySlim Get(int id, Guid objectTypeId) @@ -178,7 +200,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? Enumerable.Empty() @@ -189,7 +211,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ? (IEnumerable)Database.Fetch(sql) : Database.Fetch(sql); - var entities = dtos.Select(x => BuildEntity(false, isMedia, isMember, x)).ToArray(); + var entities = dtos.Select(BuildEntity).ToArray(); return entities; } @@ -233,7 +255,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sql = translator.Translate(); sql = AddGroupBy(false, false, false, sql, true); var dtos = Database.Fetch(sql); - return dtos.Select(x => BuildEntity(false, false, false, x)).ToList(); + return dtos.Select(BuildEntity).ToList(); } public IEnumerable GetByQuery(IQuery query, Guid objectType) @@ -242,7 +264,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var isMedia = objectType == Constants.ObjectTypes.Media; var isMember = objectType == Constants.ObjectTypes.Member; - var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, objectType); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] { objectType }); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); @@ -356,14 +378,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the full sql for a given object type, with a given filter protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action> filter) { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, objectType); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType }); return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action> filter, bool isCount = false) - { + { var sql = Sql(); if (isCount) @@ -401,15 +423,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent || isMedia || isMember) { sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId); + .LeftJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); } if (isContent) { sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId); + .LeftJoin().On((left, right) => left.NodeId == right.NodeId); } if (isMedia) @@ -433,10 +455,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM [+ filter] + WHERE sql // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid objectType) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid[] objectTypes) { return GetBase(isContent, isMedia, isMember, filter, isCount) - .Where(x => x.NodeObjectType == objectType); + .WhereIn(x => x.NodeObjectType, objectTypes); } // gets the base SELECT + FROM + WHERE sql @@ -510,8 +532,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (sql == null) throw new ArgumentNullException(nameof(sql)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - // TODO: although this works for name, it probably doesn't work for others without an alias of some sort - var orderBy = ordering.OrderBy; + // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort + // As more things are attempted to be sorted we'll prob have to add more expressions here + var orderBy = ordering.OrderBy.ToUpperInvariant() switch + { + "PATH" => SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"), + _ => ordering.OrderBy + }; if (ordering.Direction == Direction.Ascending) sql.OrderBy(orderBy); @@ -524,9 +551,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Classes /// - /// The DTO used to fetch results for a content item with its variation info + /// The DTO used to fetch results for a generic content item which could be either a document, media or a member /// - private class ContentEntityDto : BaseDto + private class GenericContentEntityDto : DocumentEntityDto + { + public string MediaPath { get; set; } + } + + /// + /// The DTO used to fetch results for a document item with its variation info + /// + private class DocumentEntityDto : BaseDto { public ContentVariation Variations { get; set; } @@ -534,11 +569,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public bool Edited { get; set; } } + /// + /// The DTO used to fetch results for a media item with its media path info + /// private class MediaEntityDto : BaseDto { public string MediaPath { get; set; } } + /// + /// The DTO used to fetch results for a member item + /// private class MemberEntityDto : BaseDto { } @@ -589,13 +630,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Factory - private EntitySlim BuildEntity(bool isContent, bool isMedia, bool isMember, BaseDto dto) + private EntitySlim BuildEntity(BaseDto dto) { - if (isContent) + if (dto.NodeObjectType == Constants.ObjectTypes.Document) return BuildDocumentEntity(dto); - if (isMedia) + if (dto.NodeObjectType == Constants.ObjectTypes.Media) return BuildMediaEntity(dto); - if (isMember) + if (dto.NodeObjectType == Constants.ObjectTypes.Member) return BuildMemberEntity(dto); // EntitySlim does not track changes @@ -650,7 +691,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = new DocumentEntitySlim(); BuildContentEntity(entity, dto); - if (dto is ContentEntityDto contentDto) + if (dto is DocumentEntityDto contentDto) { // fill in the invariant info entity.Edited = contentDto.Edited; diff --git a/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs new file mode 100644 index 0000000000..a4df5bcf78 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Scoping; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + [UmbracoTest(Mapper = true, Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class EntityRepositoryTest : TestWithDatabaseBase + { + + private EntityRepository CreateRepository(IScopeAccessor scopeAccessor) + { + var entityRepository = new EntityRepository(scopeAccessor); + return entityRepository; + } + + [Test] + public void Get_Paged_Mixed_Entities_By_Ids() + { + //Create content + + var createdContent = new List(); + var contentType = MockedContentTypes.CreateBasicContentType("blah"); + ServiceContext.ContentTypeService.Save(contentType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedContent.CreateBasicContent(contentType); + ServiceContext.ContentService.Save(c1); + createdContent.Add(c1); + } + + //Create media + + var createdMedia = new List(); + var imageType = MockedContentTypes.CreateImageMediaType("myImage"); + ServiceContext.MediaTypeService.Save(imageType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedMedia.CreateMediaImage(imageType, -1); + ServiceContext.MediaService.Save(c1); + createdMedia.Add(c1); + } + + // Create members + var memberType = MockedContentTypes.CreateSimpleMemberType("simple"); + ServiceContext.MemberTypeService.Save(memberType); + var createdMembers = MockedMember.CreateSimpleMember(memberType, 10).ToList(); + ServiceContext.MemberService.Save(createdMembers); + + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repo = CreateRepository((IScopeAccessor)provider); + + var ids = createdContent.Select(x => x.Id).Concat(createdMedia.Select(x => x.Id)).Concat(createdMembers.Select(x => x.Id)); + + var objectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member }; + + var query = SqlContext.Query() + .WhereIn(e => e.Id, ids); + + var entities = repo.GetPagedResultsByQuery(query, objectTypes, 0, 20, out var totalRecords, null, null).ToList(); + + Assert.AreEqual(20, entities.Count); + Assert.AreEqual(30, totalRecords); + + //add the next page + entities.AddRange(repo.GetPagedResultsByQuery(query, objectTypes, 1, 20, out totalRecords, null, null)); + + Assert.AreEqual(30, entities.Count); + Assert.AreEqual(30, totalRecords); + + var contentEntities = entities.OfType().ToList(); + var mediaEntities = entities.OfType().ToList(); + var memberEntities = entities.OfType().ToList(); + + Assert.AreEqual(10, contentEntities.Count); + Assert.AreEqual(10, mediaEntities.Count); + Assert.AreEqual(10, memberEntities.Count); + } + + } + + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 4b035f631e..c87e6501f9 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -140,6 +140,7 @@ + diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 0513017b70..11b1260e71 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -662,6 +662,9 @@ namespace Umbraco.Web.Editors if (pageSize <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); + // re-normalize since NULL can be passed in + filter = filter ?? string.Empty; + var objectType = ConvertToObjectType(type); if (objectType.HasValue) { diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 2e44f1327b..afd1a6b61c 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -226,11 +226,11 @@ namespace Umbraco.Web.Models.Mapping { switch (entity) { - case ContentEntitySlim contentEntity: - // NOTE: this case covers both content and media entities - return contentEntity.ContentTypeIcon; - case MemberEntitySlim memberEntity: + case IMemberEntitySlim memberEntity: return memberEntity.ContentTypeIcon.IfNullOrWhiteSpace(Constants.Icons.Member); + case IContentEntitySlim contentEntity: + // NOTE: this case covers both content and media entities + return contentEntity.ContentTypeIcon; } return null; diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index 88960fb189..aa158799cb 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -380,8 +380,8 @@ namespace Umbraco.Web.Models.Mapping .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); } - private static string MapContentTypeIcon(EntitySlim entity) - => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + private static string MapContentTypeIcon(IEntitySlim entity) + => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, MapperContext context) { diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index ac75fd831d..fd05f7cfbd 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { //get all blueprint content types - var contentTypeAliases = entities.Select(x => ((ContentEntitySlim) x).ContentTypeAlias).Distinct(); + var contentTypeAliases = entities.Select(x => ((IContentEntitySlim) x).ContentTypeAlias).Distinct(); //get the ids var contentTypeIds = Services.ContentTypeService.GetAllContentTypeIds(contentTypeAliases.ToArray()).ToArray(); @@ -75,7 +75,7 @@ namespace Umbraco.Web.Trees var ct = Services.ContentTypeService.Get(intId.Result); if (ct == null) return nodes; - var blueprintsForDocType = entities.Where(x => ct.Alias == ((ContentEntitySlim) x).ContentTypeAlias); + var blueprintsForDocType = entities.Where(x => ct.Alias == ((IContentEntitySlim) x).ContentTypeAlias); nodes.AddRange(blueprintsForDocType .Select(entity => { From f7e8a53922f074581b2106e108b3d10a20a0a30d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Nov 2019 15:53:50 +1100 Subject: [PATCH 2/8] Revert "Gets entity repository to be able to return a mix of object types" This reverts commit fff3d2648f71b375a6bec6624b140cce08a3e8e2. --- src/Umbraco.Core/Models/ContentBase.cs | 3 - src/Umbraco.Core/Models/ContentTypeBase.cs | 3 - src/Umbraco.Core/Models/DataType.cs | 3 - .../Models/Entities/ITreeEntity.cs | 4 +- .../Models/Entities/IUmbracoEntity.cs | 7 +- src/Umbraco.Core/Models/EntityContainer.cs | 2 - src/Umbraco.Core/Models/SimpleContentType.cs | 3 - .../Factories/ContentBaseFactory.cs | 2 +- .../Factories/ContentTypeFactory.cs | 1 - .../Persistence/Factories/DataTypeFactory.cs | 1 - .../Mappers/UmbracoEntityMapper.cs | 1 - .../Repositories/IEntityRepository.cs | 43 +---- .../Implement/EntityRepository.cs | 157 +++++++----------- .../Repositories/EntityRepositoryTest.cs | 95 ----------- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - src/Umbraco.Web/Editors/EntityController.cs | 3 - .../Models/Mapping/EntityMapDefinition.cs | 8 +- .../Models/Mapping/UserMapDefinition.cs | 4 +- .../Trees/ContentBlueprintTreeController.cs | 4 +- 19 files changed, 72 insertions(+), 273 deletions(-) delete mode 100644 src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 2ea0fd855b..fbb68194b7 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -108,9 +108,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public int VersionId { get; set; } - [DataMember] - public Guid NodeObjectType { get; internal set; } - /// /// Integer Id of the default ContentType /// diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 1f3f1a5c13..04bcb7424a 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -113,9 +113,6 @@ namespace Umbraco.Core.Models OnPropertyChanged(nameof(PropertyTypes)); } - [DataMember] - public Guid NodeObjectType { get; internal set; } - /// /// The Alias of the ContentType /// diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index cba53bbd10..c237f6381c 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -65,9 +65,6 @@ namespace Umbraco.Core.Models [DataMember] public string EditorAlias => _editor.Alias; - [DataMember] - public Guid NodeObjectType { get; internal set; } - /// [DataMember] public ValueStorageType DatabaseType diff --git a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs index ab63e1e1d8..afa3399202 100644 --- a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs +++ b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs @@ -24,7 +24,7 @@ /// Sets the parent entity. /// /// Use this method to set the parent entity when the parent entity is known, but has not - /// been persistent and does not yet have an identity. The parent identifier will be retrieved + /// been persistent and does not yet have an identity. The parent identifier will we retrieved /// from the parent entity when needed. If the parent entity still does not have an entity by that /// time, an exception will be thrown by getter. void SetParent(ITreeEntity parent); @@ -53,4 +53,4 @@ /// bool Trashed { get; } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs index 13087b5e95..e5f628b098 100644 --- a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Umbraco.Core.Models.Entities { @@ -12,7 +11,5 @@ namespace Umbraco.Core.Models.Entities /// An IUmbracoEntity can participate in notifications. /// public interface IUmbracoEntity : ITreeEntity, IRememberBeingDirty - { - Guid NodeObjectType { get; } - } + { } } diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index 71a477a9d6..70f6cbd878 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -62,8 +62,6 @@ namespace Umbraco.Core.Models /// public Guid ContainerObjectType => ObjectTypeMap[_containedObjectType]; - Guid IUmbracoEntity.NodeObjectType => ContainerObjectType; - /// /// Gets the container object type corresponding to a contained object type. /// diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs index b4e7688eea..5c81017ec8 100644 --- a/src/Umbraco.Core/Models/SimpleContentType.cs +++ b/src/Umbraco.Core/Models/SimpleContentType.cs @@ -44,14 +44,11 @@ namespace Umbraco.Core.Models Name = contentType.Name; AllowedAsRoot = contentType.AllowedAsRoot; IsElement = contentType.IsElement; - NodeObjectType = contentType.NodeObjectType; } /// public string Alias { get; } - public Guid NodeObjectType { get; } - public int Id { get; } /// diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index 25cc6bc358..434e0393cd 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.Persistence.Factories content.VersionId = contentVersionDto.Id; content.Name = contentVersionDto.Text; - content.NodeObjectType = nodeDto.NodeObjectType ?? Guid.Empty; + content.Path = nodeDto.Path; content.Level = nodeDto.Level; content.ParentId = nodeDto.ParentId; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index 46489aa69e..54cfee0ffa 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -107,7 +107,6 @@ namespace Umbraco.Core.Persistence.Factories { entity.Id = dto.NodeDto.NodeId; entity.Key = dto.NodeDto.UniqueId; - entity.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; entity.Alias = dto.Alias; entity.Name = dto.NodeDto.Text; entity.Icon = dto.Icon; diff --git a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs index 5d92234ca8..f189d38d05 100644 --- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs @@ -28,7 +28,6 @@ namespace Umbraco.Core.Persistence.Factories dataType.DisableChangeTracking(); dataType.CreateDate = dto.NodeDto.CreateDate; - dataType.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; dataType.DatabaseType = dto.DbType.EnumParse(true); dataType.Id = dto.NodeId; dataType.Key = dto.NodeDto.UniqueId; diff --git a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs index 96f664cc25..16e2e8bcd5 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs @@ -24,7 +24,6 @@ namespace Umbraco.Core.Persistence.Mappers DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(IUmbracoEntity.NodeObjectType), nameof(NodeDto.NodeObjectType)); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index a6a1691347..69f6ef4c5f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -15,22 +15,10 @@ namespace Umbraco.Core.Persistence.Repositories IEntitySlim Get(int id, Guid objectTypeId); IEntitySlim Get(Guid key, Guid objectTypeId); - IEnumerable GetAll(Guid objectType, params int[] ids); + IEnumerable GetAll(Guid objectType, params int[] ids); IEnumerable GetAll(Guid objectType, params Guid[] keys); - /// - /// Gets entities for a query - /// - /// - /// IEnumerable GetByQuery(IQuery query); - - /// - /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized - /// - /// - /// - /// IEnumerable GetByQuery(IQuery query, Guid objectType); UmbracoObjectTypes GetObjectType(int id); @@ -42,36 +30,7 @@ namespace Umbraco.Core.Persistence.Repositories bool Exists(int id); bool Exists(Guid key); - /// - /// Gets paged entities for a query and a subset of object types - /// - /// - /// - /// - /// - /// - /// - /// - /// A collection of mixed entity types which would be of type , , , - /// - /// - IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering); - - /// - /// Gets paged entities for a query and a specific object type - /// - /// - /// - /// - /// - /// - /// - /// - /// IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); - - } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 2b68c8fe19..161db543ba 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -36,70 +36,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Repository - //public IEnumerable GetPagedResults(IDictionary> typesAndIds, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) - //{ - // var isContent = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); - // var isMedia = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Media); - // var isMember = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Member); - - // var sql = GetBase(isContent, isMedia, isMember, null, false) - // .WhereIn(x => x.NodeObjectType, typesAndIds.Keys); - - // ordering = ordering ?? Ordering.ByDefault(); - - // sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); - - // if (!ordering.IsEmpty) - // { - // // apply ordering - // ApplyOrdering(ref sql, ordering); - // } - - // // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently - // //no matter what we always must have node id ordered at the end - // sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); - - // // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names - // var pageIndexToFetch = pageIndex + 1; - // IEnumerable dtos; - // var page = Database.Page(pageIndexToFetch, pageSize, sql); - // dtos = page.Items; - // totalRecords = page.TotalItems; - - // var entities = dtos.Select(BuildEntity).ToArray(); - - // BuildVariants(entities.OfType()); - - // return entities; - //} - + // get a page of entities public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { - query = query.Where(x => x.NodeObjectType == objectType); - return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering); - } - - // get a page of entities - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering) - { - var isContent = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); - var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); - var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); + var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; var sql = GetBaseWhere(isContent, isMedia, isMember, false, x => { if (filter == null) return; foreach (var filterClause in filter.GetWhereClauses()) x.Where(filterClause.Item1, filterClause.Item2); - }, objectTypes); + }, objectType); ordering = ordering ?? Ordering.ByDefault(); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(true, true, true, sql, ordering.IsEmpty); + sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); if (!ordering.IsEmpty) { @@ -114,13 +70,35 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names var pageIndexToFetch = pageIndex + 1; IEnumerable dtos; - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; + if(isContent) + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } + else if (isMedia) + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } + else if (isMember) + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } + else + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } - var entities = dtos.Select(BuildEntity).ToArray(); + var entities = dtos.Select(x => BuildEntity(isContent, isMedia, isMember, x)).ToArray(); - BuildVariants(entities.OfType()); + if (isContent) + BuildVariants(entities.Cast()); return entities; } @@ -129,7 +107,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, key); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); + return dto == null ? null : BuildEntity(false, false, false, dto); } @@ -138,7 +116,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } @@ -149,7 +127,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dto == null) return null; - var entity = BuildEntity(dto); + var entity = BuildEntity(false, isMedia, isMember, dto); return entity; } @@ -168,7 +146,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, id); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); + return dto == null ? null : BuildEntity(false, false, false, dto); } public IEntitySlim Get(int id, Guid objectTypeId) @@ -200,7 +178,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? Enumerable.Empty() @@ -211,7 +189,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ? (IEnumerable)Database.Fetch(sql) : Database.Fetch(sql); - var entities = dtos.Select(BuildEntity).ToArray(); + var entities = dtos.Select(x => BuildEntity(false, isMedia, isMember, x)).ToArray(); return entities; } @@ -255,7 +233,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sql = translator.Translate(); sql = AddGroupBy(false, false, false, sql, true); var dtos = Database.Fetch(sql); - return dtos.Select(BuildEntity).ToList(); + return dtos.Select(x => BuildEntity(false, false, false, x)).ToList(); } public IEnumerable GetByQuery(IQuery query, Guid objectType) @@ -264,7 +242,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var isMedia = objectType == Constants.ObjectTypes.Media; var isMember = objectType == Constants.ObjectTypes.Member; - var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] { objectType }); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, objectType); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); @@ -378,14 +356,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the full sql for a given object type, with a given filter protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action> filter) { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType }); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, objectType); return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action> filter, bool isCount = false) - { + { var sql = Sql(); if (isCount) @@ -423,15 +401,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent || isMedia || isMember) { sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId); } if (isContent) { sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId); + .InnerJoin().On((left, right) => left.NodeId == right.NodeId); } if (isMedia) @@ -455,10 +433,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM [+ filter] + WHERE sql // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid[] objectTypes) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid objectType) { return GetBase(isContent, isMedia, isMember, filter, isCount) - .WhereIn(x => x.NodeObjectType, objectTypes); + .Where(x => x.NodeObjectType == objectType); } // gets the base SELECT + FROM + WHERE sql @@ -532,13 +510,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (sql == null) throw new ArgumentNullException(nameof(sql)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort - // As more things are attempted to be sorted we'll prob have to add more expressions here - var orderBy = ordering.OrderBy.ToUpperInvariant() switch - { - "PATH" => SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"), - _ => ordering.OrderBy - }; + // TODO: although this works for name, it probably doesn't work for others without an alias of some sort + var orderBy = ordering.OrderBy; if (ordering.Direction == Direction.Ascending) sql.OrderBy(orderBy); @@ -551,17 +524,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Classes /// - /// The DTO used to fetch results for a generic content item which could be either a document, media or a member + /// The DTO used to fetch results for a content item with its variation info /// - private class GenericContentEntityDto : DocumentEntityDto - { - public string MediaPath { get; set; } - } - - /// - /// The DTO used to fetch results for a document item with its variation info - /// - private class DocumentEntityDto : BaseDto + private class ContentEntityDto : BaseDto { public ContentVariation Variations { get; set; } @@ -569,17 +534,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public bool Edited { get; set; } } - /// - /// The DTO used to fetch results for a media item with its media path info - /// private class MediaEntityDto : BaseDto { public string MediaPath { get; set; } } - /// - /// The DTO used to fetch results for a member item - /// private class MemberEntityDto : BaseDto { } @@ -630,13 +589,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Factory - private EntitySlim BuildEntity(BaseDto dto) + private EntitySlim BuildEntity(bool isContent, bool isMedia, bool isMember, BaseDto dto) { - if (dto.NodeObjectType == Constants.ObjectTypes.Document) + if (isContent) return BuildDocumentEntity(dto); - if (dto.NodeObjectType == Constants.ObjectTypes.Media) + if (isMedia) return BuildMediaEntity(dto); - if (dto.NodeObjectType == Constants.ObjectTypes.Member) + if (isMember) return BuildMemberEntity(dto); // EntitySlim does not track changes @@ -691,7 +650,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = new DocumentEntitySlim(); BuildContentEntity(entity, dto); - if (dto is DocumentEntityDto contentDto) + if (dto is ContentEntityDto contentDto) { // fill in the invariant info entity.Edited = contentDto.Edited; diff --git a/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs deleted file mode 100644 index a4df5bcf78..0000000000 --- a/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Entities; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.Scoping; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Entities; -using Umbraco.Tests.Testing; - -namespace Umbraco.Tests.Persistence.Repositories -{ - [TestFixture] - [UmbracoTest(Mapper = true, Database = UmbracoTestOptions.Database.NewSchemaPerTest)] - public class EntityRepositoryTest : TestWithDatabaseBase - { - - private EntityRepository CreateRepository(IScopeAccessor scopeAccessor) - { - var entityRepository = new EntityRepository(scopeAccessor); - return entityRepository; - } - - [Test] - public void Get_Paged_Mixed_Entities_By_Ids() - { - //Create content - - var createdContent = new List(); - var contentType = MockedContentTypes.CreateBasicContentType("blah"); - ServiceContext.ContentTypeService.Save(contentType); - for (int i = 0; i < 10; i++) - { - var c1 = MockedContent.CreateBasicContent(contentType); - ServiceContext.ContentService.Save(c1); - createdContent.Add(c1); - } - - //Create media - - var createdMedia = new List(); - var imageType = MockedContentTypes.CreateImageMediaType("myImage"); - ServiceContext.MediaTypeService.Save(imageType); - for (int i = 0; i < 10; i++) - { - var c1 = MockedMedia.CreateMediaImage(imageType, -1); - ServiceContext.MediaService.Save(c1); - createdMedia.Add(c1); - } - - // Create members - var memberType = MockedContentTypes.CreateSimpleMemberType("simple"); - ServiceContext.MemberTypeService.Save(memberType); - var createdMembers = MockedMember.CreateSimpleMember(memberType, 10).ToList(); - ServiceContext.MemberService.Save(createdMembers); - - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repo = CreateRepository((IScopeAccessor)provider); - - var ids = createdContent.Select(x => x.Id).Concat(createdMedia.Select(x => x.Id)).Concat(createdMembers.Select(x => x.Id)); - - var objectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member }; - - var query = SqlContext.Query() - .WhereIn(e => e.Id, ids); - - var entities = repo.GetPagedResultsByQuery(query, objectTypes, 0, 20, out var totalRecords, null, null).ToList(); - - Assert.AreEqual(20, entities.Count); - Assert.AreEqual(30, totalRecords); - - //add the next page - entities.AddRange(repo.GetPagedResultsByQuery(query, objectTypes, 1, 20, out totalRecords, null, null)); - - Assert.AreEqual(30, entities.Count); - Assert.AreEqual(30, totalRecords); - - var contentEntities = entities.OfType().ToList(); - var mediaEntities = entities.OfType().ToList(); - var memberEntities = entities.OfType().ToList(); - - Assert.AreEqual(10, contentEntities.Count); - Assert.AreEqual(10, mediaEntities.Count); - Assert.AreEqual(10, memberEntities.Count); - } - - } - - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c87e6501f9..4b035f631e 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -140,7 +140,6 @@ - diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 11b1260e71..0513017b70 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -662,9 +662,6 @@ namespace Umbraco.Web.Editors if (pageSize <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); - // re-normalize since NULL can be passed in - filter = filter ?? string.Empty; - var objectType = ConvertToObjectType(type); if (objectType.HasValue) { diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index afd1a6b61c..2e44f1327b 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -226,11 +226,11 @@ namespace Umbraco.Web.Models.Mapping { switch (entity) { - case IMemberEntitySlim memberEntity: - return memberEntity.ContentTypeIcon.IfNullOrWhiteSpace(Constants.Icons.Member); - case IContentEntitySlim contentEntity: + case ContentEntitySlim contentEntity: // NOTE: this case covers both content and media entities - return contentEntity.ContentTypeIcon; + return contentEntity.ContentTypeIcon; + case MemberEntitySlim memberEntity: + return memberEntity.ContentTypeIcon.IfNullOrWhiteSpace(Constants.Icons.Member); } return null; diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index aa158799cb..88960fb189 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -380,8 +380,8 @@ namespace Umbraco.Web.Models.Mapping .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); } - private static string MapContentTypeIcon(IEntitySlim entity) - => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + private static string MapContentTypeIcon(EntitySlim entity) + => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, MapperContext context) { diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index fd05f7cfbd..ac75fd831d 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { //get all blueprint content types - var contentTypeAliases = entities.Select(x => ((IContentEntitySlim) x).ContentTypeAlias).Distinct(); + var contentTypeAliases = entities.Select(x => ((ContentEntitySlim) x).ContentTypeAlias).Distinct(); //get the ids var contentTypeIds = Services.ContentTypeService.GetAllContentTypeIds(contentTypeAliases.ToArray()).ToArray(); @@ -75,7 +75,7 @@ namespace Umbraco.Web.Trees var ct = Services.ContentTypeService.Get(intId.Result); if (ct == null) return nodes; - var blueprintsForDocType = entities.Where(x => ct.Alias == ((IContentEntitySlim) x).ContentTypeAlias); + var blueprintsForDocType = entities.Where(x => ct.Alias == ((ContentEntitySlim) x).ContentTypeAlias); nodes.AddRange(blueprintsForDocType .Select(entity => { From 049d51e466e9fec0767493dfd52626845c8e789a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Nov 2019 16:45:28 +1100 Subject: [PATCH 3/8] Adds ability to bulk insert relations and adds tests --- .../Repositories/IRelationRepository.cs | 6 ++ .../Implement/ContentRepositoryBase.cs | 25 +++--- .../Implement/RelationRepository.cs | 71 ++++++++++++++--- src/Umbraco.Core/Services/IRelationService.cs | 2 + .../Services/Implement/RelationService.cs | 18 +++++ .../Services/RelationServiceTests.cs | 78 ++++++++++++++++++- 6 files changed, 175 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 9a2e42568e..885d042d83 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -5,6 +5,12 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IRelationRepository : IReadWriteQueryRepository { + /// + /// Persist multiple at once + /// + /// + void Save(IEnumerable relations); + /// /// Deletes all relations for a parent for any specified relation type alias /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index d93fd72bfb..65f6dc0472 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -853,21 +853,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty()) .ToDictionary(x => x.Alias, x => x); - foreach(var rel in trackedRelations) - { - if (!allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) - throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); + var toSave = trackedRelations.Select(rel => + { + if (!allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) + throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); - if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) - continue; // This shouldn't happen! + if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) + return null; // This shouldn't happen! - if (!keyToIds.TryGetValue(guid, out var id)) - continue; // This shouldn't happen! + if (!keyToIds.TryGetValue(guid, out var id)) + return null; // This shouldn't happen! - //Create new relation - //TODO: This is N+1, we could do this all in one operation, just need a new method on the relations repo - RelationRepository.Save(new Relation(entity.Id, id, relationType)); - } + return new Relation(entity.Id, id, relationType); + }).WhereNotNull(); + + // Save bulk relations + RelationRepository.Save(toSave); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index e6c4c6fd7e..b4fd0a349e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -156,6 +156,50 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #endregion + public void Save(IEnumerable relations) + { + foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) + { + if (hasIdentityGroup.Key) + { + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + var asArray = hasIdentityGroup.ToArray(); + foreach (var relation in hasIdentityGroup) + { + relation.UpdatingEntity(); + var dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } + PopulateObjectTypes(asArray); + } + else + { + // Do bulk inserts + var entitiesAndDtos = hasIdentityGroup.ToDictionary( + r => // key = entity + { + r.AddingEntity(); + return r; + }, + RelationFactory.BuildDto); // value = DTO + + Database.InsertBulk(entitiesAndDtos.Values); + + // All dtos now have IDs assigned + foreach (var de in entitiesAndDtos) + { + // re-assign ID to the entity + de.Key.Id = de.Value.Id; + } + + PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); + } + } + } + public void DeleteByParent(int parentId, params string[] relationTypeAliases) { var subQuery = Sql().Select(x => x.Id) @@ -171,19 +215,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(Sql().Delete().WhereIn(x => x.Id, subQuery)); } - private void PopulateObjectTypes(IRelation entity) + /// + /// Used to populate the object types after insert/update + /// + /// + private void PopulateObjectTypes(params IRelation[] entities) { - var nodes = Database.Fetch(Sql().Select().From().Where(x => x.NodeId == entity.ChildId || x.NodeId == entity.ParentId)) + var entityIds = entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); + + var nodes = Database.Fetch(Sql().Select().From() + .WhereIn(x => x.NodeId, entityIds)) .ToDictionary(x => x.NodeId, x => x.NodeObjectType); - if(nodes.TryGetValue(entity.ParentId, out var parentObjectType)) + foreach (var e in entities) { - entity.ParentObjectType = parentObjectType.GetValueOrDefault(); - } - - if(nodes.TryGetValue(entity.ChildId, out var childObjectType)) - { - entity.ChildObjectType = childObjectType.GetValueOrDefault(); + if (nodes.TryGetValue(e.ParentId, out var parentObjectType)) + { + e.ParentObjectType = parentObjectType.GetValueOrDefault(); + } + if (nodes.TryGetValue(e.ChildId, out var childObjectType)) + { + e.ChildObjectType = childObjectType.GetValueOrDefault(); + } } } } diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 49fa21af2b..6660ca5d1f 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -286,6 +286,8 @@ namespace Umbraco.Core.Services /// Relation to save void Save(IRelation relation); + void Save(IEnumerable relations); + /// /// Saves a /// diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index 490f36e7c7..4a5ca3d8d8 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -417,6 +417,24 @@ namespace Umbraco.Core.Services.Implement } } + 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) { diff --git a/src/Umbraco.Tests/Services/RelationServiceTests.cs b/src/Umbraco.Tests/Services/RelationServiceTests.cs index 0357c9e408..42b6f45047 100644 --- a/src/Umbraco.Tests/Services/RelationServiceTests.cs +++ b/src/Umbraco.Tests/Services/RelationServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using NUnit.Framework; @@ -37,7 +38,7 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.Save(content); } - for (var i = 0; i < 6; i++) + for (var i = 0; i < 6; i++) createContentWithMediaRefs(); //create 6 content items referencing the same media var relations = ServiceContext.RelationService.GetByChildId(m1.Id, Constants.Conventions.RelationTypes.RelatedMediaAlias).ToList(); @@ -83,7 +84,7 @@ namespace Umbraco.Tests.Services [Test] public void Relation_Returns_Parent_Child_Object_Types_When_Creating() { - var r = CreateNewRelation("Test", "test"); + var r = CreateAndSaveRelation("Test", "test"); Assert.AreEqual(Constants.ObjectTypes.Document, r.ParentObjectType); Assert.AreEqual(Constants.ObjectTypes.Media, r.ChildObjectType); @@ -92,7 +93,7 @@ namespace Umbraco.Tests.Services [Test] public void Relation_Returns_Parent_Child_Object_Types_When_Getting() { - var r = CreateNewRelation("Test", "test"); + var r = CreateAndSaveRelation("Test", "test"); // re-get r = ServiceContext.RelationService.GetById(r.Id); @@ -101,7 +102,47 @@ namespace Umbraco.Tests.Services Assert.AreEqual(Constants.ObjectTypes.Media, r.ChildObjectType); } - private IRelation CreateNewRelation(string name, string alias) + [Test] + public void Insert_Bulk_Relations() + { + var rs = ServiceContext.RelationService; + + var newRelations = CreateRelations(10); + + Assert.IsTrue(newRelations.All(x => !x.HasIdentity)); + + ServiceContext.RelationService.Save(newRelations); + + Assert.IsTrue(newRelations.All(x => x.HasIdentity)); + } + + [Test] + public void Update_Bulk_Relations() + { + var rs = ServiceContext.RelationService; + + var date = DateTime.Now.AddDays(-10); + var newRelations = CreateRelations(10); + foreach (var r in newRelations) + { + r.CreateDate = date; + r.UpdateDate = date; + } + + //insert + ServiceContext.RelationService.Save(newRelations); + Assert.IsTrue(newRelations.All(x => x.UpdateDate == date)); + + var newDate = DateTime.Now.AddDays(-5); + foreach (var r in newRelations) + r.UpdateDate = newDate; + + //update + ServiceContext.RelationService.Save(newRelations); + Assert.IsTrue(newRelations.All(x => x.UpdateDate == newDate)); + } + + private IRelation CreateAndSaveRelation(string name, string alias) { var rs = ServiceContext.RelationService; var rt = new RelationType(name, alias, false, null, null); @@ -124,6 +165,35 @@ namespace Umbraco.Tests.Services return r; } + /// + /// Creates a bunch of content/media items return relation objects for them (unsaved) + /// + /// + /// + private IEnumerable CreateRelations(int count) + { + var rs = ServiceContext.RelationService; + var rtName = Guid.NewGuid().ToString(); + var rt = new RelationType(rtName, rtName, false, null, null); + rs.Save(rt); + + var ct = MockedContentTypes.CreateBasicContentType(); + ServiceContext.ContentTypeService.Save(ct); + + var mt = MockedContentTypes.CreateImageMediaType("img"); + ServiceContext.MediaTypeService.Save(mt); + + return Enumerable.Range(1, count).Select(index => + { + var c1 = MockedContent.CreateBasicContent(ct); + var c2 = MockedMedia.CreateMediaImage(mt, -1); + ServiceContext.ContentService.Save(c1); + ServiceContext.MediaService.Save(c2); + + return new Relation(c1.Id, c2.Id, rt); + }).ToList(); + } + //TODO: Create a relation for entities of the wrong Entity Type (GUID) based on the Relation Type's defined parent/child object types } } From bf2727a1ebc4a1ab0aed5a2321f2ea501230ab6f Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:40:00 +0000 Subject: [PATCH 4/8] New model to return from WebAPI --- .../Models/ContentEditing/MediaReferences.cs | 24 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs new file mode 100644 index 0000000000..b10022b105 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "mediaReferences", Namespace = "")] + public class MediaReferences + { + [DataMember(Name = "content")] + public IEnumerable Content { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "members")] + public IEnumerable Members { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "media")] + public IEnumerable Media { get; set; } = Enumerable.Empty(); + + [DataContract(Name = "entityType", Namespace = "")] + public class EntityTypeReferences : EntityBasic + { + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 60b70ed9a8..361a548123 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -222,6 +222,7 @@ + @@ -1283,4 +1284,4 @@ - + \ No newline at end of file From 1c7e2367f58cd2f8a32a594a48271218c897334a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:41:21 +0000 Subject: [PATCH 5/8] New WebAPI method to return references/relations for Media items --- src/Umbraco.Web/Editors/MediaController.cs | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index a673f06e1d..36750d74ec 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -36,6 +36,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Web.ContentApps; using Umbraco.Web.Editors.Binders; using Umbraco.Web.Editors.Filters; +using Umbraco.Core.Models.Entities; namespace Umbraco.Web.Editors { @@ -943,5 +944,71 @@ namespace Umbraco.Web.Editors return hasPathAccess; } + + /// + /// Returns the references (usages) for the media item + /// + /// + /// + public MediaReferences GetReferences(int id) + { + var result = new MediaReferences(); + + var relations = Services.RelationService.GetByChildId(id, Constants.Conventions.RelationTypes.RelatedMediaAlias).ToList(); + var relationEntities = Services.RelationService.GetParentEntitiesFromRelations(relations).ToList(); + + var documents = new List(); + var members = new List(); + var media = new List(); + + foreach (var item in relationEntities) + { + switch (item) + { + case DocumentEntitySlim doc: + documents.Add(new MediaReferences.EntityTypeReferences { + Id = doc.Id, + Key = doc.Key, + Udi = Udi.Create(Constants.UdiEntityType.Document, doc.Key), + Icon = doc.ContentTypeIcon, + Name = doc.Name, + Alias = doc.ContentTypeAlias + }); + break; + + case MemberEntitySlim memb: + members.Add(new MediaReferences.EntityTypeReferences + { + Id = memb.Id, + Key = memb.Key, + Udi = Udi.Create(Constants.UdiEntityType.Member, memb.Key), + Icon = memb.ContentTypeIcon, + Name = memb.Name, + Alias = memb.ContentTypeAlias + }); + break; + + case MediaEntitySlim med: + media.Add(new MediaReferences.EntityTypeReferences + { + Id = med.Id, + Key = med.Key, + Udi = Udi.Create(Constants.UdiEntityType.Media, med.Key), + Icon = med.ContentTypeIcon, + Name = med.Name, + Alias = med.ContentTypeAlias + }); + break; + + default: + break; + } + } + + result.Content = documents; + result.Members = members; + result.Media = media; + return result; + } } } From 642247799a778c50d05e3399c06f192c28d8c55b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:42:00 +0000 Subject: [PATCH 6/8] Adds new AngularJS reference to call the new WebAPI endpoint --- .../src/common/resources/media.resource.js | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index ca7700c188..58c02b55df 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -552,8 +552,31 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { "Search", args)), 'Failed to retrieve media items for search: ' + query); - } + }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getReferences + * @methodOf umbraco.resources.mediaResource + * + * @description + * Retrieves references of a given media item. + * + * @param {Int} id id of media node to retrieve references for + * @returns {Promise} resourcePromise object. + * + */ + getReferences: function (id) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetReferences", + { id: id })), + "Failed to retrieve usages for media of id " + id); + + } }; } From 45392603bdf2d1e553f3a5eaca509dd005cde78b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:45:03 +0000 Subject: [PATCH 7/8] Adds in the new Media Relations tables to the directive view & JS to call the resource/WebAPI --- .../media/umbmedianodeinfo.directive.js | 26 +++- .../components/media/umb-media-node-info.html | 127 ++++++++++++++++-- 2 files changed, 139 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index 4993b013c7..18aa457862 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -1,13 +1,15 @@ (function () { 'use strict'; - function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper) { + function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $routeParams) { function link(scope, element, attrs, ctrl) { var evts = []; + var referencesLoaded = false; scope.allowChangeMediaType = false; + scope.loading = true; function onInit() { @@ -94,6 +96,19 @@ setMediaExtension(); }); + /** Loads in the media references one time */ + function loadRelations() { + if (!referencesLoaded) { + referencesLoaded = true; + mediaResource.getReferences($routeParams.id) + .then(function (data) { + scope.loading = false; + scope.references = data; + scope.hasReferences = data.content.length > 0 || data.members.length > 0; + }); + } + } + //ensure to unregister from all events! scope.$on('$destroy', function () { for (var e in evts) { @@ -102,6 +117,15 @@ }); onInit(); + + // load media type references when the 'info' tab is first activated/switched to + evts.push(eventsService.on("app.tabChange", function (event, args) { + $timeout(function () { + if (args.alias === "umbInfo") { + loadRelations(); + } + }); + })); } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index 4f7141559c..a82e1897a1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -1,20 +1,25 @@
-
+ + + + +
+ + - +
-
+ +
+ @@ -39,12 +141,11 @@ - + From ab87bd200c68e09199b93ba6696a0605502abd52 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:45:21 +0000 Subject: [PATCH 8/8] Adds in lang backoffice keys to EN & EN_US --- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 3 +++ src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 19c8642dc5..c95f059934 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2181,6 +2181,9 @@ To manage your website, simply open the Umbraco back office and start adding con Used in Member Types No references to Member Types. Used by + Used in Documents + Used in Members + Used in Media Log Levels diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 0c572fba26..e4cd5765c1 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2197,6 +2197,9 @@ To manage your website, simply open the Umbraco back office and start adding con Used in Member Types No references to Member Types. Used by + Used in Documents + Used in Members + Used in Media Log Levels