Fix mistakes in 15.0.0 migrations (#17814)
* Fix ConvertLocalLinks migration and add a new migration in case the old one has already run * RebuildCache * Clear cache means clear ALL caches * Fix Block Markup recursion * Fix Unittest mock constructor
This commit is contained in:
@@ -47,6 +47,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
|
||||
private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder;
|
||||
private readonly IKeyValueService _keyValueService;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly AppCaches _appCaches;
|
||||
private readonly DistributedCache _distributedCache;
|
||||
private readonly IScopeAccessor _scopeAccessor;
|
||||
private readonly ICoreScopeProvider _scopeProvider;
|
||||
@@ -62,7 +63,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
|
||||
IDatabaseCacheRebuilder databaseCacheRebuilder,
|
||||
DistributedCache distributedCache,
|
||||
IKeyValueService keyValueService,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
AppCaches appCaches)
|
||||
{
|
||||
_scopeProvider = scopeProvider;
|
||||
_scopeAccessor = scopeAccessor;
|
||||
@@ -72,10 +74,36 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
|
||||
_databaseCacheRebuilder = databaseCacheRebuilder;
|
||||
_keyValueService = keyValueService;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_appCaches = appCaches;
|
||||
_distributedCache = distributedCache;
|
||||
_logger = _loggerFactory.CreateLogger<MigrationPlanExecutor>();
|
||||
}
|
||||
|
||||
[Obsolete("Use the non obsoleted constructor instead. Scheduled for removal in v17")]
|
||||
public MigrationPlanExecutor(
|
||||
ICoreScopeProvider scopeProvider,
|
||||
IScopeAccessor scopeAccessor,
|
||||
ILoggerFactory loggerFactory,
|
||||
IMigrationBuilder migrationBuilder,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
IDatabaseCacheRebuilder databaseCacheRebuilder,
|
||||
DistributedCache distributedCache,
|
||||
IKeyValueService keyValueService,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
: this(
|
||||
scopeProvider,
|
||||
scopeAccessor,
|
||||
loggerFactory,
|
||||
migrationBuilder,
|
||||
databaseFactory,
|
||||
databaseCacheRebuilder,
|
||||
distributedCache,
|
||||
keyValueService,
|
||||
serviceScopeFactory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<AppCaches>())
|
||||
{
|
||||
}
|
||||
|
||||
public string Execute(MigrationPlan plan, string fromState) => ExecutePlan(plan, fromState).FinalState;
|
||||
|
||||
/// <summary>
|
||||
@@ -303,6 +331,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
|
||||
|
||||
private void RebuildCache()
|
||||
{
|
||||
_appCaches.RuntimeCache.Clear();
|
||||
_appCaches.IsolatedCaches.ClearAllCaches();
|
||||
_databaseCacheRebuilder.Rebuild();
|
||||
_distributedCache.RefreshAllPublishedSnapshot();
|
||||
}
|
||||
|
||||
@@ -105,5 +105,6 @@ public class UmbracoPlan : MigrationPlan
|
||||
To<V_15_0_0.ConvertRichTextEditorProperties>("{37875E80-5CDD-42FF-A21A-7D4E3E23E0ED}");
|
||||
To<V_15_0_0.ConvertLocalLinks>("{42E44F9E-7262-4269-922D-7310CB48E724}");
|
||||
To<V_15_1_0.RebuildCacheMigration>("{7B51B4DE-5574-4484-993E-05D12D9ED703}");
|
||||
To<V_15_1_0.FixConvertLocalLinks>("{F3D3EF46-1B1F-47DB-B437-7D573EEDEB98}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
@@ -26,6 +28,34 @@ public class ConvertLocalLinks : MigrationBase
|
||||
private readonly LocalLinkProcessor _localLinkProcessor;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly LocalLinkMigrationTracker _linkMigrationTracker;
|
||||
|
||||
[Obsolete("Use non obsoleted contructor instead")]
|
||||
public ConvertLocalLinks(
|
||||
IMigrationContext context,
|
||||
IUmbracoContextFactory umbracoContextFactory,
|
||||
IContentTypeService contentTypeService,
|
||||
ILogger<ConvertLocalLinks> logger,
|
||||
IDataTypeService dataTypeService,
|
||||
ILanguageService languageService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
LocalLinkProcessor localLinkProcessor,
|
||||
IMediaTypeService mediaTypeService,
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
LocalLinkMigrationTracker linkMigrationTracker)
|
||||
: base(context)
|
||||
{
|
||||
_umbracoContextFactory = umbracoContextFactory;
|
||||
_contentTypeService = contentTypeService;
|
||||
_logger = logger;
|
||||
_dataTypeService = dataTypeService;
|
||||
_languageService = languageService;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_localLinkProcessor = localLinkProcessor;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_linkMigrationTracker = linkMigrationTracker;
|
||||
}
|
||||
|
||||
public ConvertLocalLinks(
|
||||
IMigrationContext context,
|
||||
@@ -38,17 +68,19 @@ public class ConvertLocalLinks : MigrationBase
|
||||
LocalLinkProcessor localLinkProcessor,
|
||||
IMediaTypeService mediaTypeService,
|
||||
ICoreScopeProvider coreScopeProvider)
|
||||
: base(context)
|
||||
: this(
|
||||
context,
|
||||
umbracoContextFactory,
|
||||
contentTypeService,
|
||||
logger,
|
||||
dataTypeService,
|
||||
languageService,
|
||||
jsonSerializer,
|
||||
localLinkProcessor,
|
||||
mediaTypeService,
|
||||
coreScopeProvider,
|
||||
StaticServiceProvider.Instance.GetRequiredService<LocalLinkMigrationTracker>())
|
||||
{
|
||||
_umbracoContextFactory = umbracoContextFactory;
|
||||
_contentTypeService = contentTypeService;
|
||||
_logger = logger;
|
||||
_dataTypeService = dataTypeService;
|
||||
_languageService = languageService;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_localLinkProcessor = localLinkProcessor;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
}
|
||||
|
||||
protected override void Migrate()
|
||||
@@ -97,6 +129,9 @@ public class ConvertLocalLinks : MigrationBase
|
||||
propertyEditorAlias);
|
||||
}
|
||||
}
|
||||
|
||||
_linkMigrationTracker.MarkFixedMigrationRan();
|
||||
RebuildCache = true;
|
||||
}
|
||||
|
||||
private bool ProcessPropertyTypes(IPropertyType[] propertyTypes, IDictionary<int, ILanguage> languagesById)
|
||||
|
||||
@@ -13,5 +13,7 @@ public class ConvertLocalLinkComposer : IComposer
|
||||
builder.Services.AddSingleton<ITypedLocalLinkProcessor, LocalLinkBlockGridProcessor>();
|
||||
builder.Services.AddSingleton<ITypedLocalLinkProcessor, LocalLinkRteProcessor>();
|
||||
builder.Services.AddSingleton<LocalLinkProcessor>();
|
||||
builder.Services.AddSingleton<LocalLinkProcessorForFaultyLinks>();
|
||||
builder.Services.AddSingleton<LocalLinkMigrationTracker>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks;
|
||||
|
||||
public class LocalLinkMigrationTracker
|
||||
{
|
||||
public bool HasFixedMigrationRun { get; private set; }
|
||||
|
||||
public void MarkFixedMigrationRan() => HasFixedMigrationRun = true;
|
||||
}
|
||||
@@ -43,8 +43,8 @@ public class LocalLinkProcessor
|
||||
string newTagHref;
|
||||
if (tag.Udi is not null)
|
||||
{
|
||||
newTagHref = $" type=\"{tag.Udi.EntityType}\" "
|
||||
+ tag.TagHref.Replace(tag.Udi.ToString(), tag.Udi.Guid.ToString());
|
||||
newTagHref = tag.TagHref.Replace(tag.Udi.ToString(), tag.Udi.Guid.ToString())
|
||||
+ $"\" type=\"{tag.Udi.EntityType}";
|
||||
}
|
||||
else if (tag.IntId is not null)
|
||||
{
|
||||
@@ -55,8 +55,8 @@ public class LocalLinkProcessor
|
||||
continue;
|
||||
}
|
||||
|
||||
newTagHref = $" type=\"{conversionResult.Value.EntityType}\" "
|
||||
+ tag.TagHref.Replace(tag.IntId.Value.ToString(), conversionResult.Value.Key.ToString());
|
||||
newTagHref = tag.TagHref.Replace(tag.IntId.Value.ToString(), conversionResult.Value.Key.ToString())
|
||||
+ $"\" type=\"{conversionResult.Value.EntityType}";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks;
|
||||
|
||||
[Obsolete("Will be removed in V18")]
|
||||
public class LocalLinkProcessorForFaultyLinks
|
||||
{
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
private readonly IEnumerable<ITypedLocalLinkProcessor> _localLinkProcessors;
|
||||
private const string LocalLinkLocation = "__LOCALLINKLOCATION__";
|
||||
private const string TypeAttributeLocation = "__TYPEATTRIBUTELOCATION__";
|
||||
|
||||
internal static readonly Regex FaultyHrefPattern = new(
|
||||
@"<a (?<faultyHref>href=['""] ?(?<typeAttribute> type=*?['""][^'""]*?['""] )?(?<localLink>\/{localLink:[a-fA-F0-9-]+}['""])).*?>",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
|
||||
public LocalLinkProcessorForFaultyLinks(
|
||||
IIdKeyMap idKeyMap,
|
||||
IEnumerable<ITypedLocalLinkProcessor> localLinkProcessors)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
_localLinkProcessors = localLinkProcessors;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetSupportedPropertyEditorAliases() =>
|
||||
_localLinkProcessors.SelectMany(p => p.PropertyEditorAliases);
|
||||
|
||||
public bool ProcessToEditorValue(object? editorValue)
|
||||
{
|
||||
ITypedLocalLinkProcessor? processor =
|
||||
_localLinkProcessors.FirstOrDefault(p => p.PropertyEditorValueType == editorValue?.GetType());
|
||||
|
||||
return processor is not null && processor.Process.Invoke(editorValue, ProcessToEditorValue, ProcessStringValue);
|
||||
}
|
||||
|
||||
public string ProcessStringValue(string input)
|
||||
{
|
||||
MatchCollection faultyTags = FaultyHrefPattern.Matches(input);
|
||||
|
||||
foreach (Match fullTag in faultyTags)
|
||||
{
|
||||
var newValue =
|
||||
fullTag.Value.Replace(fullTag.Groups["typeAttribute"].Value, LocalLinkLocation)
|
||||
.Replace(fullTag.Groups["localLink"].Value, TypeAttributeLocation)
|
||||
.Replace(LocalLinkLocation, fullTag.Groups["localLink"].Value)
|
||||
.Replace(TypeAttributeLocation, fullTag.Groups["typeAttribute"].Value);
|
||||
input = input.Replace(fullTag.Value, newValue);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private (Guid Key, string EntityType)? CreateIntBasedKeyType(int id)
|
||||
{
|
||||
// very old data, best effort replacement
|
||||
Attempt<Guid> documentAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
|
||||
if (documentAttempt.Success)
|
||||
{
|
||||
return (Key: documentAttempt.Result, EntityType: UmbracoObjectTypes.Document.ToString());
|
||||
}
|
||||
|
||||
Attempt<Guid> mediaAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media);
|
||||
if (mediaAttempt.Success)
|
||||
{
|
||||
return (Key: mediaAttempt.Result, EntityType: UmbracoObjectTypes.Media.ToString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
@@ -28,6 +29,16 @@ public class LocalLinkRteProcessor : ITypedLocalLinkProcessor
|
||||
bool hasChanged = false;
|
||||
|
||||
var newMarkup = processStringValue.Invoke(richTextValue.Markup);
|
||||
|
||||
// fix recursive hickup in ConvertRichTextEditorProperties
|
||||
newMarkup = RteBlockHelper.BlockRegex().Replace(
|
||||
newMarkup,
|
||||
match => UdiParser.TryParse(match.Groups["udi"].Value, out GuidUdi? guidUdi)
|
||||
? match.Value
|
||||
.Replace(match.Groups["attribute"].Value, "data-content-key")
|
||||
.Replace(match.Groups["udi"].Value, guidUdi.Guid.ToString("D"))
|
||||
: string.Empty);
|
||||
|
||||
if (newMarkup.Equals(richTextValue.Markup) == false)
|
||||
{
|
||||
hasChanged = true;
|
||||
@@ -53,3 +64,10 @@ public class LocalLinkRteProcessor : ITypedLocalLinkProcessor
|
||||
return hasChanged;
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Will be removed in V18")]
|
||||
public static partial class RteBlockHelper
|
||||
{
|
||||
[GeneratedRegex("<umb-rte-block.*(?<attribute>data-content-udi)=\"(?<udi>.[^\"]*)\".*<\\/umb-rte-block")]
|
||||
public static partial Regex BlockRegex();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
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_15_0_0.LocalLinks;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_1_0;
|
||||
|
||||
public class FixConvertLocalLinks : MigrationBase
|
||||
{
|
||||
private readonly IUmbracoContextFactory _umbracoContextFactory;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly ILogger<FixConvertLocalLinks> _logger;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly LocalLinkProcessorForFaultyLinks _localLinkProcessor;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly LocalLinkMigrationTracker _localLinkMigrationTracker;
|
||||
|
||||
public FixConvertLocalLinks(
|
||||
IMigrationContext context,
|
||||
IUmbracoContextFactory umbracoContextFactory,
|
||||
IContentTypeService contentTypeService,
|
||||
ILogger<FixConvertLocalLinks> logger,
|
||||
IDataTypeService dataTypeService,
|
||||
ILanguageService languageService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
LocalLinkProcessorForFaultyLinks localLinkProcessor,
|
||||
IMediaTypeService mediaTypeService,
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
LocalLinkMigrationTracker localLinkMigrationTracker)
|
||||
: base(context)
|
||||
{
|
||||
_umbracoContextFactory = umbracoContextFactory;
|
||||
_contentTypeService = contentTypeService;
|
||||
_logger = logger;
|
||||
_dataTypeService = dataTypeService;
|
||||
_languageService = languageService;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_localLinkProcessor = localLinkProcessor;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_localLinkMigrationTracker = localLinkMigrationTracker;
|
||||
}
|
||||
|
||||
protected override void Migrate()
|
||||
{
|
||||
// the original migration was fixed, we only run this if
|
||||
// this migration hits
|
||||
// and the fixed original migration has not run, so it must have run in the past
|
||||
if (_localLinkMigrationTracker.HasFixedMigrationRun)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<string> propertyEditorAliases = _localLinkProcessor.GetSupportedPropertyEditorAliases();
|
||||
|
||||
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
|
||||
var languagesById = _languageService.GetAllAsync().GetAwaiter().GetResult()
|
||||
.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);
|
||||
|
||||
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());
|
||||
|
||||
|
||||
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);
|
||||
if (ProcessPropertyTypes(propertyTypes, languagesById))
|
||||
{
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
RebuildCache = true;
|
||||
}
|
||||
|
||||
private bool ProcessPropertyTypes(IPropertyType[] propertyTypes, IDictionary<int, ILanguage> languagesById)
|
||||
{
|
||||
foreach (IPropertyType propertyType in propertyTypes)
|
||||
{
|
||||
IDataType dataType = _dataTypeService.GetAsync(propertyType.DataTypeKey).GetAwaiter().GetResult()
|
||||
?? 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 fetched.");
|
||||
|
||||
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 = Database.Fetch<PropertyDataDto>(sql);
|
||||
if (propertyDataDtos.Count < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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 (ProcessPropertyDataDto(propertyDataDto, propertyType, languagesById, valueEditor) == 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)
|
||||
{
|
||||
// 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);
|
||||
return false;
|
||||
}
|
||||
|
||||
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 (_localLinkProcessor.ProcessToEditorValue(toEditorValue) == 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);
|
||||
return false;
|
||||
}
|
||||
|
||||
var editorValue = _jsonSerializer.Serialize(toEditorValue);
|
||||
var dbValue = valueEditor.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})",
|
||||
propertyDataDto.Id,
|
||||
propertyType.Name,
|
||||
propertyType.Id,
|
||||
propertyType.Alias);
|
||||
return false;
|
||||
}
|
||||
|
||||
propertyDataDto.TextValue = stringValue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -71,13 +71,17 @@ public class MigrationPlanTests
|
||||
Mock.Of<IServerMessenger>(),
|
||||
new CacheRefresherCollection(() => Enumerable.Empty<ICacheRefresher>()));
|
||||
|
||||
var isolatedCaches = new IsolatedCaches(type => NoAppCache.Instance);
|
||||
|
||||
var appCaches = new AppCaches(Mock.Of<IAppPolicyCache>(), Mock.Of<IRequestCache>(), isolatedCaches);
|
||||
|
||||
var executor = new MigrationPlanExecutor(
|
||||
scopeProvider,
|
||||
scopeProvider,
|
||||
loggerFactory,
|
||||
migrationBuilder,
|
||||
databaseFactory,
|
||||
Mock.Of<IDatabaseCacheRebuilder>(), distributedCache, Mock.Of<IKeyValueService>(), Mock.Of<IServiceScopeFactory>());
|
||||
Mock.Of<IDatabaseCacheRebuilder>(), distributedCache, Mock.Of<IKeyValueService>(), Mock.Of<IServiceScopeFactory>(), appCaches);
|
||||
|
||||
var plan = new MigrationPlan("default")
|
||||
.From(string.Empty)
|
||||
|
||||
Reference in New Issue
Block a user