Ha Bui
Jan 21, 2020
(2 votes)

Export content with versions

Hi guys,

As you knows, currently, EPiServer just supports latest published version in languages when you uses the default admin tool (Export / Import Data).

Of course in most case, this is enough but sometimes you need to export / import with versions as well. (at least in case editor created a common draft and doesn't want to rework)

How can we archive that? The solution (of-course by default EPiServer doesn't support it then you also should accepts some tips and tricks in code). No worry so much because of we still base on EPiServer default one quite a lot)

Below is our solution:

  1. Use structure map interceptor to apply our tips and tricks via InitializeModule
  2. Intercep on DefaultDataExporter to export contents with versions (instead of only latest published one)
  3. Intercep on DefaultContentImporter to import content version

Some small things should be considered like:

+ Keep saved date

+ Keep saved by (require user migrating)

Okay, lets start steps by steps:

1. Use structure map interceptor to apply our tips and tricks via InitializeModule

Your code should looks like this:

public class DataExporterInitializationModule : IConfigurableModule, IInitializableModule
    public void ConfigureContainer(ServiceConfigurationContext context)
         context.ConfigurationComplete += (o, e) =>
             // Your interceptor logic here
             e.Services.Intercept<IDataExporter>((locator, defaultDataExporter) =>

             e.Services.Intercept<IContentImporter>((locator, defaultDataImporter) =>

2. Intercep on DefaultDataExporter to export contents with versions (instead of only latest published one)

We will override ExportContent method, check our new option (ExportVersion) or just keep default one:

protected override void ExportContent(XmlTextWriter xml
    , ContentReference contentToExport
    , IContentTransferContext context
    , ITransferContentData transferContent) {
	if (_transferExportOptionsEx.ExportVersion) {
		ExportContentWithVerion(xml, contentToExport, context, transferContent, base.ExportContent);
	else {
		base.ExportContent(xml, contentToExport, context, transferContent);

Then get all versions  and build raw transfer content data via:

protected virtual TransferContentData BuildRawTransferContent(IContentTransferContext context
            , List<ContentLanguageSetting> contentLanguageSettings
            , IRawContentRetrieverEx rawContentRetieverEx, IContent version)
            var transferVersionContent = new TransferContentData()
                RawContentData = rawContentRetieverEx.CreateRawContent(version)
            if (contentLanguageSettings != null)
                transferVersionContent.ContentLanguageSettings = contentLanguageSettings;

            PropertyExportContext propertyExportContext = new PropertyExportContext
                TransferContext = context,
                TransferOptions = Options,
                Output = transferVersionContent.RawContentData,
                Source = version
                , transferVersionContent.RawContentData.Property
                , propertyExportContext);

            return transferVersionContent;

Viola! You have just done a big task! Export Content With Version task!

The last one is import the exported content versions (keep version status, keep saved date and saved by)! Take a coffee and relax before we go to the rest!

... (coffee break)

3. Intercep on DefaultContentImporter to import content version

This one is hard part and get much of your pains :( Remember do testings carefully. No pains no gains right :))

We should override IContent Import method (protected override IContent Import) with some tricks as below:

3.1 Tricks to keep language content version: (because of our exported versions are mixing with multiple languages and those versions are flatten)

CultureInfo originalSelectedLanguage = null;
            if (importedContentData.SelectedLanguage != null)
                originalSelectedLanguage = CultureInfo.GetCultureInfo(importedContentData.SelectedLanguage.Name);

            var selectedLanguageAction = importedContentData.GetType().GetProperty("SelectedLanguage");
            var contentLanguage = importedContentData.GetLanguageBranch();
            if (!string.IsNullOrEmpty(contentLanguage))
                selectedLanguageAction.SetValue(importedContentData, CultureInfo.GetCultureInfo(contentLanguage));

3.2 Tricks to keep content versions status (because of EPiServer just "accept" publish action OMG, other actions just for new one):

var status = importedContentData.GetStatus();
            var propSaveAction = context.GetType().GetProperty("SaveAction");
            SaveAction originalSaveActions = SaveAction.Publish | SaveAction.SkipValidation;
            if (!string.IsNullOrEmpty(status) && int.Parse(status) < (int)VersionStatus.Published)
                propSaveAction.SetValue(context, SaveAction.CheckOut | SaveAction.ForceNewVersion | SaveAction.SkipValidation);

3.3 Small things to keep saved date and saved by

ContextCache.Current["PageSaveDB:ChangedBy"] = string.Empty;
ContextCache.Current["PageSaveDB:PageSaved"] = string.Empty;

Okay, last one is: call to base method:

                    , requiredDestinationAccess
                    , context, options, out importedPageGuid);

That is all ? Not yet, something you should deal with:

+ In your interceptor class of DefaultContentImporter, please consider to save original data and then set it back right after content version is imported to avoid any impact.

+ EPiServer DefaultContentImporter only save your version if it's the new one :(

We can resolve those things like this:

                // check the current content version is new or not
                var importedContentGuid = new Guid(importedContentData.GetContentGuid());
                var handleContentGuidMethod = defaultContentImporter.GetType()
                    .GetMethod("HandleContentGuid", BindingFlags.NonPublic | BindingFlags.Instance);
                var guid = (Guid)handleContentGuidMethod.Invoke(
                    , new object[] { importedContentGuid, context });
                PermanentLinkMap permanentLinkMap = permanentLinkMapper.Find(guid);

                var baseContent = base.Import(importedContentData
                    , requiredDestinationAccess
                    , context, options, out importedPageGuid);

                if (permanentLinkMap != null && (context.SaveAction & SaveAction.Publish) != SaveAction.Publish)
                    contentRepository.Save(baseContent, context.SaveAction, requiredDestinationAccess);

                return baseContent;
            catch (Exception ex)
                selectedLanguageAction.SetValue(importedContentData, originalSelectedLanguage);
                propSaveAction.SetValue(context, originalSaveActions);
                ContextCache.Current["PageSaveDB:ChangedBy"] = orgPageSaveDBChangeBy;
                ContextCache.Current["PageSaveDB:PageSaved"] = orgPageSaveDBPageSaved;

Congratulations! Now please try it by your self with AlloyMvc template! Below is my result:

Full source code can be found under: https://github.com/NitecoOPS/ExportImportWithVersion

Hope that the article will help to reduce your headache! ;)


Happy Coding!


Jan 21, 2020


Please login to comment.
Latest blogs
4 tips and tricks for Hangfire on Optimizely CMS

Here are four useful tricks I always apply to any site where I use Hangfire (code included).

Stefan Holm Olsen | Mar 21, 2023 | Syndicated blog

The new LinkItem property in Optimizely CMS, and why it matters

In the past, we have used different tricks to achieve this. Now, the LinkItem property is finally built-in in Optimizely CMS 12!

Tomas Hensrud Gulla | Mar 20, 2023 | Syndicated blog

A day in the life of an Optimizely Developer - Vertical Slicing in CMS12

There is such a vast choice these days in how you can build a website, aside from all of the different programming languages out there, there are...

Graham Carr | Mar 20, 2023

Tag Helpers in CMS 12

Microsoft introduced the TagHelper back in the primordial soup of ASP.NET vNext which became .Net 5, then .NET Core then .Net 5 you know the drill…...

MartinOttosen | Mar 20, 2023