Read my blog here
Created
February 21, 2025 14:34
-
-
Save jstemerdink/63272480c8a262de9cced9b8ecf8faa7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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