diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index d18fb4b091..516192b905 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -115,7 +115,7 @@ namespace Umbraco.Core /// /// Determines whether a variation varies by culture and segment. /// - public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) > 0; + public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; /// /// Validates that a combination of culture and segment is valid for the variation. diff --git a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs index 0d2f817660..8af48bb881 100644 --- a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs @@ -63,20 +63,5 @@ namespace Umbraco.Core.Models aliases = a; return hasAnyPropertyVariationChanged; } - - /// - /// Returns the list of content types the composition is used in - /// - /// - /// - /// - internal static IEnumerable GetWhereCompositionIsUsedInContentTypes(this IContentTypeComposition source, - IContentTypeComposition[] allContentTypes) - { - var sourceId = source != null ? source.Id : 0; - - // find which content types are using this composition - return allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); - } } } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 838a75b98b..2f455083c0 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -114,6 +114,27 @@ namespace Umbraco.Core.Models } } + /// + /// Gets the property types obtained via composition. + /// + /// + /// Gets them raw, ie with their original variation. + /// + [IgnoreDataMember] + internal IEnumerable RawComposedPropertyTypes => GetRawComposedPropertyTypes(); + + private IEnumerable GetRawComposedPropertyTypes(bool start = true) + { + var propertyTypes = ContentTypeComposition + .Cast() + .SelectMany(x => start ? x.GetRawComposedPropertyTypes(false) : x.CompositionPropertyTypes); + + if (!start) + propertyTypes = propertyTypes.Union(PropertyTypes); + + return propertyTypes; + } + /// /// Adds a content type to the composition. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 3bb1ac38ca..cc9b86c56b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -11,13 +11,6 @@ namespace Umbraco.Core.Persistence.Repositories TItem Get(string alias); IEnumerable> Move(TItem moving, EntityContainer container); - /// - /// Returns the content types that are direct compositions of the content type - /// - /// The content type id - /// - IEnumerable GetTypesDirectlyComposedOf(int id); - /// /// Derives a unique alias from an existing alias. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs index aa61383f85..4bec3160a7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -67,7 +67,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, IsPublishing, this, _templateRepository); } - protected override IEnumerable PerformGetAll(params Guid[] ids) { // use the underlying GetAll which will force cache all content types diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index bea6eb9bce..3184c69dfe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -119,7 +119,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected void PersistNewBaseContentType(IContentTypeComposition entity) { - var dto = ContentTypeFactory.BuildContentTypeDto(entity); //Cannot add a duplicate content type type @@ -234,7 +233,6 @@ AND umbracoNode.nodeObjectType = @objectType", protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) { - var dto = ContentTypeFactory.BuildContentTypeDto(entity); // ensure the alias is not used already @@ -270,8 +268,8 @@ AND umbracoNode.id <> @id", // 1. Find content based on the current ContentType: entity.Id // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one - var compositionBase = entity as ContentTypeCompositionBase; - if (compositionBase != null && compositionBase.RemovedContentTypeKeyTracker != null && + if (entity is ContentTypeCompositionBase compositionBase && + compositionBase.RemovedContentTypeKeyTracker != null && compositionBase.RemovedContentTypeKeyTracker.Any()) { //TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? @@ -314,7 +312,7 @@ AND umbracoNode.id <> @id", } } - // delete the allowed content type entries before re-inserting the collectino of allowed content types + // delete the allowed content type entries before re-inserting the collection of allowed content types Database.Delete("WHERE Id = @Id", new { entity.Id }); foreach (var allowedContentType in entity.AllowedContentTypes) { @@ -409,40 +407,34 @@ AND umbracoNode.id <> @id", } //check if the content type variation has been changed - var ctVariationChanging = entity.IsPropertyDirty("Variations"); - if (ctVariationChanging) + var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); + var oldContentTypeVariation = (ContentVariation) dtoPk.Variations; + var newContentTypeVariation = entity.Variations; + var contentTypeVariationChanging = contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; + if (contentTypeVariationChanging) { - //we've already looked up the previous version of the content type so we know it's previous variation state - MoveVariantData(entity, (ContentVariation)dtoPk.Variations, entity.Variations); + MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); Clear301Redirects(entity); ClearScheduledPublishing(entity); - } + } - //track any content type/property types that are changing variation which will require content updates - var propertyTypeVariationChanges = new Dictionary(); + // collect property types that have a dirty variation + List propertyTypeVariationDirty = null; - // insert or update properties - // all of them, no-group and in-groups + // note: this only deals with *local* property types, we're dealing w/compositions later below foreach (var propertyType in entity.PropertyTypes) { - //if the content type variation isn't changing track if any property type is changing - if (!ctVariationChanging) + if (contentTypeVariationChanging) { - if (propertyType.IsPropertyDirty("Variations")) + // content type is changing + switch (newContentTypeVariation) { - propertyTypeVariationChanges[propertyType.Id] = propertyType.Variations; - } - } - else - { - switch(entity.Variations) - { - case ContentVariation.Nothing: - //if the content type is changing to Nothing, then all property type's must change to nothing + case ContentVariation.Nothing: // changing to Nothing + // all property types must change to Nothing propertyType.Variations = ContentVariation.Nothing; break; - case ContentVariation.Culture: - //we don't need to modify the property type in this case + case ContentVariation.Culture: // changing to Culture + // all property types can remain Nothing break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -451,15 +443,65 @@ AND umbracoNode.id <> @id", } } - var groupId = propertyType.PropertyGroupId?.Value ?? default(int); + // then, track each property individually + if (propertyType.IsPropertyDirty("Variations")) + { + // allocate the list only when needed + if (propertyTypeVariationDirty == null) + propertyTypeVariationDirty = new List(); + + propertyTypeVariationDirty.Add(propertyType); + } + } + + // figure out dirty property types that have actually changed + // before we insert or update properties, so we can read the old variations + var propertyTypeVariationChanges = propertyTypeVariationDirty != null + ? GetPropertyVariationChanges(propertyTypeVariationDirty) + : null; + + // deal with composition property types + // add changes for property types obtained via composition, which change due + // to this content type variations change + if (contentTypeVariationChanging) + { + // must use RawComposedPropertyTypes here: only those types that are obtained + // via composition, with their original variations (ie not filtered by this + // content type variations - we need this true value to make decisions. + + foreach (var propertyType in ((ContentTypeCompositionBase) entity).RawComposedPropertyTypes) + { + if (propertyType.VariesBySegment() || newContentTypeVariation.VariesBySegment()) + throw new NotSupportedException(); // TODO: support this + + if (propertyType.Variations == ContentVariation.Culture) + { + if (propertyTypeVariationChanges == null) + propertyTypeVariationChanges = new Dictionary(); + + // if content type moves to Culture, property type becomes Culture here again + // if content type moves to Nothing, property type becomes Nothing here + if (newContentTypeVariation == ContentVariation.Culture) + propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Nothing, ContentVariation.Culture); + else if (newContentTypeVariation == ContentVariation.Nothing) + propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Culture, ContentVariation.Nothing); + } + } + } + + // insert or update properties + // all of them, no-group and in-groups + foreach (var propertyType in entity.PropertyTypes) + { // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default(int)) + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) AssignDataTypeFromPropertyEditor(propertyType); // validate the alias ValidateAlias(propertyType); // insert or update property + var groupId = propertyType.PropertyGroupId?.Value ?? default; var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); var typeId = propertyType.HasIdentity ? Database.Update(propertyTypeDto) @@ -470,31 +512,22 @@ AND umbracoNode.id <> @id", typeId = propertyType.Id; // not an orphan anymore - if (orphanPropertyTypeIds != null) - orphanPropertyTypeIds.Remove(typeId); + orphanPropertyTypeIds?.Remove(typeId); } - //check if any property types were changing variation - if (propertyTypeVariationChanges.Count > 0) - { - var changes = new Dictionary(); + // must restrict property data changes to impacted content types - if changing a composing + // type, some composed types (those that do not vary) are not impacted and should be left + // unchanged + // + // getting 'all' from the cache policy is prone to race conditions - fast but dangerous + //var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll); + var all = PerformGetAll(); - //now get the current property type variations for the changed ones so that we know which variation they - //are going from and to - var from = Database.Dictionary(Sql() - .Select(x => x.Id, x => x.Variations) - .From() - .WhereIn(x => x.Id, propertyTypeVariationChanges.Keys)); - - foreach (var f in from) - { - changes[f.Key] = (propertyTypeVariationChanges[f.Key], (ContentVariation)f.Value); - } - - //perform the move - MoveVariantData(changes); - } + var impacted = GetImpactedContentTypes(entity, all); + // if some property types have actually changed, move their variant data + if (propertyTypeVariationChanges != null) + MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); // deal with orphan properties: those that were in a deleted tab, // and have not been re-mapped to another tab or to 'generic properties' @@ -503,6 +536,77 @@ AND umbracoNode.id <> @id", DeletePropertyType(entity.Id, id); } + private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable all) + { + var impact = new List(); + var set = new List { contentType }; + + var tree = new Dictionary>(); + foreach (var x in all) + foreach (var y in x.ContentTypeComposition) + { + if (!tree.TryGetValue(y.Id, out var list)) + list = tree[y.Id] = new List(); + list.Add(x); + } + + var nset = new List(); + do + { + impact.AddRange(set); + + foreach (var x in set) + { + if (!tree.TryGetValue(x.Id, out var list)) continue; + nset.AddRange(list.Where(y => y.VariesByCulture())); + } + + set = nset; + nset = new List(); + } while (set.Count > 0); + + return impact; + } + + // gets property types that have actually changed, and the corresponding changes + // returns null if no property type has actually changed + private Dictionary GetPropertyVariationChanges(IEnumerable propertyTypes) + { + var propertyTypesL = propertyTypes.ToList(); + + // select the current variations (before the change) from database + var selectCurrentVariations = Sql() + .Select(x => x.Id, x => x.Variations) + .From() + .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); + + var oldVariations = Database.Dictionary(selectCurrentVariations); + + // build a dictionary of actual changes + Dictionary changes = null; + + foreach (var propertyType in propertyTypesL) + { + // new property type, ignore + if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) + continue; + var oldVariation = (ContentVariation) oldVariationB; // NPoco cannot fetch directly + + // only those property types that *actually* changed + var newVariation = propertyType.Variations; + if (oldVariation == newVariation) + continue; + + // allocate the dictionary only when needed + if (changes == null) + changes = new Dictionary(); + + changes[propertyType.Id] = (oldVariation, newVariation); + } + + return changes; + } + /// /// Clear any redirects associated with content for a content type /// @@ -529,28 +633,39 @@ AND umbracoNode.id <> @id", } /// - /// Moves variant data for property type changes + /// Gets the default language identifier. /// - /// - private void MoveVariantData(IDictionary propertyTypeChanges) + private int GetDefaultLanguageId() { - var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + var selectDefaultLanguageId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); + + return Database.First(selectDefaultLanguageId); + } + + /// + /// Moves variant data for property type variation changes. + /// + private void MovePropertyTypeVariantData(IDictionary propertyTypeChanges, IEnumerable impacted) + { + var defaultLanguageId = GetDefaultLanguageId(); + var impactedL = impacted.Select(x => x.Id).ToList(); //Group by the "To" variation so we can bulk update in the correct batches - foreach(var g in propertyTypeChanges.GroupBy(x => x.Value.Item2)) + foreach(var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) { - var propertyTypeIds = g.Select(s => s.Key).ToList(); + var propertyTypeIds = grouping.Select(x => x.Key).ToList(); + var toVariation = grouping.Key; - //the ContentVariation that the data is moving "To" - var toVariantType = g.Key; - - switch(toVariantType) + switch (toVariation) { case ContentVariation.Culture: - MovePropertyDataToVariantCulture(defaultLangId, propertyTypeIds: propertyTypeIds); + CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); break; case ContentVariation.Nothing: - MovePropertyDataToVariantNothing(defaultLangId, propertyTypeIds: propertyTypeIds); + CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -561,24 +676,17 @@ AND umbracoNode.id <> @id", } /// - /// Moves variant data for a content type variation change + /// Moves variant data for a content type variation change. /// - /// - /// - /// - private void MoveVariantData(IContentTypeComposition contentType, ContentVariation from, ContentVariation to) + private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, ContentVariation toVariation) { - var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + var defaultLanguageId = GetDefaultLanguageId(); - var sqlPropertyTypeIds = Sql().Select(x => x.Id).From().Where(x => x.ContentTypeId == contentType.Id); - switch (to) + switch (toVariation) { case ContentVariation.Culture: - //move the property data - MovePropertyDataToVariantCulture(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); - - //now we need to move the names + //move the names //first clear out any existing names that might already exists under the default lang //there's 2x tables to update @@ -588,10 +696,11 @@ AND umbracoNode.id <> @id", .InnerJoin().On(x => x.Id, x => x.VersionId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLangId); + .Where(x => x.LanguageId == defaultLanguageId); var sqlDelete = Sql() .Delete() .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); //clear out the documentCultureVariation table @@ -599,10 +708,11 @@ AND umbracoNode.id <> @id", .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLangId); + .Where(x => x.LanguageId == defaultLanguageId); sqlDelete = Sql() .Delete() .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); //now we need to insert names into these 2 tables based on the invariant data @@ -610,32 +720,31 @@ AND umbracoNode.id <> @id", //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) - .Append($", {defaultLangId}") //default language ID + .Append($", {defaultLanguageId}") //default language ID .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id); var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); //insert rows into the documentCultureVariation table cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) .AndSelect(x => x.Text) - .Append($", 1, {defaultLangId}") //make Available + default language ID + .Append($", 1, {defaultLanguageId}") //make Available + default language ID .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id); sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); break; case ContentVariation.Nothing: - //move the property data - MovePropertyDataToVariantNothing(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); - - //we dont need to move the names! this is because we always keep the invariant names with the name of the default language. + //we don't need to move the names! this is because we always keep the invariant names with the name of the default language. //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( // if we want these SQL statements back, look into GIT history @@ -649,73 +758,102 @@ AND umbracoNode.id <> @id", } /// - /// This will move all property data from variant to invariant + /// Copies property data from one language to another. /// - /// - /// Optional list of property type ids of the properties to be updated - /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated - private void MovePropertyDataToVariantNothing(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) + /// The source language (can be null ie invariant). + /// The target language (can be null ie invariant) + /// The property type identifiers. + /// The content type identifiers. + private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) { - //first clear out any existing property data that might already exists under the default lang + // fixme - should we batch then? + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > 2000) + throw new NotSupportedException("Too many property/content types."); + + //first clear out any existing property data that might already exists under the target language var sqlDelete = Sql() - .Delete() - .Where(x => x.LanguageId == null); - if (sqlPropertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + .Delete(); + + // not ok for SqlCe (no JOIN in DELETE) + //if (contentTypeIds != null) + // sqlDelete + // .From() + // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) + // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); + + Sql inSql = null; + if (contentTypeIds != null) + { + inSql = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + sqlDelete.WhereIn(x => x.VersionId, inSql); + } + + // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it + if (targetLanguageId == null) + sqlDelete.Where(x => x.LanguageId == null); + else + sqlDelete.Where(x => x.LanguageId == targetLanguageId); + + sqlDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + // see note above, not ok for SqlCe + //if (contentTypeIds != null) + // sqlDelete + // .WhereIn(x => x.ContentTypeId, contentTypeIds); Database.Execute(sqlDelete); - //now insert all property data into the default language that exists under the invariant lang + //now insert all property data into the target language that exists under the source language + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append(", NULL") //null language ID - .From() - .Where(x => x.LanguageId == defaultLangId); - if (sqlPropertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + .Append(", " + targetLanguageIdS) //default language ID + .From(); + + if (contentTypeIds != null) + sqlSelectData + .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) + .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); + + // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it + if (sourceLanguageId == null) + sqlSelectData.Where(x => x.LanguageId == null); + else + sqlSelectData.Where(x => x.LanguageId == sourceLanguageId); + + sqlSelectData + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + sqlSelectData + .WhereIn(x => x.ContentTypeId, contentTypeIds); var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); Database.Execute(sqlInsert); - } - /// - /// This will move all property data from invariant to variant - /// - /// - /// Optional list of property type ids of the properties to be updated - /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated - private void MovePropertyDataToVariantCulture(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) - { - //first clear out any existing property data that might already exists under the default lang - var sqlDelete = Sql() - .Delete() - .Where(x => x.LanguageId == defaultLangId); - if (sqlPropertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + // when copying from Culture, keep the original values around in case we want to go back + // when copying from Nothing, kill the original values, we don't want them around + if (sourceLanguageId == null) + { + sqlDelete = Sql() + .Delete(); - Database.Execute(sqlDelete); + if (contentTypeIds != null) + sqlDelete.WhereIn(x => x.VersionId, inSql); - //now insert all property data into the default language that exists under the invariant lang - var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); - var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append($", {defaultLangId}") //default language ID - .From() - .Where(x => x.LanguageId == null); - if (sqlPropertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + sqlDelete + .Where(x => x.LanguageId == null) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - Database.Execute(sqlInsert); + Database.Execute(sqlDelete); + } } private void DeletePropertyType(int contentTypeId, int propertyTypeId) @@ -851,24 +989,6 @@ AND umbracoNode.id <> @id", } } - /// - public IEnumerable GetTypesDirectlyComposedOf(int id) - { - //fixme - this will probably be more efficient to simply load all content types and do the calculation, see GetWhereCompositionIsUsedInContentTypes - - var sql = Sql() - .SelectAll() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.ChildId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.ParentId == id); - var dtos = Database.Fetch(sql); - return dtos.Any() - ? GetMany(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) - : Enumerable.Empty(); - } - internal static class ContentTypeQueryMapper { public class AssociatedTemplate diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 66b3982b49..06ba1ada79 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -30,12 +30,12 @@ namespace Umbraco.Core.Services string[] filterPropertyTypes = null) { filterContentTypes = filterContentTypes == null - ? new string[] { } - : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + ? Array.Empty() + : filterContentTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); filterPropertyTypes = filterPropertyTypes == null - ? new string[] {} - : filterPropertyTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + ? Array.Empty() + : filterPropertyTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); //create the full list of property types to use as the filter //this is the combination of all property type aliases found in the content types passed in for the filter @@ -47,7 +47,7 @@ namespace Umbraco.Core.Services .Union(filterPropertyTypes) .ToArray(); - var sourceId = source != null ? source.Id : 0; + var sourceId = source?.Id ?? 0; // find out if any content type uses this content type var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); @@ -161,6 +161,5 @@ namespace Umbraco.Core.Services return all; } - } } diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 60677cfd81..b74abc03f7 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -344,37 +344,18 @@ namespace Umbraco.Core.Services.Implement } } + public IEnumerable GetComposedOf(int id, IEnumerable all) + { + return all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id)); + + } + public IEnumerable GetComposedOf(int id) { - //fixme: this is essentially the same as ContentTypeServiceExtensions.GetWhereCompositionIsUsedInContentTypes which loads - // all content types to figure this out, this instead makes quite a few queries so should be replaced - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(ReadLockIds); - - // hash set handles duplicates - var composed = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id.GetHashCode())); - - var ids = new Stack(); - ids.Push(id); - - while (ids.Count > 0) - { - var i = ids.Pop(); - var result = Repository.GetTypesDirectlyComposedOf(i).ToArray(); - - foreach (var c in result) - { - composed.Add(c); - ids.Push(c.Id); - } - } - - return composed.ToArray(); - } + // GetAll is cheap, repository has a full dataset cache policy + // fixme - still, because it uses the cache, race conditions! + var allContentTypes = GetAll(Array.Empty()); + return GetComposedOf(id, allContentTypes); } public int Count() diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index f25382d557..f186ae8e83 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -1,10 +1,8 @@ -using System.Runtime.Remoting; -using NUnit.Framework; +using NUnit.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using NPoco; using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; @@ -12,10 +10,9 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; -using Umbraco.Core.Components; +using Umbraco.Tests.Scoping; namespace Umbraco.Tests.Services { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs new file mode 100644 index 0000000000..c28d4f7955 --- /dev/null +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using LightInject; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Sync; +using Umbraco.Tests.Testing; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.PublishedCache.NuCache.DataSource; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true)] + public class ContentTypeServiceVariantsTests : TestWithSomeContentBase + { + protected override void Compose() + { + base.Compose(); + + // pfew - see note in ScopedNuCacheTests? + Container.RegisterSingleton(); + Container.RegisterSingleton(f => Mock.Of()); + Container.RegisterCollectionBuilder() + .Add(f => f.TryGetInstance().GetCacheRefreshers()); + } + + protected override IPublishedSnapshotService CreatePublishedSnapshotService() + { + var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor); + var runtimeStateMock = new Mock(); + runtimeStateMock.Setup(x => x.Level).Returns(() => RuntimeLevel.Run); + + var contentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), Mock.Of()); + //var documentRepository = Mock.Of(); + var documentRepository = Container.GetInstance(); + var mediaRepository = Mock.Of(); + var memberRepository = Mock.Of(); + + return new PublishedSnapshotService( + options, + null, + runtimeStateMock.Object, + ServiceContext, + contentTypeFactory, + null, + publishedSnapshotAccessor, + Mock.Of(), + Logger, + ScopeProvider, + documentRepository, mediaRepository, memberRepository, + DefaultCultureAccessor, + new DatabaseDataSource(), + Container.GetInstance(), new SiteDomainHelper()); + } + + public class LocalServerMessenger : ServerMessengerBase + { + public LocalServerMessenger() + : base(false) + { } + + protected override void DeliverRemote(ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + throw new NotImplementedException(); + } + } + + private void AssertJsonStartsWith(int id, string expected) + { + var json = GetJson(id).Replace('"', '\''); + var pos = json.IndexOf("'cultureData':", StringComparison.InvariantCultureIgnoreCase); + json = json.Substring(0, pos + "'cultureData':".Length); + Assert.AreEqual(expected, json); + } + + private string GetJson(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var selectJson = SqlContext.Sql().Select().From().Where(x => x.NodeId == id && !x.Published); + var dto = scope.Database.Fetch(selectJson).FirstOrDefault(); + Assert.IsNotNull(dto); + var json = dto.Data; + return json; + } + } + + [Test] + public void Change_Variations_SimpleContentType_VariantToInvariantAndBack() + { + // one simple content type, variant, with both variant and invariant properties + // can change it to invariant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Culture + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value2", ValueStorageType.Ntext) + { + Alias = "value2", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent) new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en", "en"); + document.SetValue("value1", "v1fr", "fr"); + document.SetValue("value2", "v2"); + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content type to Nothing + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.IsNull(document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content back to Culture + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property back to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_SimpleContentType_InvariantToVariantAndBack() + { + // one simple content type, invariant + // can change it to variant and back + // can then switch one property to variant + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Nothing + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Nothing + }, + new PropertyType("value2", ValueStorageType.Ntext) + { + Alias = "value2", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent) new Content("document", -1, contentType); + document.Name = "doc1"; + document.SetValue("value1", "v1"); + document.SetValue("value2", "v2"); + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.Name); + Assert.IsNull(document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content type to Culture + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.AreEqual("v1", document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content back to Nothing + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.Name); + Assert.IsNull(document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_SimpleContentType_VariantPropertyToInvariantAndBack() + { + // one simple content type, variant, with both variant and invariant properties + // can change an invariant property to variant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Culture + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value2", ValueStorageType.Ntext) + { + Alias = "value2", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent)new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en", "en"); + document.SetValue("value1", "v1fr", "fr"); + document.SetValue("value2", "v2"); + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property type to Nothing + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property back to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch other property to Culture + contentType.PropertyTypes.First(x => x.Alias == "value2").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2", "en")); + Assert.IsNull(document.GetValue("value2", "fr")); + Assert.IsNull(document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value2").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'en','seg':'','val':'v2'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_ComposedContentType_1() + { + // one composing content type, variant, with both variant and invariant properties + // one composed content type, variant, with both variant and invariant properties + // can change the composing content type to invariant and back + // can change the composed content type to invariant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var composing = new ContentType(-1) + { + Alias = "composing", + Name = "composing", + Variations = ContentVariation.Culture + }; + + var properties1 = new PropertyTypeCollection(true) + { + new PropertyType("value11", ValueStorageType.Ntext) + { + Alias = "value11", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value12", ValueStorageType.Ntext) + { + Alias = "value12", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(composing); + + var composed = new ContentType(-1) + { + Alias = "composed", + Name = "composed", + Variations = ContentVariation.Culture + }; + + var properties2 = new PropertyTypeCollection(true) + { + new PropertyType("value21", ValueStorageType.Ntext) + { + Alias = "value21", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value22", ValueStorageType.Ntext) + { + Alias = "value22", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composed.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); + composed.AddContentType(composing); + ServiceContext.ContentTypeService.Save(composed); + + var document = (IContent) new Content("document", -1, composed); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value11", "v11en", "en"); + document.SetValue("value11", "v11fr", "fr"); + document.SetValue("value12", "v12"); + document.SetValue("value21", "v21en", "en"); + document.SetValue("value21", "v21fr", "fr"); + document.SetValue("value22", "v22"); + ServiceContext.ContentService.Save(document); + + // both value11 and value21 are variant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composed.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composed); + + // both value11 and value21 are invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composed.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed); + + // value11 is variant again, but value21 is still invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composed.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed); + + // we can make it variant again + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composing.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composing.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is still invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // we can make it variant again + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_ComposedContentType_2() + { + // one composing content type, variant, with both variant and invariant properties + // one composed content type, variant, with both variant and invariant properties + // one composed content type, invariant + // can change the composing content type to invariant and back + // can change the variant composed content type to invariant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var composing = new ContentType(-1) + { + Alias = "composing", + Name = "composing", + Variations = ContentVariation.Culture + }; + + var properties1 = new PropertyTypeCollection(true) + { + new PropertyType("value11", ValueStorageType.Ntext) + { + Alias = "value11", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value12", ValueStorageType.Ntext) + { + Alias = "value12", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(composing); + + var composed1 = new ContentType(-1) + { + Alias = "composed1", + Name = "composed1", + Variations = ContentVariation.Culture + }; + + var properties2 = new PropertyTypeCollection(true) + { + new PropertyType("value21", ValueStorageType.Ntext) + { + Alias = "value21", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value22", ValueStorageType.Ntext) + { + Alias = "value22", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composed1.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); + composed1.AddContentType(composing); + ServiceContext.ContentTypeService.Save(composed1); + + var composed2 = new ContentType(-1) + { + Alias = "composed2", + Name = "composed2", + Variations = ContentVariation.Nothing + }; + + var properties3 = new PropertyTypeCollection(true) + { + new PropertyType("value31", ValueStorageType.Ntext) + { + Alias = "value31", + DataTypeId = -88, + Variations = ContentVariation.Nothing + }, + new PropertyType("value32", ValueStorageType.Ntext) + { + Alias = "value32", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composed2.PropertyGroups.Add(new PropertyGroup(properties3) { Name = "Content" }); + composed2.AddContentType(composing); + ServiceContext.ContentTypeService.Save(composed2); + + var document1 = (IContent) new Content ("document1", -1, composed1); + document1.SetCultureName("doc1en", "en"); + document1.SetCultureName("doc1fr", "fr"); + document1.SetValue("value11", "v11en", "en"); + document1.SetValue("value11", "v11fr", "fr"); + document1.SetValue("value12", "v12"); + document1.SetValue("value21", "v21en", "en"); + document1.SetValue("value21", "v21fr", "fr"); + document1.SetValue("value22", "v22"); + ServiceContext.ContentService.Save(document1); + + var document2 = (IContent)new Content("document2", -1, composed2); + document2.Name = "doc2"; + document2.SetValue("value11", "v11"); + document2.SetValue("value12", "v12"); + document2.SetValue("value31", "v31"); + document2.SetValue("value32", "v32"); + ServiceContext.ContentService.Save(document2); + + // both value11 and value21 are variant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composed1.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composed1); + + // both value11 and value21 are invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composed1.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed1); + + // value11 is variant again, but value21 is still invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composed1.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed1); + + // we can make it variant again + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composing.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composing.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is still invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // we can make it variant again + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 04bccc8bcb..c62e79b4ef 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -129,6 +129,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index de52021220..3dc5cd053a 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -348,8 +348,6 @@ - - Designer diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 22d86631ca..563e8dda1a 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -119,66 +119,74 @@ namespace Umbraco.Web.Editors /// Type of content Type, eg documentType or mediaType /// Id of composition content type /// - protected IEnumerable PerformGetWhereCompositionIsUsedInContentTypes(int contentTypeId, - UmbracoObjectTypes type) + protected IEnumerable PerformGetWhereCompositionIsUsedInContentTypes(int contentTypeId, UmbracoObjectTypes type) { - IContentTypeComposition source = null; + var id = 0; - //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic + if (contentTypeId > 0) + { + IContentTypeComposition source; - IContentTypeComposition[] allContentTypes; + switch (type) + { + case UmbracoObjectTypes.DocumentType: + source = Services.ContentTypeService.Get(contentTypeId); + break; + + case UmbracoObjectTypes.MediaType: + source = Services.ContentTypeService.Get(contentTypeId); + break; + + case UmbracoObjectTypes.MemberType: + source = Services.MemberTypeService.Get(contentTypeId); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + + if (source == null) + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + id = source.Id; + } + + IEnumerable composedOf; switch (type) { case UmbracoObjectTypes.DocumentType: - if (contentTypeId > 0) - { - source = Services.ContentTypeService.Get(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.ContentTypeService.GetAll().Cast().ToArray(); + composedOf = Services.ContentTypeService.GetComposedOf(id); break; case UmbracoObjectTypes.MediaType: - if (contentTypeId > 0) - { - source = Services.ContentTypeService.Get(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.ContentTypeService.GetAll().Cast().ToArray(); + composedOf = Services.MediaTypeService.GetComposedOf(id); break; case UmbracoObjectTypes.MemberType: - if (contentTypeId > 0) - { - source = Services.MemberTypeService.Get(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.MemberTypeService.GetAll().Cast().ToArray(); + composedOf = Services.MemberTypeService.GetComposedOf(id); break; default: - throw new ArgumentOutOfRangeException("The entity type was not a content type"); + throw new ArgumentOutOfRangeException(nameof(type)); } - var contentTypesWhereCompositionIsUsed = source.GetWhereCompositionIsUsedInContentTypes(allContentTypes); - return contentTypesWhereCompositionIsUsed - .Select(x => Mapper.Map(x)) - .Select(x => - { - //translate the name - x.Name = TranslateItem(x.Name); + EntityBasic TranslateName(EntityBasic e) + { + e.Name = TranslateItem(e.Name); + return e; + } - return x; - }) + return composedOf + .Select(Mapper.Map) + .Select(TranslateName) .ToList(); } + protected string TranslateItem(string text) { if (text == null) - { return null; - } if (text.StartsWith("#") == false) return text; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index f414702824..671a949a77 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1160,6 +1160,10 @@ namespace Umbraco.Web.PublishedCache.NuCache var pdatas = new List(); foreach (var pvalue in prop.Values) { + // sanitize - properties should be ok but ... never knows + if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) + continue; + // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' var value = published ? pvalue.PublishedValue : pvalue.EditedValue; if (value != null) @@ -1191,15 +1195,19 @@ namespace Umbraco.Web.PublishedCache.NuCache var cultureData = new Dictionary(); - var names = content is IContent document + // sanitize - names should be ok but ... never knows + if (content.GetContentType().VariesByCulture()) + { + var infos = content is IContent document ? (published ? document.PublishCultureInfos : document.CultureInfos) : content.CultureInfos; - foreach (var (culture, name) in names) - { - cultureData[culture] = new CultureVariation { Name = name.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + foreach (var (culture, info) in infos) + { + cultureData[culture] = new CultureVariation { Name = info.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + } } //the dictionary that will be serialized