Views: 386
Number of votes: 0
Average rating:

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:

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
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
            };
            _propertyExporter.ExportProperties(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:

base.Import(importedContentData
                    , 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:

try
            {
                // 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(
                    defaultContentImporter
                    , 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)
            {
                Logger.Error(ex);
                throw;
            }
            finally
            {
                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!

.HaBui

Jan 21, 2020

Please login to comment.