Single block migration (#20663)

* WiP blocklist migration

* Mostly working migration

* [WIP] deconstructed the migration to prefetch and process all data that requires the old definitions

* Working singleblock migration

* Abstracted some logic and applied it to settings elements too.

* Align class and file name.

* Minor code warning resolution.

* More and better comments + made classes internal where it made sense

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Sven Geusens
2025-11-11 12:43:03 +01:00
committed by GitHub
parent 524912a893
commit 0858d02172
12 changed files with 792 additions and 4 deletions

View File

@@ -6,9 +6,11 @@ namespace Umbraco.Cms.Core.Cache.PropertyEditors;
internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache
{
private const string CacheKey = $"{nameof(BlockEditorElementTypeCache)}_ElementTypes";
private readonly IContentTypeService _contentTypeService;
private readonly AppCaches _appCaches;
public BlockEditorElementTypeCache(IContentTypeService contentTypeService, AppCaches appCaches)
{
_contentTypeService = contentTypeService;
@@ -20,15 +22,15 @@ internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache
public IEnumerable<IContentType> GetAll()
{
// TODO: make this less dumb; don't fetch all elements, only fetch the items that aren't yet in the cache and amend the cache as more elements are loaded
const string cacheKey = $"{nameof(BlockEditorElementTypeCache)}_ElementTypes";
IEnumerable<IContentType>? cachedElements = _appCaches.RequestCache.GetCacheItem<IEnumerable<IContentType>>(cacheKey);
IEnumerable<IContentType>? cachedElements = _appCaches.RequestCache.GetCacheItem<IEnumerable<IContentType>>(CacheKey);
if (cachedElements is null)
{
cachedElements = _contentTypeService.GetAllElementTypes();
_appCaches.RequestCache.Set(cacheKey, cachedElements);
_appCaches.RequestCache.Set(CacheKey, cachedElements);
}
return cachedElements;
}
public void ClearAll() => _appCaches.RequestCache.Remove(CacheKey);
}

View File

@@ -6,4 +6,5 @@ public interface IBlockEditorElementTypeCache
{
IEnumerable<IContentType> GetMany(IEnumerable<Guid> keys);
IEnumerable<IContentType> GetAll();
void ClearAll() { }
}

View File

@@ -139,5 +139,9 @@ public class UmbracoPlan : MigrationPlan
To<V_17_0_0.AddDistributedJobLock>("{263075BF-F18A-480D-92B4-4947D2EAB772}");
To<V_17_0_0.AddLastSyncedTable>("26179D88-58CE-4C92-B4A4-3CBA6E7188AC");
To<V_17_0_0.EnsureDefaultMediaFolderHasDefaultCollection>("{8B2C830A-4FFB-4433-8337-8649B0BF52C8}");
// To 18.0.0
// TODO (V18): Enable on 18 branch
//// To<V_18_0_0.MigrateSingleBlockList>("{74332C49-B279-4945-8943-F8F00B1F5949}");
}
}

View File

@@ -0,0 +1,436 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.PropertyEditors;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0;
public class MigrateSingleBlockList : AsyncMigrationBase
{
private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly ILanguageService _languageService;
private readonly IContentTypeService _contentTypeService;
private readonly IMediaTypeService _mediaTypeService;
private readonly IDataTypeService _dataTypeService;
private readonly ICoreScopeProvider _coreScopeProvider;
private readonly SingleBlockListProcessor _singleBlockListProcessor;
private readonly IJsonSerializer _jsonSerializer;
private readonly SingleBlockListConfigurationCache _blockListConfigurationCache;
private readonly IBlockEditorElementTypeCache _elementTypeCache;
private readonly AppCaches _appCaches;
private readonly ILogger<MigrateSingleBlockList> _logger;
private readonly IDataValueEditor _dummySingleBlockValueEditor;
public MigrateSingleBlockList(
IMigrationContext context,
IUmbracoContextFactory umbracoContextFactory,
ILanguageService languageService,
IContentTypeService contentTypeService,
IMediaTypeService mediaTypeService,
IDataTypeService dataTypeService,
ILogger<MigrateSingleBlockList> logger,
ICoreScopeProvider coreScopeProvider,
SingleBlockListProcessor singleBlockListProcessor,
IJsonSerializer jsonSerializer,
SingleBlockListConfigurationCache blockListConfigurationCache,
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory,
IBlockEditorElementTypeCache elementTypeCache,
AppCaches appCaches)
: base(context)
{
_umbracoContextFactory = umbracoContextFactory;
_languageService = languageService;
_contentTypeService = contentTypeService;
_mediaTypeService = mediaTypeService;
_dataTypeService = dataTypeService;
_logger = logger;
_coreScopeProvider = coreScopeProvider;
_singleBlockListProcessor = singleBlockListProcessor;
_jsonSerializer = jsonSerializer;
_blockListConfigurationCache = blockListConfigurationCache;
_elementTypeCache = elementTypeCache;
_appCaches = appCaches;
_dummySingleBlockValueEditor = new SingleBlockPropertyEditor(dataValueEditorFactory, jsonSerializer, ioHelper, blockValuePropertyIndexValueFactory).GetValueEditor();
}
protected override async Task MigrateAsync()
{
// gets filled by all registered ITypedSingleBlockListProcessor
IEnumerable<string> propertyEditorAliases = _singleBlockListProcessor.GetSupportedPropertyEditorAliases();
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
var languagesById = (await _languageService.GetAllAsync())
.ToDictionary(language => language.Id);
IEnumerable<IContentType> allContentTypes = _contentTypeService.GetAll();
IEnumerable<IPropertyType> contentPropertyTypes = allContentTypes
.SelectMany(ct => ct.PropertyTypes);
IMediaType[] allMediaTypes = _mediaTypeService.GetAll().ToArray();
IEnumerable<IPropertyType> mediaPropertyTypes = allMediaTypes
.SelectMany(ct => ct.PropertyTypes);
// get all relevantPropertyTypes
var relevantPropertyEditors =
contentPropertyTypes.Concat(mediaPropertyTypes).DistinctBy(pt => pt.Id)
.Where(pt => propertyEditorAliases.Contains(pt.PropertyEditorAlias))
.GroupBy(pt => pt.PropertyEditorAlias)
.ToDictionary(group => group.Key, group => group.ToArray());
// populate the cache to limit amount of db locks in recursion logic.
var blockListsConfiguredAsSingleCount = await _blockListConfigurationCache.Populate();
if (blockListsConfiguredAsSingleCount == 0)
{
_logger.LogInformation(
"No blocklist were configured as single, nothing to do.");
return;
}
_logger.LogInformation(
"Found {blockListsConfiguredAsSingleCount} number of blockListConfigurations with UseSingleBlockMode set to true",
blockListsConfiguredAsSingleCount);
// we want to batch actual update calls to the database, so we are grouping them by propertyEditorAlias
// and again by propertyType(dataType).
var updateItemsByPropertyEditorAlias = new Dictionary<string, Dictionary<IPropertyType, List<UpdateItem>>>();
// For each propertyEditor, collect and process all propertyTypes and their propertyData
foreach (var propertyEditorAlias in propertyEditorAliases)
{
if (relevantPropertyEditors.TryGetValue(propertyEditorAlias, out IPropertyType[]? propertyTypes) is false)
{
continue;
}
_logger.LogInformation(
"Migration starting for all properties of type: {propertyEditorAlias}",
propertyEditorAlias);
Dictionary<IPropertyType, List<UpdateItem>> updateItemsByPropertyType = await ProcessPropertyTypesAsync(propertyTypes, languagesById);
if (updateItemsByPropertyType.Count < 1)
{
_logger.LogInformation(
"No properties have been found to migrate for {propertyEditorAlias}",
propertyEditorAlias);
return;
}
updateItemsByPropertyEditorAlias[propertyEditorAlias] = updateItemsByPropertyType;
}
// update the configuration of all propertyTypes
var singleBlockListDataTypesIds = _blockListConfigurationCache.CachedDataTypes.ToList().Select(type => type.Id).ToList();
string updateSql = $@"
UPDATE umbracoDataType
SET propertyEditorAlias = '{Constants.PropertyEditors.Aliases.SingleBlock}',
propertyEditorUiAlias = 'Umb.PropertyEditorUi.SingleBlock'
WHERE nodeId IN (@0)";
await Database.ExecuteAsync(updateSql, singleBlockListDataTypesIds);
// we need to clear the elementTypeCache so the second part of the migration can work with the update dataTypes
// and also the isolated/runtime Caches as that is what its build from in the default implementation
_elementTypeCache.ClearAll();
_appCaches.IsolatedCaches.ClearAllCaches();
_appCaches.RuntimeCache.Clear();
RebuildCache = true;
// now that we have updated the configuration of all propertyTypes, we can save the updated propertyTypes
foreach (string propertyEditorAlias in updateItemsByPropertyEditorAlias.Keys)
{
if (await SavePropertyTypes(updateItemsByPropertyEditorAlias[propertyEditorAlias]))
{
_logger.LogInformation(
"Migration succeeded for all properties of type: {propertyEditorAlias}",
propertyEditorAlias);
}
else
{
_logger.LogError(
"Migration failed for one or more properties of type: {propertyEditorAlias}",
propertyEditorAlias);
}
}
}
private async Task<Dictionary<IPropertyType, List<UpdateItem>>> ProcessPropertyTypesAsync(IPropertyType[] propertyTypes, IDictionary<int, ILanguage> languagesById)
{
var updateItemsByPropertyType = new Dictionary<IPropertyType, List<UpdateItem>>();
foreach (IPropertyType propertyType in propertyTypes)
{
// make sure the passed in data is valid and can be processed
IDataType dataType = await _dataTypeService.GetAsync(propertyType.DataTypeKey)
?? throw new InvalidOperationException("The data type could not be fetched.");
IDataValueEditor valueEditor = dataType.Editor?.GetValueEditor()
?? throw new InvalidOperationException(
"The data type value editor could not be obtained.");
// fetch all the propertyData for the current propertyType
Sql<ISqlContext> sql = Sql()
.Select<PropertyDataDto>()
.From<PropertyDataDto>()
.InnerJoin<ContentVersionDto>()
.On<PropertyDataDto, ContentVersionDto>((propertyData, contentVersion) =>
propertyData.VersionId == contentVersion.Id)
.LeftJoin<DocumentVersionDto>()
.On<ContentVersionDto, DocumentVersionDto>((contentVersion, documentVersion) =>
contentVersion.Id == documentVersion.Id)
.Where<PropertyDataDto, ContentVersionDto, DocumentVersionDto>((propertyData, contentVersion, documentVersion) =>
(contentVersion.Current == true || documentVersion.Published == true)
&& propertyData.PropertyTypeId == propertyType.Id);
List<PropertyDataDto> propertyDataDtos = await Database.FetchAsync<PropertyDataDto>(sql);
if (propertyDataDtos.Count < 1)
{
continue;
}
var updateItems = new List<UpdateItem>();
// process all the propertyData
// if none of the processors modify the value, the propertyData is skipped from being saved.
foreach (PropertyDataDto propertyDataDto in propertyDataDtos)
{
if (ProcessPropertyDataDto(propertyDataDto, propertyType, languagesById, valueEditor, out UpdateItem? updateItem) is false)
{
continue;
}
updateItems.Add(updateItem!);
}
updateItemsByPropertyType[propertyType] = updateItems;
}
return updateItemsByPropertyType;
}
private async Task<bool> SavePropertyTypes(IDictionary<IPropertyType, List<UpdateItem>> propertyTypes)
{
foreach (IPropertyType propertyType in propertyTypes.Keys)
{
// The dataType and valueEditor should be constructed as we have done this before, but we hate null values.
IDataType dataType = await _dataTypeService.GetAsync(propertyType.DataTypeKey)
?? throw new InvalidOperationException("The data type could not be fetched.");
IDataValueEditor updatedValueEditor = dataType.Editor?.GetValueEditor()
?? throw new InvalidOperationException(
"The data type value editor could not be obtained.");
// batch by datatype
var propertyDataDtos = propertyTypes[propertyType].Select(item => item.PropertyDataDto).ToList();
var updateBatch = propertyDataDtos.Select(propertyDataDto =>
UpdateBatch.For(propertyDataDto, Database.StartSnapshot(propertyDataDto))).ToList();
var updatesToSkip = new ConcurrentBag<UpdateBatch<PropertyDataDto>>();
var progress = 0;
void HandleUpdateBatch(UpdateBatch<PropertyDataDto> update)
{
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
progress++;
if (progress % 100 == 0)
{
_logger.LogInformation(" - finíshed {progress} of {total} properties", progress, updateBatch.Count);
}
PropertyDataDto propertyDataDto = update.Poco;
if (FinalizeUpdateItem(propertyTypes[propertyType].First(item => Equals(item.PropertyDataDto, update.Poco)), updatedValueEditor) is false)
{
updatesToSkip.Add(update);
}
}
if (DatabaseType == DatabaseType.SQLite)
{
// SQLite locks up if we run the migration in parallel, so... let's not.
foreach (UpdateBatch<PropertyDataDto> update in updateBatch)
{
HandleUpdateBatch(update);
}
}
else
{
Parallel.ForEachAsync(updateBatch, async (update, token) =>
{
//Foreach here, but we need to suppress the flow before each task, but not the actuall await of the task
Task task;
using (ExecutionContext.SuppressFlow())
{
task = Task.Run(
() =>
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
scope.Complete();
HandleUpdateBatch(update);
},
token);
}
await task;
}).GetAwaiter().GetResult();
}
updateBatch.RemoveAll(updatesToSkip.Contains);
if (updateBatch.Any() is false)
{
_logger.LogDebug(" - no properties to convert, continuing");
continue;
}
_logger.LogInformation(" - {totalConverted} properties converted, saving...", updateBatch.Count);
var result = Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 });
if (result != updateBatch.Count)
{
throw new InvalidOperationException(
$"The database batch update was supposed to update {updateBatch.Count} property DTO entries, but it updated {result} entries.");
}
_logger.LogDebug(
"Migration completed for property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias}, editor alias: {propertyTypeEditorAlias}) - {updateCount} property DTO entries updated.",
propertyType.Name,
propertyType.Id,
propertyType.Alias,
propertyType.PropertyEditorAlias,
result);
}
return true;
}
private bool ProcessPropertyDataDto(
PropertyDataDto propertyDataDto,
IPropertyType propertyType,
IDictionary<int, ILanguage> languagesById,
IDataValueEditor valueEditor,
out UpdateItem? updateItem)
{
// NOTE: some old property data DTOs can have variance defined, even if the property type no longer varies
var culture = propertyType.VariesByCulture()
&& propertyDataDto.LanguageId.HasValue
&& languagesById.TryGetValue(propertyDataDto.LanguageId.Value, out ILanguage? language)
? language.IsoCode
: null;
if (culture is null && propertyType.VariesByCulture())
{
// if we end up here, the property DTO is bound to a language that no longer exists. this is an error scenario,
// and we can't really handle it in any other way than logging; in all likelihood this is an old property version,
// and it won't cause any runtime issues
_logger.LogWarning(
" - property data with id: {propertyDataId} references a language that does not exist - language id: {languageId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})",
propertyDataDto.Id,
propertyDataDto.LanguageId,
propertyType.Name,
propertyType.Id,
propertyType.Alias);
updateItem = null;
return false;
}
// create a fake property to be able to get a typed value and run it trough the processors.
var segment = propertyType.VariesBySegment() ? propertyDataDto.Segment : null;
var property = new Property(propertyType);
property.SetValue(propertyDataDto.Value, culture, segment);
var toEditorValue = valueEditor.ToEditor(property, culture, segment);
if (TryTransformValue(toEditorValue, property, out var updatedValue) is false)
{
_logger.LogDebug(
" - skipping as no processor modified the data for property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})",
propertyDataDto.Id,
propertyType.Name,
propertyType.Id,
propertyType.Alias);
updateItem = null;
return false;
}
updateItem = new UpdateItem(propertyDataDto, propertyType, updatedValue);
return true;
}
/// <summary>
/// Takes the updated value that was instanced from the db value by the old ValueEditors
/// And runs it through the updated ValueEditors and sets it on the PropertyDataDto
/// </summary>
private bool FinalizeUpdateItem(UpdateItem updateItem, IDataValueEditor updatedValueEditor)
{
var editorValue = _jsonSerializer.Serialize(updateItem.UpdatedValue);
var dbValue = updateItem.UpdatedValue is SingleBlockValue
? _dummySingleBlockValueEditor.FromEditor(new ContentPropertyData(editorValue, null), null)
: updatedValueEditor.FromEditor(new ContentPropertyData(editorValue, null), null);
if (dbValue is not string stringValue || stringValue.DetectIsJson() is false)
{
_logger.LogWarning(
" - value editor did not yield a valid JSON string as FromEditor value property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})",
updateItem.PropertyDataDto.Id,
updateItem.PropertyType.Name,
updateItem.PropertyType.Id,
updateItem.PropertyType.Alias);
return false;
}
updateItem.PropertyDataDto.TextValue = stringValue;
return true;
}
/// <summary>
/// If the value is a BlockListValue, and its datatype is configured as single
/// We also need to convert the outer BlockListValue to a SingleBlockValue
/// Either way, we need to run the value through the processors to possibly update nested values
/// </summary>
private bool TryTransformValue(object? toEditorValue, Property property, out object? value)
{
bool hasChanged = _singleBlockListProcessor.ProcessToEditorValue(toEditorValue);
if (toEditorValue is BlockListValue blockListValue
&& _blockListConfigurationCache.IsPropertyEditorBlockListConfiguredAsSingle(property.PropertyType.DataTypeKey))
{
value = _singleBlockListProcessor.ConvertBlockListToSingleBlock(blockListValue);
return true;
}
value = toEditorValue;
return hasChanged;
}
private class UpdateItem
{
public UpdateItem(PropertyDataDto propertyDataDto, IPropertyType propertyType, object? updatedValue)
{
PropertyDataDto = propertyDataDto;
PropertyType = propertyType;
UpdatedValue = updatedValue;
}
public object? UpdatedValue { get; set; }
public PropertyDataDto PropertyDataDto { get; set; }
public IPropertyType PropertyType { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using Umbraco.Cms.Core.Models.Blocks;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
public interface ITypedSingleBlockListProcessor
{
/// <summary>
/// The type of the propertyEditor expects to receive as a value to process
/// </summary>
public Type PropertyEditorValueType { get; }
/// <summary>
/// The property (data)editor aliases that this processor supports, as defined on their DataEditor attributes
/// </summary>
public IEnumerable<string> PropertyEditorAliases { get; }
/// <summary>
/// object?: the editorValue being processed
/// Func<object?, bool>: the function that will be called when nested content is detected
/// </summary>
public Func<object?, Func<object?, bool>, Func<BlockListValue, object>, bool> Process { get; }
}

View File

@@ -0,0 +1,18 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
internal class MigrateSingleBlockListComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSingleton<ITypedSingleBlockListProcessor, SingleBlockListBlockListProcessor>();
builder.Services.AddSingleton<ITypedSingleBlockListProcessor, SingleBlockListBlockGridProcessor>();
builder.Services.AddSingleton<ITypedSingleBlockListProcessor, SingleBlockListRteProcessor>();
builder.Services.AddSingleton<SingleBlockListProcessor>();
builder.Services.AddSingleton<SingleBlockListConfigurationCache>();
}
}

View File

@@ -0,0 +1,41 @@
using Umbraco.Cms.Core.Models.Blocks;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
internal abstract class SingleBlockBlockProcessorBase
{
private readonly SingleBlockListConfigurationCache _blockListConfigurationCache;
public SingleBlockBlockProcessorBase(
SingleBlockListConfigurationCache blockListConfigurationCache)
{
_blockListConfigurationCache = blockListConfigurationCache;
}
protected bool ProcessBlockItemDataValues(
BlockItemData blockItemData,
Func<object?, bool> processNested,
Func<BlockListValue, object> processOuterValue)
{
var hasChanged = false;
foreach (BlockPropertyValue blockPropertyValue in blockItemData.Values)
{
if (processNested.Invoke(blockPropertyValue.Value))
{
hasChanged = true;
}
if (_blockListConfigurationCache.IsPropertyEditorBlockListConfiguredAsSingle(
blockPropertyValue.PropertyType!.DataTypeKey)
&& blockPropertyValue.Value is BlockListValue blockListValue)
{
blockPropertyValue.Value = processOuterValue.Invoke(blockListValue);
hasChanged = true;
}
}
return hasChanged;
}
}

View File

@@ -0,0 +1,50 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Blocks;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
internal class SingleBlockListBlockGridProcessor : SingleBlockBlockProcessorBase, ITypedSingleBlockListProcessor
{
public SingleBlockListBlockGridProcessor(SingleBlockListConfigurationCache blockListConfigurationCache)
: base(blockListConfigurationCache)
{
}
public Type PropertyEditorValueType => typeof(BlockGridValue);
public IEnumerable<string> PropertyEditorAliases => [Constants.PropertyEditors.Aliases.BlockGrid];
public Func<object?, Func<object?, bool>,Func<BlockListValue,object>, bool> Process => ProcessBlocks;
private bool ProcessBlocks(
object? value,
Func<object?, bool> processNested,
Func<BlockListValue, object> processOuterValue)
{
if (value is not BlockGridValue blockValue)
{
return false;
}
bool hasChanged = false;
foreach (BlockItemData contentData in blockValue.ContentData)
{
if (ProcessBlockItemDataValues(contentData, processNested, processOuterValue))
{
hasChanged = true;
}
}
foreach (BlockItemData settingsData in blockValue.SettingsData)
{
if (ProcessBlockItemDataValues(settingsData, processNested, processOuterValue))
{
hasChanged = true;
}
}
return hasChanged;
}
}

View File

@@ -0,0 +1,52 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Blocks;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
internal class SingleBlockListBlockListProcessor : SingleBlockBlockProcessorBase, ITypedSingleBlockListProcessor
{
public SingleBlockListBlockListProcessor(
SingleBlockListConfigurationCache blockListConfigurationCache)
: base(blockListConfigurationCache)
{
}
public Type PropertyEditorValueType => typeof(BlockListValue);
public IEnumerable<string> PropertyEditorAliases => [Constants.PropertyEditors.Aliases.BlockList];
public Func<object?, Func<object?, bool>, Func<BlockListValue, object>, bool> Process => ProcessBlocks;
private bool ProcessBlocks(
object? value,
Func<object?, bool> processNested,
Func<BlockListValue, object> processOuterValue)
{
if (value is not BlockListValue blockValue)
{
return false;
}
bool hasChanged = false;
// there might be another list inside the single list so more recursion, yeeey!
foreach (BlockItemData contentData in blockValue.ContentData)
{
if (ProcessBlockItemDataValues(contentData, processNested, processOuterValue))
{
hasChanged = true;
}
}
foreach (BlockItemData settingsData in blockValue.SettingsData)
{
if (ProcessBlockItemDataValues(settingsData, processNested, processOuterValue))
{
hasChanged = true;
}
}
return hasChanged;
}
}

View File

@@ -0,0 +1,52 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
/// <summary>
/// Used by the SingleBlockList Migration and its processors to avoid having to fetch (and thus lock)
/// data from the db multiple times during the migration.
/// </summary>
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
public class SingleBlockListConfigurationCache
{
private readonly IDataTypeService _dataTypeService;
private readonly List<IDataType> _singleBlockListDataTypes = new();
public SingleBlockListConfigurationCache(IDataTypeService dataTypeService)
{
_dataTypeService = dataTypeService;
}
/// <summary>
/// Populates a cache that holds all the property editor aliases that have a BlockList configuration with UseSingleBlockMode set to true.
/// </summary>
/// <returns> The number of blocklists with UseSingleBlockMode set to true.</returns>
public async Task<int> Populate()
{
IEnumerable<IDataType> blockListDataTypes =
await _dataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.BlockList);
foreach (IDataType dataType in blockListDataTypes)
{
if (dataType.ConfigurationObject is BlockListConfiguration
{
UseSingleBlockMode: true, ValidationLimit.Max: 1
})
{
_singleBlockListDataTypes.Add(dataType);
}
}
return _singleBlockListDataTypes.Count;
}
// returns whether the passed in key belongs to a blocklist with UseSingleBlockMode set to true
public bool IsPropertyEditorBlockListConfiguredAsSingle(Guid key) =>
_singleBlockListDataTypes.Any(dt => dt.Key == key);
// The list of all blocklist data types that have UseSingleBlockMode set to true
public IEnumerable<IDataType> CachedDataTypes => _singleBlockListDataTypes.AsReadOnly();
}

View File

@@ -0,0 +1,51 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Blocks;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
public class SingleBlockListProcessor
{
private readonly IEnumerable<ITypedSingleBlockListProcessor> _processors;
public SingleBlockListProcessor(IEnumerable<ITypedSingleBlockListProcessor> processors) => _processors = processors;
public IEnumerable<string> GetSupportedPropertyEditorAliases() =>
_processors.SelectMany(p => p.PropertyEditorAliases);
/// <summary>
/// The entry point of the recursive conversion
/// Find the first processor that can handle the value and call it's Process method
/// </summary>
/// <returns>Whether the value was changed</returns>
public bool ProcessToEditorValue(object? editorValue)
{
ITypedSingleBlockListProcessor? processor =
_processors.FirstOrDefault(p => p.PropertyEditorValueType == editorValue?.GetType());
return processor is not null && processor.Process.Invoke(editorValue, ProcessToEditorValue, ConvertBlockListToSingleBlock);
}
/// <summary>
/// Updates and returns the passed in BlockListValue to a SingleBlockValue
/// Should only be called by a core processor once a BlockListValue has been found that is configured in single block mode.
/// </summary>
public BlockValue ConvertBlockListToSingleBlock(BlockListValue blockListValue)
{
IBlockLayoutItem blockListLayoutItem = blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].First();
var singleBlockLayoutItem = new SingleBlockLayoutItem
{
ContentKey = blockListLayoutItem.ContentKey,
SettingsKey = blockListLayoutItem.SettingsKey,
};
var singleBlockValue = new SingleBlockValue(singleBlockLayoutItem)
{
ContentData = blockListValue.ContentData,
SettingsData = blockListValue.SettingsData,
Expose = blockListValue.Expose,
};
return singleBlockValue;
}
}

View File

@@ -0,0 +1,58 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Blocks;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
[Obsolete("Will be removed in V22")] // available in v17, activated in v18 migration needs to work on LTS to LTS 17=>21
internal class SingleBlockListRteProcessor : SingleBlockBlockProcessorBase, ITypedSingleBlockListProcessor
{
public SingleBlockListRteProcessor(SingleBlockListConfigurationCache blockListConfigurationCache)
: base(blockListConfigurationCache)
{
}
public Type PropertyEditorValueType => typeof(RichTextEditorValue);
public IEnumerable<string> PropertyEditorAliases =>
[
"Umbraco.TinyMCE", Constants.PropertyEditors.Aliases.RichText
];
public Func<object?, Func<object?, bool>,Func<BlockListValue,object>, bool> Process => ProcessRichText;
public bool ProcessRichText(
object? value,
Func<object?, bool> processNested,
Func<BlockListValue, object> processOuterValue)
{
if (value is not RichTextEditorValue richTextValue)
{
return false;
}
var hasChanged = false;
if (richTextValue.Blocks is null)
{
return hasChanged;
}
foreach (BlockItemData contentData in richTextValue.Blocks.ContentData)
{
if (ProcessBlockItemDataValues(contentData, processNested, processOuterValue))
{
hasChanged = true;
}
}
foreach (BlockItemData settingsData in richTextValue.Blocks.SettingsData)
{
if (ProcessBlockItemDataValues(settingsData, processNested, processOuterValue))
{
hasChanged = true;
}
}
return hasChanged;
}
}