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