From adedc9b64b58b2d9749d5f5e5551fb6fa51c9958 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Wed, 20 Mar 2013 20:53:12 +0600 Subject: [PATCH] Ensures mocked test entities don't have dirty properties on initialization. Ensures content xml structure is rebuilt when a content type's alias is changed or a property type is removed from it. Adds ability to rebuild content xml structures for specified content types, not for all content. Adds ContentTypeExtensions and more unit tests for ContentTypeTests. Changes RuntimeCacheProvider to use new ConcurrentHashSet instead of a dictionary with the same key/value. Adds overload to IRepositoryCacheProvider to clear cache by type. Clears the IContent cache when a Content type is saved. All relates to fixing #U4-1943 --- src/Umbraco.Core/ConcurrentHashSet.cs | 167 ++++++++++++++++++ .../Models/ContentTypeExtensions.cs | 35 ++++ .../Caching/IRepositoryCacheProvider.cs | 6 + .../Caching/InMemoryCacheProvider.cs | 14 ++ .../Persistence/Caching/NullCacheProvider.cs | 5 + .../Caching/RuntimeCacheProvider.cs | 39 +++- .../Repositories/ContentTypeBaseRepository.cs | 14 ++ src/Umbraco.Core/Services/ContentService.cs | 23 ++- .../Services/ContentTypeService.cs | 138 +++++++++++---- src/Umbraco.Core/Umbraco.Core.csproj | 4 +- .../Caching/InMemoryCacheProviderTest.cs | 16 ++ .../Caching/RuntimeCacheProviderTest.cs | 16 ++ .../Services/ContentTypeServiceTests.cs | 116 +++++++++++- .../TestHelpers/Entities/MockedContent.cs | 11 ++ .../Entities/MockedContentTypes.cs | 30 +++- .../TestHelpers/Entities/MockedEntity.cs | 6 + 16 files changed, 587 insertions(+), 53 deletions(-) create mode 100644 src/Umbraco.Core/ConcurrentHashSet.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeExtensions.cs diff --git a/src/Umbraco.Core/ConcurrentHashSet.cs b/src/Umbraco.Core/ConcurrentHashSet.cs new file mode 100644 index 0000000000..1e318b36d1 --- /dev/null +++ b/src/Umbraco.Core/ConcurrentHashSet.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Umbraco.Core +{ + /// + /// A thread-safe representation of a . + /// Enumerating this collection is thread-safe and will only operate on a clone that is generated before returning the enumerator. + /// + /// + [Serializable] + public class ConcurrentHashSet : ICollection + { + private readonly HashSet _innerSet = new HashSet(); + private readonly ReaderWriterLockSlim _instanceLocker = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + /// 1 + public IEnumerator GetEnumerator() + { + return GetThreadSafeClone().GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + /// 2 + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// + /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . + /// + /// The object to remove from the .The is read-only. + public bool Remove(T item) + { + using (new WriteLock(_instanceLocker)) + { + return _innerSet.Remove(item); + } + } + + + /// + /// Gets the number of elements contained in the . + /// + /// + /// The number of elements contained in the . + /// + /// 2 + public int Count + { + get { return GetThreadSafeClone().Count; } + } + + /// + /// Gets a value indicating whether the is read-only. + /// + /// + /// true if the is read-only; otherwise, false. + /// + public bool IsReadOnly + { + get { return false; } + } + + /// + /// Adds an item to the . + /// + /// The object to add to the .The is read-only. + public void Add(T item) + { + using (new WriteLock(_instanceLocker)) + { + _innerSet.Add(item); + } + } + + /// + /// Attempts to add an item to the collection + /// + /// + /// + public bool TryAdd(T item) + { + var clone = GetThreadSafeClone(); + if (clone.Contains(item)) return false; + using (new WriteLock(_instanceLocker)) + { + //double check + if (_innerSet.Contains(item)) return false; + _innerSet.Add(item); + return true; + } + } + + /// + /// Removes all items from the . + /// + /// The is read-only. + public void Clear() + { + using (new WriteLock(_instanceLocker)) + { + _innerSet.Clear(); + } + } + + /// + /// Determines whether the contains a specific value. + /// + /// + /// true if is found in the ; otherwise, false. + /// + /// The object to locate in the . + public bool Contains(T item) + { + return GetThreadSafeClone().Contains(item); + } + + /// + /// Copies the elements of the to an , starting at a specified index. + /// + /// The one-dimensional that is the destination of the elements copied from the . The array must have zero-based indexing.The zero-based index in at which copying begins. is a null reference (Nothing in Visual Basic). is less than zero. is equal to or greater than the length of the -or- The number of elements in the source is greater than the available space from to the end of the destination . + public void CopyTo(T[] array, int index) + { + var clone = GetThreadSafeClone(); + clone.CopyTo(array, index); + } + + private HashSet GetThreadSafeClone() + { + HashSet clone = null; + using (new WriteLock(_instanceLocker)) + { + clone = new HashSet(_innerSet, _innerSet.Comparer); + } + return clone; + } + + /// + /// Copies the elements of the to an , starting at a particular index. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. The zero-based index in at which copying begins. is null. is less than zero. is multidimensional.-or- The number of elements in the source is greater than the available space from to the end of the destination . The type of the source cannot be cast automatically to the type of the destination . 2 + public void CopyTo(Array array, int index) + { + var clone = GetThreadSafeClone(); + Array.Copy(clone.ToArray(), 0, array, index, clone.Count); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeExtensions.cs b/src/Umbraco.Core/Models/ContentTypeExtensions.cs new file mode 100644 index 0000000000..b328cb5e0b --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeExtensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models +{ + public static class ContentTypeExtensions + { + /// + /// Get all descendant content types + /// + /// + /// + public static IEnumerable Descendants(this IContentType contentType) + { + var contentTypeService = ApplicationContext.Current.Services.ContentTypeService; + var descendants = contentTypeService.GetContentTypeChildren(contentType.Id) + .FlattenList(type => contentTypeService.GetContentTypeChildren(type.Id)); + return descendants; + } + + /// + /// Get all descendant and self content types + /// + /// + /// + public static IEnumerable DescendantsAndSelf(this IContentType contentType) + { + var contentTypeService = ApplicationContext.Current.Services.ContentTypeService; + var descendants = contentTypeService.GetContentTypeChildren(contentType.Id) + .FlattenList(type => contentTypeService.GetContentTypeChildren(type.Id)); + var descendantsAndSelf = new[] { contentType }.Concat(contentType.Descendants()); + return descendantsAndSelf; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Caching/IRepositoryCacheProvider.cs b/src/Umbraco.Core/Persistence/Caching/IRepositoryCacheProvider.cs index d65a819871..d03c81de1e 100644 --- a/src/Umbraco.Core/Persistence/Caching/IRepositoryCacheProvider.cs +++ b/src/Umbraco.Core/Persistence/Caching/IRepositoryCacheProvider.cs @@ -45,5 +45,11 @@ namespace Umbraco.Core.Persistence.Caching /// /// void Delete(Type type, IEntity entity); + + /// + /// Clears the cache by type + /// + /// + void Clear(Type type); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Caching/InMemoryCacheProvider.cs b/src/Umbraco.Core/Persistence/Caching/InMemoryCacheProvider.cs index 2ef7b075af..5544c74494 100644 --- a/src/Umbraco.Core/Persistence/Caching/InMemoryCacheProvider.cs +++ b/src/Umbraco.Core/Persistence/Caching/InMemoryCacheProvider.cs @@ -92,6 +92,20 @@ namespace Umbraco.Core.Persistence.Caching bool result = _cache.TryRemove(GetCompositeId(type, entity.Id), out entity1); } + /// + /// Clear cache by type + /// + /// + public void Clear(Type type) + { + var keys = _cache.Keys; + foreach (var key in keys.Where(x => x.StartsWith(string.Format("{0}-", type.Name)))) + { + IEntity e; + _cache.TryRemove(key, out e); + } + } + public void Clear() { _cache.Clear(); diff --git a/src/Umbraco.Core/Persistence/Caching/NullCacheProvider.cs b/src/Umbraco.Core/Persistence/Caching/NullCacheProvider.cs index fe196fa477..c7e868389b 100644 --- a/src/Umbraco.Core/Persistence/Caching/NullCacheProvider.cs +++ b/src/Umbraco.Core/Persistence/Caching/NullCacheProvider.cs @@ -46,6 +46,11 @@ namespace Umbraco.Core.Persistence.Caching return; } + public void Clear(Type type) + { + return; + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs b/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs index bb2b80a766..e79b61ed5d 100644 --- a/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Runtime.Caching; using System.Threading; using Umbraco.Core.Models.EntityBase; @@ -25,7 +26,7 @@ namespace Umbraco.Core.Persistence.Caching #endregion //TODO Save this in cache as well, so its not limited to a single server usage - private ConcurrentDictionary _keyTracker = new ConcurrentDictionary(); + private readonly ConcurrentHashSet _keyTracker = new ConcurrentHashSet(); private ObjectCache _memoryCache = new MemoryCache("in-memory"); private static readonly ReaderWriterLockSlim ClearLock = new ReaderWriterLockSlim(); @@ -46,7 +47,7 @@ namespace Umbraco.Core.Persistence.Caching public IEnumerable GetAllByType(Type type) { - foreach (var key in _keyTracker.Keys) + foreach (var key in _keyTracker) { if (key.StartsWith(type.Name)) { @@ -59,8 +60,7 @@ namespace Umbraco.Core.Persistence.Caching { var key = GetCompositeId(type, entity.Id); var exists = _memoryCache.GetCacheItem(key) != null; - - _keyTracker.TryAdd(key, key); + _keyTracker.TryAdd(key); if (exists) { _memoryCache.Set(key, entity, new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(5) }); @@ -72,16 +72,37 @@ namespace Umbraco.Core.Persistence.Caching public void Delete(Type type, IEntity entity) { - string throwaway = null; var key = GetCompositeId(type, entity.Id); - var keyBeSure = _keyTracker.TryGetValue(key, out throwaway); - object itemRemoved = _memoryCache.Remove(key); - _keyTracker.TryRemove(key, out throwaway); + _memoryCache.Remove(key); + _keyTracker.Remove(key); + } + + /// + /// Clear cache by type + /// + /// + public void Clear(Type type) + { + using (new WriteLock(ClearLock)) + { + var keys = new string[_keyTracker.Count]; + _keyTracker.CopyTo(keys, 0); + var keysToRemove = new List(); + foreach (var key in keys.Where(x => x.StartsWith(string.Format("{0}-", type.Name)))) + { + _keyTracker.Remove(key); + keysToRemove.Add(key); + } + foreach (var key in keysToRemove) + { + _memoryCache.Remove(key); + } + } } public void Clear() { - using (new ReadLock(ClearLock)) + using (new WriteLock(ClearLock)) { _keyTracker.Clear(); _memoryCache.DisposeIfDisposable(); diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 3aa55b36e3..6a93b0698d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -55,6 +55,20 @@ namespace Umbraco.Core.Persistence.Repositories } } + /// + /// We need to override this method to ensure that any content cache is cleared + /// + /// + /// + /// see: http://issues.umbraco.org/issue/U4-1963 + /// + public override void PersistUpdatedItem(IEntity entity) + { + InMemoryCacheProvider.Current.Clear(typeof(IContent)); + RuntimeCacheProvider.Current.Clear(typeof(IContent)); + base.PersistUpdatedItem(entity); + } + protected void PersistNewBaseContentType(ContentTypeDto dto, IContentTypeComposition entity) { //Logic for setting Path, Level and SortOrder diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 15f81f9049..c6cd7de277 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -470,7 +470,7 @@ namespace Umbraco.Core.Services /// /// This will rebuild the xml structures for content in the database. /// - /// Optional Id of the User issueing the publishing + /// This is not used for anything /// True if publishing succeeded, otherwise False /// /// This is used for when a document type alias or a document type property is changed, the xml will need to @@ -490,7 +490,26 @@ namespace Umbraco.Core.Services } } - /// + /// + /// This will rebuild the xml structures for content in the database. + /// + /// + /// If specified will only rebuild the xml for the content type's specified, otherwise will update the structure + /// for all published content. + /// + internal void RePublishAll(params int[] contentTypeIds) + { + try + { + RePublishAllDo(contentTypeIds); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred executing RePublishAll", ex); + } + } + + /// /// Publishes a single object /// /// The to publish diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 9ecafc9ef6..3c81b7c80a 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; @@ -115,6 +116,56 @@ namespace Umbraco.Core.Services } } + /// + /// This is called after an IContentType is saved and is used to update the content xml structures in the database + /// if they are required to be updated. + /// + /// + private void UpdateContentXmlStructure(params IContentType[] contentTypes) + { + + var toUpdate = new List(); + + foreach (var contentType in contentTypes) + { + //we need to determine if we need to refresh the xml content in the database. This is to be done when: + // - a content type changes it's alias + // - if a content type has it's property removed + //here we need to check if the alias of the content type changed or if one of the properties was removed. + var dirty = contentType as IRememberBeingDirty; + if (dirty != null && (dirty.WasPropertyDirty("Alias") || dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved"))) + { + //if the alias was changed then we only need to update the xml structures for content of the current content type. + //if a property was deleted then we need to update the xml structures for any content of the current content type + // and any of the content type's child content types. + if (dirty.WasPropertyDirty("Alias") && !dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved")) + { + //if only the alias changed then only update the current content type + toUpdate.Add(contentType); + } + else + { + //if a property was deleted (and maybe the alias changed too), the update all content of the current content type + // and all of it's desscendant doc types. + toUpdate.AddRange(contentType.DescendantsAndSelf()); + } + } + } + + var typedContentService = _contentService as ContentService; + if (typedContentService != null && toUpdate.Any()) + { + typedContentService.RePublishAll(toUpdate.Select(x => x.Id).ToArray()); + } + else if (toUpdate.Any()) + { + //this should never occur, the content service should always be typed but we'll check anyways. + _contentService.RePublishAll(); + } + + + } + /// /// Saves a single object /// @@ -124,18 +175,21 @@ namespace Umbraco.Core.Services { if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(contentType), this)) return; - - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) - { - contentType.CreatorId = userId; - repository.AddOrUpdate(contentType); - uow.Commit(); + using (new WriteLock(Locker)) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) + { + contentType.CreatorId = userId; + repository.AddOrUpdate(contentType); - SavedContentType.RaiseEvent(new SaveEventArgs(contentType, false), this); - } + uow.Commit(); + } + UpdateContentXmlStructure(contentType); + } + SavedContentType.RaiseEvent(new SaveEventArgs(contentType, false), this); Audit.Add(AuditTypes.Save, string.Format("Save ContentType performed by user"), userId, contentType.Id); } @@ -146,24 +200,30 @@ namespace Umbraco.Core.Services /// Optional id of the user saving the ContentType public void Save(IEnumerable contentTypes, int userId = 0) { - if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(contentTypes), this)) + var asArray = contentTypes.ToArray(); + + if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) return; - - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) - { - foreach (var contentType in contentTypes) - { - contentType.CreatorId = userId; - repository.AddOrUpdate(contentType); - } - //save it all in one go - uow.Commit(); + using (new WriteLock(Locker)) + { - SavedContentType.RaiseEvent(new SaveEventArgs(contentTypes, false), this); - } + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) + { + foreach (var contentType in asArray) + { + contentType.CreatorId = userId; + repository.AddOrUpdate(contentType); + } + //save it all in one go + uow.Commit(); + } + + UpdateContentXmlStructure(asArray); + } + SavedContentType.RaiseEvent(new SaveEventArgs(asArray, false), this); Audit.Add(AuditTypes.Save, string.Format("Save ContentTypes performed by user"), userId, -1); } @@ -205,13 +265,14 @@ namespace Umbraco.Core.Services /// public void Delete(IEnumerable contentTypes, int userId = 0) { - if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(contentTypes), this)) + var asArray = contentTypes.ToArray(); + + if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) return; using (new WriteLock(Locker)) { - var contentTypeList = contentTypes.ToList(); - foreach (var contentType in contentTypeList) + foreach (var contentType in asArray) { _contentService.DeleteContentOfType(contentType.Id); } @@ -219,14 +280,14 @@ namespace Umbraco.Core.Services var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) { - foreach (var contentType in contentTypeList) + foreach (var contentType in asArray) { repository.Delete(contentType); } uow.Commit(); - DeletedContentType.RaiseEvent(new DeleteEventArgs(contentTypes, false), this); + DeletedContentType.RaiseEvent(new DeleteEventArgs(asArray, false), this); } Audit.Add(AuditTypes.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); @@ -335,14 +396,16 @@ namespace Umbraco.Core.Services /// Optional Id of the user savging the MediaTypes public void Save(IEnumerable mediaTypes, int userId = 0) { - if (SavingMediaType.IsRaisedEventCancelled(new SaveEventArgs(mediaTypes), this)) + var asArray = mediaTypes.ToArray(); + + if (SavingMediaType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) return; var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateMediaTypeRepository(uow)) { - foreach (var mediaType in mediaTypes) + foreach (var mediaType in asArray) { mediaType.CreatorId = userId; repository.AddOrUpdate(mediaType); @@ -351,7 +414,7 @@ namespace Umbraco.Core.Services //save it all in one go uow.Commit(); - SavedMediaType.RaiseEvent(new SaveEventArgs(mediaTypes, false), this); + SavedMediaType.RaiseEvent(new SaveEventArgs(asArray, false), this); } Audit.Add(AuditTypes.Save, string.Format("Save MediaTypes performed by user"), userId, -1); @@ -392,13 +455,14 @@ namespace Umbraco.Core.Services /// /// Deleting a will delete all the objects based on this public void Delete(IEnumerable mediaTypes, int userId = 0) - { - if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(mediaTypes), this)) + { + var asArray = mediaTypes.ToArray(); + + if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) return; using (new WriteLock(Locker)) { - var mediaTypeList = mediaTypes.ToList(); - foreach (var mediaType in mediaTypeList) + foreach (var mediaType in asArray) { _mediaService.DeleteMediaOfType(mediaType.Id); } @@ -406,13 +470,13 @@ namespace Umbraco.Core.Services var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateMediaTypeRepository(uow)) { - foreach (var mediaType in mediaTypeList) + foreach (var mediaType in asArray) { repository.Delete(mediaType); } uow.Commit(); - DeletedMediaType.RaiseEvent(new DeleteEventArgs(mediaTypes, false), this); + DeletedMediaType.RaiseEvent(new DeleteEventArgs(asArray, false), this); } Audit.Add(AuditTypes.Delete, string.Format("Delete MediaTypes performed by user"), userId, -1); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 5e3a76aa3f..d6b8f1c3b1 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -114,6 +114,7 @@ + @@ -160,6 +161,7 @@ + @@ -736,7 +738,7 @@ Constants.cs - + Constants.cs diff --git a/src/Umbraco.Tests/Persistence/Caching/InMemoryCacheProviderTest.cs b/src/Umbraco.Tests/Persistence/Caching/InMemoryCacheProviderTest.cs index daf1bc03aa..775e6ee078 100644 --- a/src/Umbraco.Tests/Persistence/Caching/InMemoryCacheProviderTest.cs +++ b/src/Umbraco.Tests/Persistence/Caching/InMemoryCacheProviderTest.cs @@ -34,6 +34,22 @@ namespace Umbraco.Tests.Persistence.Caching _registry.Save(typeof(MockedEntity), entity6); } + [Test] + public void Can_Clear_By_Type() + { + var customObj1 = new CustomMockedEntity { Id = 5, Key = 5.ToGuid(), Alias = "mocked5", Name = "Mocked5", Value = Guid.NewGuid().ToString("n") }; + var customObj2 = new CustomMockedEntity { Id = 6, Key = 6.ToGuid(), Alias = "mocked6", Name = "Mocked6", Value = Guid.NewGuid().ToString("n") }; + + _registry.Save(typeof(CustomMockedEntity), customObj1); + _registry.Save(typeof(CustomMockedEntity), customObj2); + + Assert.AreEqual(2, _registry.GetAllByType(typeof(CustomMockedEntity)).Count()); + + _registry.Clear(typeof(CustomMockedEntity)); + + Assert.AreEqual(0, _registry.GetAllByType(typeof(CustomMockedEntity)).Count()); + } + [Test] public void Can_Get_Entity_From_Registry() { diff --git a/src/Umbraco.Tests/Persistence/Caching/RuntimeCacheProviderTest.cs b/src/Umbraco.Tests/Persistence/Caching/RuntimeCacheProviderTest.cs index 5d8f5c6824..8c176df515 100644 --- a/src/Umbraco.Tests/Persistence/Caching/RuntimeCacheProviderTest.cs +++ b/src/Umbraco.Tests/Persistence/Caching/RuntimeCacheProviderTest.cs @@ -34,6 +34,22 @@ namespace Umbraco.Tests.Persistence.Caching _registry.Save(typeof(MockedEntity), entity6); } + [Test] + public void Can_Clear_By_Type() + { + var customObj1 = new CustomMockedEntity { Id = 5, Key = 5.ToGuid(), Alias = "mocked5", Name = "Mocked5", Value = Guid.NewGuid().ToString("n") }; + var customObj2 = new CustomMockedEntity { Id = 6, Key = 6.ToGuid(), Alias = "mocked6", Name = "Mocked6", Value = Guid.NewGuid().ToString("n") }; + + _registry.Save(typeof(CustomMockedEntity), customObj1); + _registry.Save(typeof(CustomMockedEntity), customObj2); + + Assert.AreEqual(2, _registry.GetAllByType(typeof(CustomMockedEntity)).Count()); + + _registry.Clear(typeof(CustomMockedEntity)); + + Assert.AreEqual(0, _registry.GetAllByType(typeof(CustomMockedEntity)).Count()); + } + [Test] public void Can_Get_Entity_From_Registry() { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index f1cd764bc5..1c459d66a3 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.Rdbms; using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Services @@ -23,7 +24,120 @@ namespace Umbraco.Tests.Services base.TearDown(); } - [Test] + [Test] + public void Deleting_PropertyType_Removes_The_Property_From_Content() + { + IContentType contentType1 = MockedContentTypes.CreateTextpageContentType("test1", "Test1"); + ServiceContext.ContentTypeService.Save(contentType1); + IContent contentItem = MockedContent.CreateTextpageContent(contentType1, "Testing", -1); + ServiceContext.ContentService.SaveAndPublish(contentItem); + var initProps = contentItem.Properties.Count; + var initPropTypes = contentItem.PropertyTypes.Count(); + + //remove a property + contentType1.RemovePropertyType(contentType1.PropertyTypes.First().Alias); + ServiceContext.ContentTypeService.Save(contentType1); + + //re-load it from the db + contentItem = ServiceContext.ContentService.GetById(contentItem.Id); + + Assert.AreEqual(initPropTypes - 1, contentItem.PropertyTypes.Count()); + Assert.AreEqual(initProps -1, contentItem.Properties.Count); + } + + [Test] + public void Rebuild_Content_Xml_On_Alias_Change() + { + var contentType1 = MockedContentTypes.CreateTextpageContentType("test1", "Test1"); + var contentType2 = MockedContentTypes.CreateTextpageContentType("test2", "Test2"); + ServiceContext.ContentTypeService.Save(contentType1); + ServiceContext.ContentTypeService.Save(contentType2); + var contentItems1 = MockedContent.CreateTextpageContent(contentType1, -1, 10).ToArray(); + contentItems1.ForEach(x => ServiceContext.ContentService.SaveAndPublish(x)); + var contentItems2 = MockedContent.CreateTextpageContent(contentType2, -1, 5).ToArray(); + contentItems2.ForEach(x => ServiceContext.ContentService.SaveAndPublish(x)); + //only update the contentType1 alias which will force an xml rebuild for all content of that type + contentType1.Alias = "newAlias"; + ServiceContext.ContentTypeService.Save(contentType1); + + foreach (var c in contentItems1) + { + var xml = DatabaseContext.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = c.Id }); + Assert.IsNotNull(xml); + Assert.IsTrue(xml.Xml.StartsWith("("WHERE nodeId = @Id", new { Id = c.Id }); + Assert.IsNotNull(xml); + Assert.IsTrue(xml.Xml.StartsWith(" ServiceContext.ContentService.SaveAndPublish(x)); + var alias = contentType1.PropertyTypes.First().Alias; + var elementToMatch = "<" + alias + ">"; + foreach (var c in contentItems1) + { + var xml = DatabaseContext.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = c.Id }); + Assert.IsNotNull(xml); + Assert.IsTrue(xml.Xml.Contains(elementToMatch)); //verify that it is there before we remove the property + } + + //remove a property + contentType1.RemovePropertyType(contentType1.PropertyTypes.First().Alias); + ServiceContext.ContentTypeService.Save(contentType1); + + var reQueried = ServiceContext.ContentTypeService.GetContentType(contentType1.Id); + var reContent = ServiceContext.ContentService.GetById(contentItems1.First().Id); + + foreach (var c in contentItems1) + { + var xml = DatabaseContext.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = c.Id }); + Assert.IsNotNull(xml); + Assert.IsFalse(xml.Xml.Contains(elementToMatch)); //verify that it is no longer there + } + } + + [Test] + public void Get_Descendants() + { + // Arrange + var contentTypeService = ServiceContext.ContentTypeService; + var hierarchy = CreateContentTypeHierarchy(); + contentTypeService.Save(hierarchy, 0); //ensure they are saved! + var master = hierarchy.First(); + + //Act + var descendants = master.Descendants(); + + //Assert + Assert.AreEqual(10, descendants.Count()); + } + + [Test] + public void Get_Descendants_And_Self() + { + // Arrange + var contentTypeService = ServiceContext.ContentTypeService; + var hierarchy = CreateContentTypeHierarchy(); + contentTypeService.Save(hierarchy, 0); //ensure they are saved! + var master = hierarchy.First(); + + //Act + var descendants = master.DescendantsAndSelf(); + + //Assert + Assert.AreEqual(11, descendants.Count()); + } + + [Test] public void Can_Bulk_Save_New_Hierarchy_Content_Types() { // Arrange diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs index b2303f7cd9..02929dcc76 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs @@ -19,6 +19,8 @@ namespace Umbraco.Tests.TestHelpers.Entities content.PropertyValues(obj); + content.ResetDirtyProperties(false); + return content; } @@ -35,6 +37,8 @@ namespace Umbraco.Tests.TestHelpers.Entities content.PropertyValues(obj); + content.ResetDirtyProperties(false); + return content; } @@ -51,6 +55,8 @@ namespace Umbraco.Tests.TestHelpers.Entities content.PropertyValues(obj); + content.ResetDirtyProperties(false); + return content; } @@ -68,6 +74,8 @@ namespace Umbraco.Tests.TestHelpers.Entities content.PropertyValues(obj); + content.ResetDirtyProperties(false); + return content; } @@ -89,6 +97,9 @@ namespace Umbraco.Tests.TestHelpers.Entities }; content.PropertyValues(obj); + + content.ResetDirtyProperties(false); + list.Add(content); } diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index c90c3aef8f..49176dfb5d 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -7,12 +7,12 @@ namespace Umbraco.Tests.TestHelpers.Entities { public class MockedContentTypes { - public static ContentType CreateTextpageContentType() + public static ContentType CreateTextpageContentType(string alias = "textPage", string name = "Text Page") { var contentType = new ContentType(-1) { - Alias = "textPage", - Name = "Text Page", + Alias = alias, + Name = name, Description = "ContentType used for Text pages", Icon = ".sprTreeDoc3", Thumbnail = "doc.png", @@ -32,6 +32,9 @@ namespace Umbraco.Tests.TestHelpers.Entities contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); contentType.PropertyGroups.Add(new PropertyGroup(metaCollection) { Name = "Meta", SortOrder = 2 }); + //ensure that nothing is marked as dirty + contentType.ResetDirtyProperties(false); + return contentType; } @@ -55,6 +58,9 @@ namespace Umbraco.Tests.TestHelpers.Entities contentType.PropertyGroups.Add(new PropertyGroup(metaCollection) { Name = "Meta", SortOrder = 2 }); + //ensure that nothing is marked as dirty + contentType.ResetDirtyProperties(false); + return contentType; } @@ -79,6 +85,9 @@ namespace Umbraco.Tests.TestHelpers.Entities contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + //ensure that nothing is marked as dirty + contentType.ResetDirtyProperties(false); + return contentType; } @@ -102,6 +111,9 @@ namespace Umbraco.Tests.TestHelpers.Entities contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + //ensure that nothing is marked as dirty + contentType.ResetDirtyProperties(false); + return contentType; } @@ -126,6 +138,9 @@ namespace Umbraco.Tests.TestHelpers.Entities contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + //ensure that nothing is marked as dirty + contentType.ResetDirtyProperties(false); + return contentType; } @@ -145,6 +160,9 @@ namespace Umbraco.Tests.TestHelpers.Entities contentType.PropertyGroups.Add(new PropertyGroup(collection) { Name = "Content", SortOrder = 1 }); + //ensure that nothing is marked as dirty + contentType.ResetDirtyProperties(false); + return contentType; } @@ -168,6 +186,9 @@ namespace Umbraco.Tests.TestHelpers.Entities mediaType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Media", SortOrder = 1 }); + //ensure that nothing is marked as dirty + mediaType.ResetDirtyProperties(false); + return mediaType; } @@ -194,6 +215,9 @@ namespace Umbraco.Tests.TestHelpers.Entities mediaType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Media", SortOrder = 1 }); + //ensure that nothing is marked as dirty + mediaType.ResetDirtyProperties(false); + return mediaType; } } diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedEntity.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedEntity.cs index cbe1509327..4e39798ef5 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedEntity.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedEntity.cs @@ -17,4 +17,10 @@ namespace Umbraco.Tests.TestHelpers.Entities [DataMember] public string Value { get; set; } } + + public class CustomMockedEntity : MockedEntity + { + [DataMember] + public string Title { get; set; } + } } \ No newline at end of file