Johan Björnfot
Jan 14, 2010
  9755
(3 votes)

Custom Export – Handle your “own” stream

In CMS6 it is possible to get an own exclusive stream to write data into the export package (and read data from on the import side).

The concept is that you register a handler (which must derive from a special base class, more on that later) on the export site. During the export process the EPiServer CMS framework will call your handler at certain points, for example when a page is exported. All registered handlers are also accessible through EPiServer.Core.Transfer.ITransferHandlerContext which DataExporter implements. This means you can access your registered handler for example from your custom property transform class.

At the end of the export the CMS framework will call your handler on the abstract method Write(Stream writeStream) where you get a chance to write the data you have collected throughout the export. You are free to write data in your preferred format (e.g. binary or xml serialized).

On the Import side the CMS framework will first check if there is a registered handler of the same type (as on the export side) available and if so use it (If there is no handler registered the framework will try to create an instance of the handler). The framework will then call the method Read(Stream readStream) on the handler with an open stream to the data the export handler wrote.

What has to be implemented?

Your handler must inherit from the abstract class EPiServer.Core.Transfer.TransferHandlerBase (located in EPiServer.dll). There is only two methods that are abstract, as mentioned above, Read and Write which both takes a Stream as an argument.

To register your handler with the export/import you should register an eventhandler to event EPiServer.Core.Transfer.RegisterTransferHandler.RegisterTransferHandlers. The event will be raised whenever an export package is to be created (could be export, mirroring or even local copy of pages). In the event argument there is a list of all handlers that have been registered so far, you can add your own handler to the list. You can also from the event argument see which type of export the current one is (e.g. Copying, MirroringExporting, Exporting). In CMS6 RC1 the type of transfer is not part of event argument but you can get the type of transfer by casting event source to EPiServer.Core.Transfer.ITransferHandlerContext.

Dynamic Data Store objects

The implementation of import/export of data from the Dynamic Data Store is implemented in the class EPiServer.Enterprise.DynamicDataTransferHandler using this new model. It will for example export all objects that are part of  PageData.PageObjects for pages included in the export. You may add other objects from the Dynamic Data Store (could be your custom objects or EPiServer objects like e.g. XForm postings) to the export package. You can do so by locating the instance of DynamicDataTransferHandler in the list EPiServer.Core.Transfer.ITransferHandlerContext.TransferHandlers and then call method AddToExport(Guid externalId, string storeName) on the instance.

Things to consider

  • The sample below exports all users/roles each time. This is not efficient when the code is executing as part of a mirroring job. To behave well with mirroring the code should have some kind of “change” tracking so it knows which users/roles that have changed since the last time the job executed. It should only export new or changed data.
  • “With great power comes great responsibility” – If large amount of data is exported the execution time will of course increase as well as the package size.
  • Many serializers (e.g. Binary and NetDataContractSerializer) serialize assembly version information as part of the format. These serializers could be hard to use if you want to support export from a newer EPiServer and import it on an older version (e.g. export from CMS 6 SP1 and import on CMS6).

Example

Below is a simple example implementation that exports/imports all SQL users and roles. Since it is not possible to retrieve the password for the users the implementation generates random passwords at import. So in a real implementation the importing site should have some kind of password handling for the user. Note that this is sample code and not production code and hence it has no proper error handling and has not been tested thoroughly.

[InitializableModule]
public class UserTransfer : TransferHandlerBase, IInitializableModule
{
    public override void Write(System.IO.Stream writer)
    {
        using (XmlTextWriter xmlWriter = new XmlTextWriter(writer, System.Text.Encoding.UTF8))
        {
            xmlWriter.WriteStartElement("root");

            //Serialize all roles
            xmlWriter.WriteStartElement("roles");
            foreach (string role in Roles.GetAllRoles())
            {
                xmlWriter.WriteElementString("role", role);
            }
            xmlWriter.WriteEndElement();//roles

            //Serialize all users
            xmlWriter.WriteStartElement("users");
            foreach (MembershipUser user in Membership.GetAllUsers())
            {
                XElement userElement = new XElement("user",
                                         new XAttribute("username", user.UserName),
                                         new XAttribute("email", user.Email), 
                                         String.Join(",", Roles.GetRolesForUser(user.UserName)));
                userElement.WriteTo(xmlWriter);
            }
            xmlWriter.WriteEndElement();//users   

            xmlWriter.WriteEndElement();//root
        }
    }

    public override void Read(System.IO.Stream reader)
    {
        using (XmlTextReader xmlReader = new XmlTextReader(reader))
        {
            xmlReader.MoveToContent();
            xmlReader.ReadToFollowing("role");
            while (String.Equals(xmlReader.LocalName, "role"))
            {
                XElement roleElement = XElement.ReadFrom(xmlReader) as XElement;
                if (!Roles.RoleExists(roleElement.Value))
                {
                    Roles.CreateRole(roleElement.Value);
                }
            }

            xmlReader.ReadToFollowing("user");
            while (String.Equals(xmlReader.LocalName, "user"))
            {
                XElement userElement = XElement.ReadFrom(xmlReader) as XElement;
                MembershipUser user = Membership.GetUser(userElement.Attribute("username").Value);
                if (user == null)
                {
                    string newPassword = Membership.GeneratePassword(
                        Membership.Provider.MinRequiredPasswordLength, 
                        Membership.Provider.MinRequiredNonAlphanumericCharacters);
                    user = Membership.CreateUser(userElement.Attribute("username").Value, 
                        newPassword, userElement.Attribute("email").Value);
                }

                String[] exportedRoles = userElement.Value.Split(',');
                foreach (string role in exportedRoles)
                {
                    if (!String.IsNullOrEmpty(role) && !Roles.IsUserInRole(user.UserName, role))
                    {
                        Roles.AddUserToRole(user.UserName, role);
                    }
                }
            }
        }
    }

    #region IInitializableModule Members
    public void Initialize(EPiServer.Framework.Initialization.InitializationEngine context)
    {
        if (Membership.Provider is SqlMembershipProvider)
        {
            RegisterTransferHandler.RegisterTransferHandlers += 
                new EventHandler<RegisterTransferHandlerEventArgs>(RegisterTransferHandlers);
        }
    }

    void RegisterTransferHandlers(object sender, RegisterTransferHandlerEventArgs e)
    {
        if (e.TransferType == TypeOfTransfer.Exporting)
        {
            e.RegisteredHandlers.Add(new UserTransfer());
        }
    }

    public bool IsInitialized
    {
        get;
        set;
    }

    public void Preload(string[] parameters)
    {
    }

    public void Uninitialize(EPiServer.Framework.Initialization.InitializationEngine context)
    {     
    }
    #endregion
}
Jan 14, 2010

Comments

Sep 21, 2010 10:33 AM

Cool! Let's go ahead and build some advanced export/import scenarios ;)
/ Tom Stenius

henriknystrom
henriknystrom Sep 21, 2010 10:33 AM

Maybe even add the ability to completely export/import all settings of a site! Scheduled jobs, workflows, module settings, permissions
Question is how to best expose an interface to select what is to be included in the export/import. Maybe a new plugin area?

Please login to comment.
Latest blogs
Resource Editor - A localization management tool for Optimizely CMS

If you have worked with Optimizely CMS for any amount of time you know that managing localization through XML files can be tedious. Content type...

Per Nergård (MVP) | Feb 23, 2026

Storing JSON in a property the efficient way

Here is a little-known trick to store and retrieve JSON property data more efficiently.

Stefan Holm Olsen | Feb 23, 2026 |

Upgrade RSS Feed Integration to Optimizely CMS 13 – v3.0.0 Beta

I’ve upgraded my  RSS Feed Integration library for Optimizely CMS to support Optimizely CMS 13. Version 3.0.0 is currently released as a beta to...

David Drouin-Prince | Feb 21, 2026 |

Multi Site NuGet v2 for Optimizely CMS 13 – Breaking Changes & Migration

The beta version 2 of DavidHome.Optimizely.MultiSite is now available on NuGet: https://www.nuget.org/packages?q=DavidHome.Optimizely.MultiSite Thi...

David Drouin-Prince | Feb 21, 2026 |

Automate Your OCP Opal Tool Development with PowerShell

Creating an OCP (Optimizely Connect Platform) Opal Tool app from scratch can be time consuming and error prone. You need to set up the project...

Sanjay Kumar | Feb 21, 2026

Using HeadlessKit to build a head for an Optimizely SaaS CMS in .NET 10

Headless has a tendency to promise freedom and deliver alignment meetings. Two codebases. Two sets of models. Two teams trying very hard not to dri...

Allan Thraen | Feb 19, 2026 |