Skip to content

Instantly share code, notes, and snippets.

@MilanLund
Last active June 17, 2024 10:46
Show Gist options
  • Save MilanLund/fee8f9b0ef150a2093a04e05a554f0df to your computer and use it in GitHub Desktop.
Save MilanLund/fee8f9b0ef150a2093a04e05a554f0df to your computer and use it in GitHub Desktop.
Migrate Content from Kentico CMS Portal Engine to Xperience by Kentico
This gist represents an implementation of content migration from Kentico CMS Portal Engine to Xperience by Kentico
The details about this gist could be found on https://www.milanlund.com/knowledge-base/how-to-migrate-content-from-kentico-cms-portal-engine-to-xperience-by-kentico and https://www.milanlund.com/knowledge-base/migrate-media-library-items-from-kentico-cms-portal-engine-to-xperience-by-kentico
using CMS.ContentEngine;
using CMS.MediaLibrary;
using CMS.Membership;
using CMS.Websites;
using CMS.Websites.Routing;
using Kentico.Content.Web.Mvc;
using CMS.DataEngine;
using Kentico.Content.Web.Mvc.Routing;
namespace Project.Helpers;
/// <summary>
/// Represents a helper class for migration operations.
/// </summary>
public class MigrationHelper
{
private readonly IContentQueryExecutor contentQueryExecutor;
private readonly IWebPageManager webPageManager;
private readonly IContentItemManager contentItemManager;
private readonly IHttpClientFactory clientFactory;
private readonly IPreferredLanguageRetriever currentLanguageRetriever;
private readonly IWebsiteChannelContext websiteChannelContext;
private readonly IInfoProvider<MediaLibraryInfo> mediaLibraryInfoProvider;
private readonly IInfoProvider<MediaFileInfo> mediaFileInfoProvider;
private readonly IMediaFileUrlRetriever mediaFileUrlRetriever;
public MigrationHelper(
IContentQueryExecutor contentQueryExecutor,
IWebPageManagerFactory webPageManagerFactory,
IWebsiteChannelContext websiteChannelContext,
IUserInfoProvider userInfoProvider,
IContentItemManagerFactory contentItemManagerFactory,
IHttpClientFactory clientFactory,
IPreferredLanguageRetriever currentLanguageRetriever,
IInfoProvider<MediaFileInfo> mediaFileInfoProvider,
IInfoProvider<MediaLibraryInfo> mediaLibraryInfoProvider,
IMediaFileUrlRetriever mediaFileUrlRetriever
)
{
UserInfo user = userInfoProvider.Get("Administrator");
webPageManager = webPageManagerFactory.Create(websiteChannelContext.WebsiteChannelID, user.UserID);
contentItemManager = contentItemManagerFactory.Create(user.UserID);
this.contentQueryExecutor = contentQueryExecutor;
this.clientFactory = clientFactory;
this.currentLanguageRetriever = currentLanguageRetriever;
this.websiteChannelContext = websiteChannelContext;
this.mediaFileInfoProvider = mediaFileInfoProvider;
this.mediaLibraryInfoProvider = mediaLibraryInfoProvider;
this.mediaFileUrlRetriever = mediaFileUrlRetriever;
}
/// <summary>
/// Gets the migration data from the specified endpoint.
/// </summary>
/// <param name="endpoint">The endpoint URL.</param>
/// <returns>The HTTP response message.</returns>
public async Task<HttpResponseMessage> GetMigrationData(string endpoint)
{
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
var client = clientFactory.CreateClient();
var response = await client.SendAsync(request);
return response;
}
/// <summary>
/// Deletes a page if it exists.
/// </summary>
/// <param name="contentType">The content type of the page.</param>
/// <param name="codename">The codename of the page.</param>
public async Task DeletePageIfExists(string contentType, string codename)
{
var languageName = currentLanguageRetriever.Get();
var builder = new ContentItemQueryBuilder()
.ForContentType(contentType, config =>
{
config
.TopN(1)
.ForWebsite(websiteChannelContext.WebsiteChannelName)
.Where(where => where.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemName), codename));
})
.InLanguage(languageName);
var pageId = await contentQueryExecutor
.GetWebPageResult(builder: builder, resultSelector: rowData =>
{
var pageRowId = rowData.WebPageItemID;
return pageRowId;
});
if (pageId.Any())
{
await webPageManager.Delete(pageId.First(), languageName);
}
}
/// <summary>
/// Deletes a content item if it exists.
/// </summary>
/// <param name="contentType">The content type of the item.</param>
/// <param name="codename">The codename of the item.</param>
public async Task DeleteContentItemIfExists(string contentType, string codename)
{
var languageName = currentLanguageRetriever.Get();
var builder = new ContentItemQueryBuilder()
.ForContentType(contentType, config =>
{
config
.TopN(1)
.Where(where => where.WhereEquals(nameof(IContentItemFieldsSource.SystemFields.ContentItemName), codename));
})
.InLanguage(languageName);
var contentItemId = await contentQueryExecutor
.GetResult(builder: builder, resultSelector: rowData =>
{
var contentItemId = rowData.ContentItemID;
return contentItemId;
});
if (contentItemId.Any())
{
await contentItemManager.Delete(contentItemId.First(), languageName);
}
}
/// <summary>
/// Creates a media library file.
/// </summary>
/// <param name="remoteFileUrl">The URL of the remote file.</param>
/// <param name="mediaLibraryName">The name of the media library.</param>
/// <param name="mediaLibraryTargetFolder">The target folder in the media library.</param>
/// <returns>The created media library file.</returns>
public async Task<string> CreateMediaLibraryFile(string remoteFileUrl, string mediaLibraryName, string mediaLibraryTargetFolder)
{
// Get Media Library object reference
MediaLibraryInfo library = mediaLibraryInfoProvider.Get(mediaLibraryName);
if (library != null && !string.IsNullOrEmpty(remoteFileUrl))
{
// Get the file name from the URL
string remoteFilePath = remoteFileUrl.Split("?").First();
string fileName = remoteFilePath.Split("/").Last();
// Check if the file already exists in the media library
MediaFileInfo mediaFile = MediaFileInfoProvider.GetMediaFileInfo(library.LibraryID, $"{mediaLibraryTargetFolder}{fileName}");
if (mediaFile == null)
{
// Get local path for the file
string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filePath = Path.Combine(documentsPath, "Project", fileName);
// Download the file to the local path
using (var httpClient = new HttpClient())
{
using (var response = await httpClient.GetAsync(remoteFilePath))
{
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await response.Content.CopyToAsync(fileStream);
}
}
}
// Create a new media file
CMS.IO.FileInfo file = CMS.IO.FileInfo.New(filePath);
if (file != null)
{
mediaFile = new MediaFileInfo(filePath, library.LibraryID)
{
FileName = file.Name.Replace(file.Extension, ""),
FileTitle = "",
FileDescription = "",
FilePath = mediaLibraryTargetFolder,
FileExtension = file.Extension,
FileMimeType = MimeTypeHelper.GetMimetype(file.Extension),
FileLibraryID = library.LibraryID,
FileSize = file.Length
};
mediaFileInfoProvider.Set(mediaFile);
}
}
// Return the media file reference
if (mediaFile != null)
{
return $"[{{\"Identifier\":\"{mediaFile.FileGUID}\",\"Name\":\"{fileName}\",\"Size\":{mediaFile.FileSize},\"Dimensions\":{{\"Width\":{mediaFile.FileImageWidth},\"Height\":{mediaFile.FileImageHeight}}}}}]";
}
}
return null;
}
/// <summary>
/// Creates media list based on the media items.
/// </summary>
/// <param name="mediaItem">The media items.</param>
/// <param name="mediaLibraryName">The name of the media library.</param>
/// <param name="mediaLibraryTargetFolder">The target folder in the media library.</param>
/// <param name="host">The host URL.</param>
/// <returns>The list of media item references.</returns>
public async Task<IEnumerable<ContentItemReference>> CreateMediaList(List<MediaItemMigration> mediaItems, string mediaLibraryName, string mediaLibraryTargetFolder, string host)
{
var mediaList = new List<ContentItemReference>();
foreach (var item in mediaItems)
{
// Generate unique code name identifier for the reusable content item
string codename = $"MigratedMediaItem{item.DocumentID}";
// If item with such name already exists (when we repeat the migration), delete it
await DeleteContentItemIfExists(MediaItemReusableContent.CONTENT_TYPE_NAME, codename);
// Create media library file and get reference to it
string image = await CreateMediaLibraryFile(item.Image.Replace("~", host), mediaLibraryName, mediaLibraryTargetFolder);
// Create reusable content item
CreateContentItemParameters createParams = new CreateContentItemParameters(
MediaItemReusableContent.CONTENT_TYPE_NAME,
codename,
item.Name.Truncate(100),
WebsiteConstants.LANGUAGE_DEFAULT
);
var itemData = new ContentItemData(new Dictionary<string, object>{
{ "Image", image }
});
var id = await contentItemManager.Create(createParams, itemData);
await contentItemManager.TryPublish(id, WebsiteConstants.LANGUAGE_DEFAULT);
// Get the created content item reference
var builder = new ContentItemQueryBuilder();
builder.ForContentType(MediaItemReusableContent.CONTENT_TYPE_NAME, subqueryConfiguration =>
{
subqueryConfiguration
.TopN(1)
.Where(where => where.WhereEquals("ContentItemID", id));
});
MediaItemReusableContent contentItem =
(await contentQueryExecutor.GetMappedWebPageResult<MediaItemReusableContent>(builder)).FirstOrDefault();
// Add the content item reference to the list
mediaList.Add(new ContentItemReference
{
Identifier = contentItem.SystemFields.ContentItemGUID
});
}
return mediaList;
}
}
public class NewsItemDto
{
public int DocumentID { get; set; }
public string NodeAlias { get; set; }
public string Title { get; set; }
public string Date { get; set; }
public string Text { get; set; }
public List<MediaItemDto> Image { get; set; }
}
public class MediaItemDto
{
public int DocumentID { get; set; }
public string Name { get; set; }
public string Url { get; set; }
}
using System.Text.Json;
using CMS.ContentEngine;
using CMS.Helpers;
using CMS.Membership;
using CMS.Websites;
using CMS.Websites.Routing;
using Kentico.Content.Web.Mvc.Routing;
using Microsoft.AspNetCore.Mvc;
using Project.Helpers;
namespace Project.Controllers;
public class NewsMigrationController : Controller
{
private readonly IWebPageManager webPageManager;
private readonly IContentQueryExecutor contentQueryExecutor;
private readonly MigrationHelper migrationHelper;
private readonly IContentItemManager contentItemManager;
private readonly IPreferredLanguageRetriever currentLanguageRetriever;
public NewsMigrationController(
IContentQueryExecutor contentQueryExecutor,
IWebPageManagerFactory webPageManagerFactory,
IUserInfoProvider userInfoProvider,
IWebsiteChannelContext websiteChannelContext,
MigrationHelper migrationHelper,
IContentItemManagerFactory contentItemManagerFactory,
IPreferredLanguageRetriever currentLanguageRetriever
)
{
UserInfo user = userInfoProvider.Get("Administrator");
webPageManager = webPageManagerFactory.Create(websiteChannelContext.WebsiteChannelID, user.UserID);
this.contentQueryExecutor = contentQueryExecutor;
this.migrationHelper = migrationHelper;
contentItemManager = contentItemManagerFactory.Create(user.UserID);
this.currentLanguageRetriever = currentLanguageRetriever;
}
/// <summary>
/// Handles the HTTP POST request for news migration.
/// </summary>
/// <returns>The result of the migration.</returns>
[HttpPost("migration/news")]
public async Task<IActionResult> Index()
{
// Request the URL where our JSON data resides and deserialize it into a list of News, based on a Data Transfer Object model
var response = await migrationHelper.GetMigrationData("https://<your host domain name>/Export/News-export");
if (!response.IsSuccessStatusCode)
{
return StatusCode((int)response.StatusCode);
}
var responseStream = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<List<NewsItemDto>>(responseStream);
int newsPageId = 324;
// Iterate through the list and for each item
foreach (var item in data)
{
// Generate unique code name identifiers for the page and reusable content item
string pageName = $"MigratedNewsItem{item.DocumentID}";
string contentItemName = $"MigratedContentItemNewsItem{item.DocumentID}";
// If items with such names already exist (when we repeat the migration), delete them
await migrationHelper.DeletePageIfExists(NewsItemPage.CONTENT_TYPE_NAME, pageName);
await migrationHelper.DeleteContentItemIfExists(NewsItemReusableContent.CONTENT_TYPE_NAME, contentItemName);
// Parse incoming data into appropriate data types.
DateTime date;
DateTime.TryParse(item.Date, out date);
var image = await migrationHelper.CreateMediaList(item.Image, "Images", "News/", "https://<your host domain name>");
string pageDisplayName = item.Title.Truncate(100);
var languageName = currentLanguageRetriever.Get();
// Create a reusable content item
CreateContentItemParameters createParams = new CreateContentItemParameters(
NewsItemReusableContent.CONTENT_TYPE_NAME,
contentItemName,
pageDisplayName,
languageName
);
var contentItemData = new ContentItemData(new Dictionary<string, object>{
{ "Title", item.Title },
{ "Date", date },
{ "Text", item.Text },
{ "Image", image }
});
var id = await contentItemManager.Create(createParams, contentItemData);
await contentItemManager.TryPublish(id, languageName);
var builder = new ContentItemQueryBuilder();
builder.ForContentType(NewsItemReusableContent.CONTENT_TYPE_NAME, subqueryConfiguration =>
{
subqueryConfiguration
.TopN(1)
.Where(where => where.WhereEquals("ContentItemID", id));
});
// Obtain a reference to the reusable content type
NewsItemReusableContent contentItem =
(await contentQueryExecutor.GetMappedResult<NewsItemReusableContent>(builder)).FirstOrDefault();
IEnumerable<ContentItemReference> itemList =
[
new ContentItemReference
{
Identifier = contentItem.SystemFields.ContentItemGUID
},
];
// Create a page using the reference, attach the page to the parent News page by its Page ID, and assign it a URL slug.
var itemData = new ContentItemData(new Dictionary<string, object>{
{"Item", itemList }
});
var contentItemParameters = new ContentItemParameters(NewsItemPage.CONTENT_TYPE_NAME, itemData);
var createPageParameters = new CreateWebPageParameters(pageName, pageDisplayName, languageName, contentItemParameters)
{
ParentWebPageItemID = newsPageId,
UrlSlug = item.NodeAlias
};
int webPageItemId = await webPageManager.Create(createPageParameters);
await webPageManager.TryPublish(webPageItemId, languageName);
}
return Ok();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment