Introduction

This blog post describes the process of migrating content from one Episerver solution to a new Episerver solution. I do not cover converting to Episerver from other systems.

The most common scenario for this process is when you implement a brand new site to replace an existing site, and you want a "clean start".

We are often told to remove old content or that old content should not be migrated to the new site. But, there are serveral situations where you have to migrate content. Sometimes, this means migrating a lot of content, and quite often, it involves a lot of content types. This may feel like a difficult task, but with proper planning and doing things in the right order, it is a manageable task.

Planning

The key to migrating content is planning. The goal is to make content types on your old site match the content types on the new site. 

This is essential for migrating content to the new site. Do not plan on cleaning up content on the new site. You need to do all the cleaning in the old site, before transferring anything.

Create a spreadsheet with three columns, "Contentype name old site", "Contenttype name new site" and "Action". List all your content types from the old site. This list should include all pagetypes and all blocktypes. Then, go through the list and make a descision for each line:

  • Delete it?
  • Migrate it?

If the content type is going to be migrated, you need to answer the following questions and document the answers in your spreadsheet.

  • Does the content type exist in the new site?
  • If not, which content type shoult it be migrated to?

When you have made a decision for each content type, open Visual Studio and start implementing the changes. Just make sure you have a database backup before you start :)

Preparations

The following steps will make your site ready to be migrated to the new site.

Delete the content

You should delete all the content so it does not create unexpected errors when importing content to your new site. The easiest way to do this is by creating a simple tool. This is "just make it work coding" that will be deleted after the migration process, so keep it simple. See appendix A for an example implementation of an admin tool. This code will display a button so you can simply delete all content of that content type.

It is very important that you read the section called "Ghost content" further down. This is especially important if you are migrating from an old site.

Migrate the content

The content type exsist in the new site

If the content type exist in the new site, you need to "clean it" to make sure it will not create any import errors. First, make sure the class names are identical. If they are different, change the class name in your code and create a migration step. The migration step will handle renaming content types and property names on the content type. 

Then, in your spreadsheet, create columns for all the properties on the content types that are going to be migrated. You need one column for the name in the old solution, and one for the name in the new solution. Then, go through each property on the old site and map it to a property on the new site. If a property does not exist in the new site, you need to delete it or rename it. 

Delete property

To delete it, simply add a [Ignore] attribute to the property definition. Then, go to admin mode and delete the property from the content type.

Rename property

Renaming is done by using a migration step. In the following example, the property EventStartDate is renamed to EventStart.

The contains an example for renaming content types as well. The content type "CalendarEventPage" is renamed to "EventPage"

public class CalendarEventMigrationStep : MigrationStep
{  private void RenameContentType()
  {
    ContentType("EventPage").UsedToBeNamed("CalendarEventPage");
  }
  private void RenameProperty(){
    ContentType("EventPage").Property("EventStart").UsedToBeNamed("EventStartDate");
  }
  public override void AddChanges()
  {
    RenameContentType();
    RenameProperty();      
  }
}

Duplicate content type definitions 

You are likely to have several content types being migrated to the same content type in the new solution. Eg. you might have an "article" and a "newarticle" content type that should be migrated to "article" in the new solution. These content types must be merged before migrating to the new site.

Merging content types is done using the convert pages tool in admin mode. Select the content types and go through all the properties. You should map it to a matching name on the target content type or delete it.

The content type does not exist in the new site

If the content type does not exist in the new site, you need to make yet another descision. Should the content type be deleted, or should it be migrated to a content type that exists in the new site?

Delete

Deleting content types is described earlier in this blog post.

Migrate

 If you are migrating to an existing content type, you should follow the same description as for exsisting content types. It is especially important that you make sure you clean up the content type properties.

Ghost content

If you are moving from an old site, there are a chance that the database contains "ghost content". This content exists in the database, but are not properly referenced in Episerver. You need to remove that content before exporting your content. If you ignore doing this, the import process will most likely fail on the new site.

First, you need to find the content type id. You can find this by running the SQL-query "SELECT * FROM tblContentType". Find your content type in the "name" column, and copy the number in the pkID column.

When you have the id, you run the SQL-query "SELECT * FROM tblContent WHERE fkContentTypeID = <pkID from the previous query>" If this query returns anything, your database contains ghost content that we will need to delete. We created a simple "Ghost content deleter tool" for this. You can add a method to our admin tool that we created for deleting content. (Defined in appendix A)

protected void DeleteContent(object sender, EventArgs e)
{
  string[] contentIdString = uxTextBox.Text.Split(',');
  foreach (string s in contentIdString)
  {
    int converted = int.Parse(s);
    ServiceLocator.Current.GetInstance<IContentRepository>().Delete(new ContentReference(converted), true, AccessLevel.NoAccess);
  }
}

Create a comma separated list of all the pkIDs from the last SQL query, paste it into your admin tool and let it delete the content. Re-run you SQL query to make sure all the content are deleted.

Delete properties

The code changes you have implemented will result in content types with properties no longer defined in code. Make sure you go through all the page types, blocks and media types and delete properties marked with "missing" in the "From code" column.

Misc

Plan

I would reccomend that you create a step-by-step plan where you write down everything you are going to do in the production environment. It will make the process a lot easier, and you will remember all the details.

Scheduled tasks

You need to stop all scheduled tasks. You do not want scheduled tasks to change your content during the migration process.

web.config changes

You need to make a few temporarly changes in your web.config file in the new solution. The files you are importing are often quite big, so you need to make sure the solution can handle them. Change the maxRequestLenghth as shown here:

<location path="EPiServer">
  <system.web>
    <httpRuntime maxRequestLength="2147483647" requestValidationMode="2.0" />

 Then, change the requestFiltering elment:

<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="4294967295" />

 Xhtml/string

Make sure you check your property types so you do not try to migrate a Xhtml property to a string property on your new site. 

Migrating content

When you are done with all the preparations above, you are ready to migrate your content. This is done by using the export/import functionality in Episerver.

Export

In your old site, go to the export data tool. Select the start node for you export, check the radiobutton for exporting linked files and press the "Export" button.

Import

In your new site, go to the import data tool. Upload your file, select a target node for importing conent, uncheck the box for updating content with the same id, and press the "Begin import" button.

That's all! You are done, and your content are migrated to your new site.

Appendices

Appendix A

Aspx file:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ConvertingTools.aspx.cs" Inherits="Your.Namespace.AdminPlugin.ConvertingTools" %>
<%@ Import Namespace="EPiServer.Shell" %>
<%@ Import Namespace="EPiServer" %><!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
 <head runat="server">
  <title>Converting tools</title>
 </head>
 <body>
  <form id="form1" runat="server">
   <fieldset>
    <legend>Comments</legend>
    <asp:Button ID="uxCountNumberOfCommentPagesInSolution" Text="Count comment pages" OnClick="FindCommentPages_OnClick" runat="server" CssClass="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Compare" /><br/>
    <asp:Literal runat="server" ID="uxNumberOfComments"></asp:Literal><br/>
    <span class="epi-cmsButton">
     <asp:Button ID="uxDeleteCommentPagesButton" Text="Delete all comment pages" OnClick="DeleteCommentPages_OnClick" runat="server" CssClass="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Compare" />
    </span>
    <p>This will take a while....</p>
   </fieldset>
  </form>
 </body>
</html>

C# file:

[GuiPlugIn(LanguagePath = "/admin/convertingtools/adminplugin",
  DisplayName = "Tools for converting content",
  Description = "Tools used for converting content to the new site",
  Area = PlugInArea.AdminMenu,
  Url = "~/Core/AdminPlugin/ConvertingTools.aspx")]
 public partial class ConvertingTools : SimplePage, ICustomPlugInLoader
 {
  protected override void OnLoad(EventArgs e)
  {
   base.OnLoad(e);
   if (PrincipalInfo.HasAdminAccess == false)
    this.AccessDenied();
  }

  public PlugInDescriptor[] List()
  {
   PlugInDescriptor[] descriptors = null;
   if (PrincipalInfo.HasAdminAccess)
   {
    descriptors = new PlugInDescriptor[1];
    descriptors[0] = PlugInDescriptor.Load(this.GetType());
   }
   return descriptors;
  }
  protected void DeleteCommentPages_OnClick(object sender, EventArgs e)
  {
   IContentRepository _contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
   PageDataCollection comments = GetCommentPages();
   int numberOfComments = comments.Count;
   uxNumberOfComments.Text = string.Format("Number of commment pages: {0}", numberOfComments);
   uxNumberOfComments.DataBind();
   foreach (PageData pageData in comments)
   {
    _contentRepository.Delete(pageData.ContentLink, true, AccessLevel.NoAccess);
   }
   PageDataCollection comments2 = GetCommentPages();
   int numberOfComments2 = comments2.Count;
   uxNumberOfComments.Text = string.Format("Number of comment pages after deleting: {0}", numberOfComments2);
   uxNumberOfComments.DataBind();
  }
  private PageDataCollection GetCommentPages()
  {
   IPageCriteriaQueryService _criteriaQueryService = ServiceLocator.Current.GetInstance<IPageCriteriaQueryService>();
   var criterias = new PropertyCriteriaCollection
   {
    new PropertyCriteria
    {
     Condition = CompareCondition.Equal,
     Name = "PageTypeID",
     Type = PropertyDataType.PageType,
     Value = ServiceLocator.Current.GetInstance<IContentTypeRepository>().Load<CommentPage>().ID.ToString(),
     Required = true
    },
   };
   PageDataCollection comments = _criteriaQueryService.FindPagesWithCriteria(ContentReference.StartPage, criterias);
   return comments;
  }
}

Read more