Skip to content

Instantly share code, notes, and snippets.

@jstemerdink
Created February 21, 2025 14:34
Show Gist options
  • Save jstemerdink/63272480c8a262de9cced9b8ecf8faa7 to your computer and use it in GitHub Desktop.
Save jstemerdink/63272480c8a262de9cced9b8ecf8faa7 to your computer and use it in GitHub Desktop.

Convert media type in a migrationstep

Read my blog here

using Alloy12.Models.Media;
using EPiServer.DataAbstraction.Migration;
using System.Data.Common;
using System.Data;
using System.Globalization;
using EPiServer.Data;
using Newtonsoft.Json;
using EPiServer.ServiceLocation;
namespace Alloy12.Business.Migrations
{
/// <summary>
/// Class WebpMigrationStep.
/// Implements the <see cref="MigrationStep" />
/// </summary>
/// <seealso cref="MigrationStep" />
/// <remarks>
/// <para>
/// There is no versioning handling in a migration step. It is intended to be a very specific class for specific databases.
/// It is safe to remove the migration step implementation after the changes described in it have been commited to the database.
/// </para>
/// </remarks>
public class WebpMigrationStep : MigrationStep
{
private readonly IContentRepository _contentRepository;
private readonly IContentTypeRepository _contentTypeRepository;
private readonly IContentModelUsage _contentModelUsage;
private readonly ILanguageBranchRepository _languageBranchRepository;
private readonly IPropertyDefinitionRepository _propertyDefinitionRepository;
private readonly IContentCacheRemover _contentCacheRemover;
private readonly ContentMediaResolver _contentMediaResolver;
private readonly ILogger<WebpMigrationStep> _logger;
private IDatabaseExecutor Executor { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="WebpMigrationStep"/> class.
/// </summary>
public WebpMigrationStep() : this(
ServiceLocator.Current.GetService<IContentRepository>(),
ServiceLocator.Current.GetService<IContentTypeRepository>(),
ServiceLocator.Current.GetService<IContentModelUsage>(),
ServiceLocator.Current.GetService<ILanguageBranchRepository>(),
ServiceLocator.Current.GetService<IPropertyDefinitionRepository>(),
ServiceLocator.Current.GetService<IContentCacheRemover>(),
ServiceLocator.Current.GetService<ContentMediaResolver>(),
ServiceLocator.Current.GetService<ILogger<WebpMigrationStep>>()
)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WebpMigrationStep"/> class.
/// </summary>
/// <param name="contentRepository">The content repository.</param>
/// <param name="contentTypeRepository">The content type repository.</param>
/// <param name="contentModelUsage">The content model usage.</param>
/// <param name="languageBranchRepository">The language branch repository.</param>
/// <param name="propertyDefinitionRepository">The property definition repository.</param>
/// <param name="contentCacheRemover">The content cache remover.</param>
/// <param name="contentMediaResolver">The content media resolver.</param>
/// <param name="logger">The logger.</param>
public WebpMigrationStep(
IContentRepository contentRepository,
IContentTypeRepository contentTypeRepository,
IContentModelUsage contentModelUsage,
ILanguageBranchRepository languageBranchRepository,
IPropertyDefinitionRepository propertyDefinitionRepository,
IContentCacheRemover contentCacheRemover,
ContentMediaResolver contentMediaResolver,
ILogger<WebpMigrationStep> logger)
{
_contentRepository = contentRepository;
_contentTypeRepository = contentTypeRepository;
_contentModelUsage = contentModelUsage;
_languageBranchRepository = languageBranchRepository;
_propertyDefinitionRepository = propertyDefinitionRepository;
_contentCacheRemover = contentCacheRemover;
_contentMediaResolver = contentMediaResolver;
_logger = logger;
}
/// <summary>
/// Populate the <see cref="P:EPiServer.DataAbstraction.Migration.MigrationStep.Changes" /> list.
/// </summary>
/// <remarks>Use the <see cref="M:EPiServer.DataAbstraction.Migration.MigrationStep.ContentType(System.String)" /> method to register changes.</remarks>
public override void AddChanges()
{
MoveWebpFiles();
}
private void MoveWebpFiles()
{
// If you don't do this you might get errors like this one: 'Call on database executor not created on current context and on different thread.'
Executor = ServiceLocator.Current.GetService<IDatabaseExecutor>();
var oldContentType = _contentTypeRepository.Load<GenericMedia>();
var newContentType =_contentTypeRepository.Load<ImageFile>();
// You could also load the new content type in a more dynamic way
////var mediaType = _contentMediaResolver.GetFirstMatching(".webp");
////var newContentType = _contentTypeRepository.Load(mediaType);
// Get the webp files that are of a generic media type
var webpFiles = GetWebpFiles(oldContentType).Where(content => content.ContentTypeID != newContentType.ID);
int i = 0;
foreach (var content in webpFiles)
{
_logger.LogDebug("Changing type for media with id '{ContentId}'", content.ContentLink.ID);
try
{
var oldProperties = oldContentType.PropertyDefinitions;
var newProperties = newContentType.PropertyDefinitions;
var propertyMap = new List<KeyValuePair<int, int>>();
foreach (var oldProperty in oldProperties)
{
var propertyDefinition = newProperties.FirstOrDefault(p =>
p.Name.Equals(oldProperty.Name, StringComparison.OrdinalIgnoreCase));
if (propertyDefinition != null)
{
propertyMap.Add(new KeyValuePair<int, int>(oldProperty.ID, propertyDefinition.ID));
}
}
// Of course if you have vastly different properties/named properties you have to do a "manual" lookup in the definitions and add it to the mapping
var description = oldProperties.FirstOrDefault(p => p.Name.Equals("Description", StringComparison.OrdinalIgnoreCase));
var copyRight = newProperties.FirstOrDefault(p => p.Name.Equals("Copyright", StringComparison.OrdinalIgnoreCase));
if (description != null && copyRight != null)
{
propertyMap.Add(new KeyValuePair<int, int>(description.ID, copyRight.ID));
}
var result = ConvertMedia(content.ContentLink.ID, oldContentType.ID, newContentType.ID, propertyMap, false, false);
_logger.LogDebug("{Result} for '{ContentId}'", JsonConvert.SerializeObject(result), content.ContentLink.ID);
i++;
}
catch (Exception ex)
{
_logger.LogError(ex, "Changing type for {ContentId} failed", content.ContentLink.ID);
}
}
_contentCacheRemover.Clear();
_logger.LogDebug($"Fixed {i} webp files");
}
private List<GenericMedia> GetWebpFiles(ContentType contentType)
{
var usages = _contentModelUsage.ListContentOfContentType(contentType).Select(u => u.ContentLink.ToReferenceWithoutVersion()).Distinct().ToList();
var total = usages.Count;
_logger.LogDebug($"Found {total} generic media assets");
var webpFiles = new List<GenericMedia>();
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * 0.75) * 2.0))
};
Parallel.ForEach(usages, parallelOptions, contentUsage =>
{
if (!_contentRepository.TryGet<GenericMedia>(contentUsage, out var genericMedia))
{
return;
}
if (genericMedia.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase))
{
webpFiles.Add(genericMedia);
}
});
return webpFiles;
}
private DataSet ConvertMedia(int pageLinkId, int fromPageTypeId, int toPageTypeId, List<KeyValuePair<int, int>> propertyTypeMap, bool recursive, bool isTest)
{
var masterLanguage = LanguageLoaderOption.MasterLanguage().Language;
int masterLanguageId = _languageBranchRepository.Load(masterLanguage).ID;
return Executor.ExecuteTransaction((Func<DataSet>)(() => new DataSet()
{
Locale = CultureInfo.InvariantCulture,
Tables = {
ConvertPageTypeProperties(pageLinkId, fromPageTypeId, masterLanguageId, propertyTypeMap, recursive, isTest),
ConvertPageType(pageLinkId, fromPageTypeId, toPageTypeId, recursive, isTest)
}
}));
}
private DataTable ConvertPageTypeProperties(int pageLinkId, int fromPageTypeId, int masterLanguageId, List<KeyValuePair<int, int>> propertyTypeMap, bool recursive, bool isTest)
{
DataTable dataTable = new DataTable("Properties")
{
Locale = CultureInfo.InvariantCulture
};
dataTable.Columns.Add("FromPropertyID");
dataTable.Columns.Add("ToPropertyID");
dataTable.Columns.Add("Count");
foreach (KeyValuePair<int, int> propertyType in propertyTypeMap)
{
DbCommand command = CreateCommand("netConvertPropertyForPageType");
command.Parameters.Add(CreateReturnParameter());
command.Parameters.Add(CreateParameter("PageID", pageLinkId));
command.Parameters.Add(CreateParameter("FromPageType", fromPageTypeId));
command.Parameters.Add(CreateParameter("FromPropertyID", propertyType.Key));
command.Parameters.Add(CreateParameter("ToPropertyID", propertyType.Value));
command.Parameters.Add(CreateParameter("Recursive", recursive));
command.Parameters.Add(CreateParameter("MasterLanguageID", masterLanguageId));
command.Parameters.Add(CreateParameter("IsTest", isTest));
command.ExecuteNonQuery();
DataRow row = dataTable.NewRow();
row[0] = propertyType.Key;
row[1] = propertyType.Value;
row[2] = GetReturnValue(command);
dataTable.Rows.Add(row);
if (_propertyDefinitionRepository.Load(propertyType.Key).Type.DataType == PropertyDataType.Category)
{
command.CommandText = "netConvertCategoryPropertyForPageType";
command.ExecuteNonQuery();
}
}
return dataTable;
}
private DataTable ConvertPageType(
int pageLinkId,
int fromPageTypeId,
int toPageTypeId,
bool recursive,
bool isTest)
{
DataTable dataTable = new DataTable("Pages");
dataTable.Locale = CultureInfo.InvariantCulture;
dataTable.Columns.Add("Count");
DbCommand command = CreateCommand("netConvertPageType");
command.Parameters.Add(CreateReturnParameter());
command.Parameters.Add(CreateParameter("PageID", pageLinkId));
command.Parameters.Add(CreateParameter("FromPageType", fromPageTypeId));
command.Parameters.Add(CreateParameter("ToPageType", toPageTypeId));
command.Parameters.Add(CreateParameter("Recursive", recursive));
command.Parameters.Add(CreateParameter("IsTest", isTest));
command.ExecuteNonQuery();
DataRow row = dataTable.NewRow();
row["Count"] = GetReturnValue(command);
dataTable.Rows.Add(row);
return dataTable;
}
private DbCommand CreateCommand()
{
DbCommand command = Executor.CreateCommand();
command.CommandType = CommandType.StoredProcedure;
return command;
}
private DbCommand CreateCommand(string cmdText)
{
DbCommand command = CreateCommand();
command.CommandText = cmdText;
return command;
}
private DbParameter CreateReturnParameter() => Executor.CreateReturnParameter();
private DbParameter CreateParameter(string parameterName, object val)
{
return Executor.CreateParameter(parameterName, val);
}
private int GetReturnValue(DbCommand cmd) => Executor.GetReturnValue(cmd);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment