I ended copying (using) the code from Umbraco migration Umbraco-CMS/src/Umbraco.Infrastructure/Migrations/Upgrade at main · umbraco/Umbraco-CMS · GitHub and running in my local setup to fix the issue before upgrading.
Something like this and using Composer to use the service.
This code below is not complete, is just an idea on how to achieve similar code for Umbraco own migration.
public class PropertyDataMigrationHandler : INotificationAsyncHandler<UmbracoApplicationStartedNotification>
{
private readonly ILogger<PropertyDataMigrationHandler> _logger;
private readonly IScopeProvider _scopeProvider;
private readonly IDataTypeService _dataTypeService;
private readonly IContentTypeService _contentTypeService;
private readonly ILocalizationService _localizationService;
private readonly PropertyEditorCollection _propertyEditors;
public PropertyDataMigrationHandler(
ILogger<PropertyDataMigrationHandler> logger,
IScopeProvider scopeProvider,
IDataTypeService dataTypeService,
IContentTypeService contentTypeService,
ILocalizationService localizationService,
PropertyEditorCollection propertyEditors)
{
_logger = logger;
_scopeProvider = scopeProvider;
_dataTypeService = dataTypeService;
_contentTypeService = contentTypeService;
_localizationService = localizationService;
_propertyEditors = propertyEditors;
}
//public void Handle(UmbracoApplicationStartedNotification notification)
//{
// throw new NotImplementedException();
//}
public async Task HandleAsync(UmbracoApplicationStartedNotification notification, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Starting property data migration process...");
using var scope = _scopeProvider.CreateScope(autoComplete: true);
var database = scope.Database;
// Step 1: Process Property Types (similar to ProcessPropertyTypes in Umbraco migration)
var propertyTypes = await ProcessPropertyTypesAsync(database);
if (!propertyTypes.Any())
{
_logger.LogInformation("No property types requiring migration found.");
return;
}
// Step 2: Get languages for multi-language support
var languagesById = GetLanguagesById();
// Step 3: Process Property Data (similar to ProcessPropertyDataDto)
await ProcessPropertyDataAsync(database, propertyTypes, languagesById);
_logger.LogInformation("Property data migration completed successfully.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during property data migration: {Message}", ex.Message);
throw;
}
}
/// <summary>
/// Fetches property types that need processing, similar to ProcessPropertyTypes in Umbraco migration
/// </summary>
private async Task<List<PropertyTypeInfo>> ProcessPropertyTypesAsync(IUmbracoDatabase database)
{
const string sql = @"
SELECT DISTINCT
pt.id as PropertyTypeId,
pt.Alias as PropertyTypeAlias,
pt.dataTypeId,
dt.propertyEditorAlias,
dt.config
FROM cmsPropertyType pt
INNER JOIN umbracoDataType dt ON pt.dataTypeId = dt.nodeId
WHERE dt.propertyEditorAlias IN (@rteAlias, @textboxAlias, @textareaAlias)
AND EXISTS (
SELECT 1 FROM umbracoPropertyData pd
WHERE pd.propertyTypeId = pt.id
AND pd.textValue IS NOT NULL
AND pd.textValue != ''
)";
var propertyTypes = await database.FetchAsync<PropertyTypeInfo>(sql, new
{
rteAlias = Constants.PropertyEditors.Aliases.RichText,
textboxAlias = Constants.PropertyEditors.Aliases.TextBox,
textareaAlias = Constants.PropertyEditors.Aliases.TextArea,
});
_logger.LogInformation("Found {Count} property types requiring migration", propertyTypes.Count);
return propertyTypes;
}
/// <summary>
/// Processes property data DTOs, similar to ProcessPropertyDataDto in Umbraco migration
/// </summary>
private async Task ProcessPropertyDataAsync(
IUmbracoDatabase database,
List<PropertyTypeInfo> propertyTypes,
Dictionary<int, ILanguage> languagesById)
{
const string propertyDataSql = @"
SELECT pd.id, pd.propertyTypeId, pd.languageId,
pd.segment, pd.textValue, pd.versionId,
pd.intValue, pd.decimalValue, pd.dateValue, pd.varcharValue
FROM umbracoPropertyData pd
WHERE pd.propertyTypeId IN (@propertyTypeIds)
AND pd.textValue IS NOT NULL
AND pd.textValue != ''
ORDER BY pd.id";
var propertyTypeIds = propertyTypes.Select(x => x.PropertyTypeId).ToArray();
var propertyDataList = await database.FetchAsync<PropertyDataDto>(propertyDataSql, new { propertyTypeIds });
_logger.LogInformation("Processing {Count} property data records", propertyDataList.Count);
var batchSize = 1000;
var totalBatches = (int)Math.Ceiling((double)propertyDataList.Count / batchSize);
for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++)
{
var batch = propertyDataList
.Skip(batchIndex * batchSize)
.Take(batchSize)
.ToList();
await ProcessBatchAsync(database, batch, propertyTypes, languagesById);
_logger.LogInformation("Completed batch {Current}/{Total}", batchIndex + 1, totalBatches);
}
}
/// <summary>
/// Processes a batch of property data records
/// </summary>
private async Task ProcessBatchAsync(
IUmbracoDatabase database,
List<PropertyDataDto> batch,
List<PropertyTypeInfo> propertyTypes,
Dictionary<int, ILanguage> languagesById)
{
var propertyTypesByIds = propertyTypes.ToDictionary(x => x.PropertyTypeId);
var updates = new List<PropertyDataDto>();
foreach (var propertyData in batch)
{
try
{
if (!propertyTypesByIds.TryGetValue(propertyData.PropertyTypeId, out var propertyType))
continue;
// Get the appropriate value editor
if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor))
continue;
var valueEditor = propertyEditor.GetValueEditor();
// Process the editor value (similar to ProcessToEditorValue)
var processedValue = ProcessToEditorValue(propertyData.TextValue, propertyType.PropertyEditorAlias);
if (processedValue != propertyData.TextValue)
{
propertyData.TextValue = processedValue;
updates.Add(propertyData);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing property data ID {Id}: {Message}",
propertyData.Id, ex.Message);
// Continue processing other records
}
}
if (updates.Any())
{
await UpdatePropertyDataAsync(database, updates);
_logger.LogInformation("Updated {Count} property data records in batch", updates.Count);
}
}
/// <summary>
/// Processes editor values, similar to ProcessToEditorValue in LocalLinkProcessorForFaultyLinks
/// Fixes the System.ArgumentException for empty oldValue
/// </summary>
private string ProcessToEditorValue(string editorValue, string propertyEditorAlias)
{
if (string.IsNullOrEmpty(editorValue))
return editorValue;
try
{
switch (propertyEditorAlias)
{
case Constants.PropertyEditors.Aliases.RichText:
return ProcessRichText(editorValue);
case Constants.PropertyEditors.Aliases.TextBox:
case Constants.PropertyEditors.Aliases.TextArea:
return ProcessStringValue(editorValue);
default:
return editorValue;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error processing editor value for {PropertyEditorAlias}: {Message}",
propertyEditorAlias, ex.Message);
return editorValue; // Return original value if processing fails
}
}
/// <summary>
/// Processes rich text content, handling nested processing
/// </summary>
private string ProcessRichText(string value)
{
if (string.IsNullOrEmpty(value))
return value;
// This is a simplified version - you may need to implement more complex RTE processing
// based on your specific requirements and the original LocalLinkRteProcessor logic
return ProcessStringValue(value);
}
internal static readonly Regex FaultyHrefPattern = new(
@"<a (?<faultyHref>href=['""] ?(?<typeAttribute> type=*?['""][^'""]*?['""] )?(?<localLink>\/{localLink:[a-fA-F0-9-]+}['""])).*?>",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
private const string LocalLinkLocation = "__LOCALLINKLOCATION__";
private const string TypeAttributeLocation = "__TYPEATTRIBUTELOCATION__";
/// <summary>
/// Processes string values, fixing the ArgumentException for empty oldValue
/// This addresses the main error you encountered
/// </summary>
private string ProcessStringValue(string input)
{
if (string.IsNullOrEmpty(input))
return input;
MatchCollection faultyTags = FaultyHrefPattern.Matches(input);
if (faultyTags.Count != 0)
{
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);
}
_logger.LogInformation("Processing {Count} faulty href tags in input", faultyTags.Count);
}
try
{
// Define patterns that need to be replaced
// This is where you'd implement your specific link processing logic
var replacements = new Dictionary<string, string>
{
// Example replacements - customize based on your needs
{ "/{localLink:", "/localLink:" },
{ "localLink:umb://", "umb://" },
// Add more patterns as needed
};
string result = input;
foreach (var replacement in replacements)
{
// Fix for the ArgumentException: check if oldValue is not empty
if (!string.IsNullOrEmpty(replacement.Key) && result.Contains(replacement.Key))
{
result = result.Replace(replacement.Key, replacement.Value);
}
}
// Additional processing for malformed local links
result = ProcessMalformedLocalLinks(result);
return result;
}
catch (ArgumentException ex) when (ex.ParamName == "oldValue")
{
_logger.LogWarning("Attempted to replace empty string in input: {Input}", input);
return input; // Return original input if replacement fails
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing string value: {Input}", input);
return input; // Return original input if processing fails
}
}
/// <summary>
/// Processes malformed local links that might cause empty string replacement issues
/// </summary>
private string ProcessMalformedLocalLinks(string input)
{
if (string.IsNullOrEmpty(input))
return input;
// Handle cases where there might be empty or malformed link patterns
// that could cause the empty string replacement error
// Remove empty local link patterns
input = System.Text.RegularExpressions.Regex.Replace(
input,
@"/{localLink:}\s*",
"",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
// Clean up malformed umb:// references
input = System.Text.RegularExpressions.Regex.Replace(
input,
@"umb://\s*}",
"}",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return input;
}
/// <summary>
/// Updates property data in the database
/// </summary>
private async Task UpdatePropertyDataAsync(IUmbracoDatabase database, List<PropertyDataDto> updates)
{
const string updateSql = @"
UPDATE umbracoPropertyData
SET textValue = @textValue
WHERE id = @id";
foreach (var update in updates)
{
await database.ExecuteAsync(updateSql, new
{
textValue = update.TextValue,
id = update.Id
});
}
}
/// <summary>
/// Gets languages by ID for multi-language support
/// </summary>
private Dictionary<int, ILanguage> GetLanguagesById()
{
var languages = _localizationService.GetAllLanguages();
return languages.ToDictionary(x => x.Id, x => x);
}
}
/// <summary>
/// DTO for property type information
/// </summary>
public class PropertyTypeInfo
{
public int PropertyTypeId { get; set; }
public string PropertyTypeAlias { get; set; }
public int DataTypeId { get; set; }
public string PropertyEditorAlias { get; set; }
public string Config { get; set; }
}
/// <summary>
/// DTO for property data - matches umbracoPropertyData table structure
/// </summary>
public class PropertyDataDto
{
public int Id { get; set; }
public int VersionId { get; set; }
public int PropertyTypeId { get; set; }
public int? LanguageId { get; set; }
public string? Segment { get; set; }
public int? IntValue { get; set; }
public decimal? DecimalValue { get; set; }
public DateTime? DateValue { get; set; }
public string? VarcharValue { get; set; }
public string? TextValue { get; set; }
}