May The Tools Be With You

As a software developer, I'm hooked on tools such as vs.net and to build .net and Umbraco solutions. And thus hooked on Umbraco tools that simplify greatly our development workflow, or can automate otherwise time-consuming or error-prone tasks.

While I already knew about a specific tool I'd like to discuss in this article, I didn't feel the need to use it until very recently, when I was (currently still am) tasked with rolling out a complete new responsive version of a website, originally built in v4+, upgraded to v6+, but was able to convince the customer to jump to v7 latest.

Whilst I'm not going to discuss every aspect of the upgrade, I'd love to introduce a tool that has helped us a lot (and I mean a LOT) in making the shift from v6 to v7, especially when it comes down to migrating data from the old to new Umbraco instance.

Introducing Chauffeur

According to its official documentation:

Chauffeur is a CLI for Umbraco, it will sit with your Umbraco website's bin folder and give you an interface to which you can execute commands, known as Deliverables, against your installed Umbraco instance.

Chauffeur is developed for Umbraco 7.x as it is designed around the new Umbraco API.

Before I jump start into how this tool has helped us moving and migrating data, let us start with a short intro

Getting started

If you've never used the tool before and are familiar with vs.net, hit up your favorite editor and install Chauffeur using Nuget through the Package Manager Console:

PM> Install-Package Chauffeur.Runner

After package has been installed, you should have a Chauffeur.Runner.exe executable sitting in the /bin folder of your Umbraco installation.

Open up a command prompt, navigate to your /bin folder and execute Chauffeur.Runner. You should get a new prompt waiting for your commands to execute.

umbraco >

You say commands, I say deliverable

Each command executed against the runner is called a deliverable. If you wish to get a list of deliverables available out-of-the-box, type in

umbraco > help

and it should list all deliverables. And to get more info about a specific deliverable, use

umbraco > help <deliverable>

For example, to get more info about the user deliverable which is shipped together with the tool, use

umbraco > help user
A series of operations that can be run against an Umbraco User.

change-password <username> <new password>
  Changes the password for a given user. This will also hash it if hashing is turned on
  in the web.config

Great. Sounds like a nice feature if you wish to change your admin password without having to go via the backoffice. Just follow the instructions...

umbraco > user change-password admin didyoureallythinkimfoolishtoentermypasswordhere

Wow, but how exactly does it work?

Each deliverable you wish to execute via the Chauffeur command prompt should inherit from an abstract class Deliverable. I won't list the code from the UserDeliverable here (it's up on GitHub), but here goes a basic pattern for implementing a deliverable

[DeliverableName("deliverable-long-name")]
[DeliverableAlias("deliverable-alias")]
public sealed class MyDeliverable : Deliverable
{
    private readonly IService service;

    public MyDeliverable(TextReader reader, TextWriter writer, IService service) : base(reader, writer)
    {
        this.service = service;
    }

    public override async Task<DeliverableResponse> Run(string command, string[] args)
    {
        if (!args.Any())
        {
            await Out.WriteLineAsync("No arguments were provided");

            return DeliverableResponse.Continue;
        }

        //Do your thing...

        return DeliverableResponse.Continue;
    }
}

Basic implementation of a deliverable

Most important things to note:

  • Both deliverable-long-name and deliverable-alias can be used as command name to execute at prompt.
  • Services can be injected into constructor using dependency injection (DI). Makes it easy to inject services exposed by the Umbraco core such as IContentService, IUserService, IMemberService etc...
  • Optionally, you can implement in interface IProvideDirections which has a single method
public interface IProvideDirections
{
    Task Directions();
}

IProvideDirections interface

which you should implement to provide the user with more info about what to expect. Basically this is what the user will get when typing 'help <deliverable>' at the prompt

umbraco > help deliverable-alias

Got it? Let's move on

We decided to roll our own deliverables to perform some data conversions for datatypes that no longer exist in v6 (usercontrols, anyone...) but for which we found a valuable alternative in v7.

Only problem was a difference in data format (xml vs json).

Using a tool such as Chauffeur, which was designed with extensibility and - evenly important - testability in mind, give us a real boost in terms of development time. We didn't have to worry about a setup, we only needed to focus on one particular issue, converting data from xml to json and update documents.

I'll walk you through a code example of such a data conversion. Here's how the old legacy datatype (implemented as a usercontrol containing a textstring/textarea for each language) and new datatype (implemented as a Vorto editor) are storing their data

<?xml version="1.0"?>
<ArrayOfValue xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<value xml:lang="en-US" id="1">Bow Roller</value>
<value xml:lang="nl-NL" id="2">Bow Roller</value>
<value xml:lang="fr-FR" id="3">Davier à rouleau</value>
<value xml:lang="it-IT" id="4">Rullo salpancora</value>
<value xml:lang="es-ES" id="5">Roldana de Proa</value>
<value xml:lang="da-DK" id="6">Bow Roller</value>
<value xml:lang="sv-SE" id="7">Stävrulle</value>
<value xml:lang="nn-NO" id="8">Baugrulle</value>
<value xml:lang="de-DE" id="9">Bugrolle</value>
<value xml:lang="fi-FI" id="10">Bow Roller</value>
<value xml:lang="en-GB" id="11">Bow Roller</value>
<value xml:lang="bg-BG" id="12">Bow Roller</value>
</ArrayOfValue>

Legacy data format

{"values":{"en-US":"Bow Roller","nl-NL":"Bow Roller","fr-FR":"Davier à rouleau","it-IT":"Rullo salpancora","es-ES":"Roldana de Proa","da-DK":"Bow Roller","sv-SE":"Stävrulle","nn-NO":"Baugrulle","de-DE":"Bugrolle","fi-FI":"Bow Roller","en-GB":"Bow Roller","bg-BG":"Bow Roller"},"dtdGuid":"b6f2478c-90af-4bb0-960c-b1b957b9a70b"}

v7 Vorto data format

So, for conversion of hundreds of documents, we've knocked together a simple class that would get all documents of a specific type, read in the xml value and construct and store a json object that a Vorto property uses to store data in the cmsPropertyData table.

[DeliverableName("convert-dictionary-2-vorto")]
[DeliverableAlias("cd2v")]
public class ConvertDictionary2Vorto : Deliverable, IProvideDirections
{
    private readonly IContentTypeService contentTypeService;
    private readonly IContentService contentService;

    public ConvertDictionary2Vorto(TextReader reader, TextWriter writer, IContentTypeService contentTypeService, IContentService contentService)
        : base(reader, writer)
    {
        this.contentTypeService = contentTypeService;
        this.contentService = contentService;
    }

    public override async Task<DeliverableResponse> Run(string command, string[] args)
    {
        if (args.Length != 4)
        {
            await this.Out.WriteLineAsync("Invalid arguments, expected format of `convert-dictionary-2-vorto <guid> <documentTypeAlias> <propertyTypeAlias> <newPropertyTypeAlias>");

            return DeliverableResponse.Continue;
        }

        var guid = args[0].ToLowerInvariant();
        var documentTypeAlias = args[1];
        var propertyTypeAlias = args[2];
        var newPropertyTypeAlias = args[3];

        var documents = this.contentService.GetContentOfContentType(this.contentTypeService.GetContentType(documentTypeAlias).Id);
        var documentsForSave = new List<IContent>();

        foreach (var document in documents.Where(document => document.HasProperty(propertyTypeAlias)))
        {
            var xml = document.GetValue<string>(propertyTypeAlias);
            var conversion = "{" + "\"values\":" + "{" + "}," + $"\"dtdGuid\":\"{guid}\"" + "}";

            if (!string.IsNullOrWhiteSpace(xml))
            {
                var xmlDocument = XDocument.Parse(xml);

                if (xmlDocument?.Document != null)
                {
                    var translations = xmlDocument.Document.XPathSelectElements("/ArrayOfValue/value");

                    conversion = "{" + "\"values\":" + "{"
                                 + string.Join(
                                     ",",
                                     translations.Select(
                                         x => $"\"{x.FirstAttribute.Value}\":{JsonConvert.ToString(x.Value)}"))
                                 + "}," + $"\"dtdGuid\":\"{guid}\"" + "}";
                }
            }

            document.SetValue(newPropertyTypeAlias, conversion);
            documentsForSave.Add(document);
        }

        this.contentService.Save(documentsForSave, raiseEvents: false);

        await this.Out.WriteLineAsync($"Finished converting documents ({guid},{documentTypeAlias},{propertyTypeAlias},{newPropertyTypeAlias})!");

        return DeliverableResponse.Continue;
    }

    public async Task Directions()
    {
        await this.Out.WriteLineAsync("convert-dictionary-2-vorto");
        await this.Out.WriteLineAsync("\tAlias: cd2v");
        await this.Out.WriteLineAsync("\tConverts data from a legacy Dictionary datatype (implemented as usercontrol) into Vorto datatype");
        await this.Out.WriteLineAsync("\tSyntax: convert-dictionary-2-vorto <guid> <documentTypeAlias> <propertyTypeAlias> <newPropertyTypeAlias>");
    }
}

Conversion of data using Chauffeur

Most interesting stuff from this code snippet:

  • Passing in required IContentTypeService and IContentService into ctor() using DI
  • Passing in a couple of arguments for maximum reuse of this deliverable (we were able to reuse these bits for several document types and document type properties).
  • Iterating documents, fetching old format data from document and storing new data format (using a different property, so we could always rollback in case something went horribly wrong)

I'll be honest with you... I didn't create any unit test to build this functionality although Chauffeur has support for this already built-in. Find more info about how to unit test your deliverables on Chauffeur's wiki pages.

Are we done yet?

Yes, that's really are there is to it, it took us approx. 30 mins to build this solution. But wait, there's more.

Did you know you can combine multiple deliverables into a single command? It's called a delivery and Chauffeur expects these files to reside in App_Data/Chauffeur folder (Must have .delivery extension)

For example:

convert-dictionary-2-vorto 8C56E1C1-F798-4ADD-B703-9ECA3657AF4A Boatequipment description descriptionV7
convert-dictionary-2-vorto 8C56E1C1-F798-4ADD-B703-9ECA3657AF4A Boatequipment imageDescription imageDescriptionV7
convert-dictionary-2-vorto 8C56E1C1-F798-4ADD-B703-9ECA3657AF4A BoatAccessory title titleV7
convert-dictionary-2-vorto 8C56E1C1-F798-4ADD-B703-9ECA3657AF4A BoatEquipmentGroup title titleV7
convert-dictionary-2-vorto 8C56E1C1-F798-4ADD-B703-9ECA3657AF4A Boatkeyfeature title titleV7
convert-dictionary-2-vorto 6DA67943-EF21-4920-972F-206362DC186F BoatAccessories intro introV7
convert-dictionary-2-vorto 6DA67943-EF21-4920-972F-206362DC186F BoatAccessory description descriptionV7
convert-dictionary-2-vorto 6DA67943-EF21-4920-972F-206362DC186F Boatequipments intro introV7
convert-dictionary-2-vorto 6DA67943-EF21-4920-972F-206362DC186F Boatkeyfeature description descriptionV7

A delivery file holding multiple deliverables

umbraco > delivery

Above command tells Chauffeur to locate and run all .delivery files found in App_Data/Chauffeur

Bonus: A delivery is tracked in the database, so next time you run a delivery, it will be skipped if already run (Continuous integration, anyone?).

Yes, but we're already using uSync, CmsImport, ...

Fine, and I love these tools as well... Just not for this particular case, I couldn't have migrated such amount of data in less than 1 hour.

Give it a try!

credit where credit's due

Big thanks to Aaron Powell for creating such a great valuable tool.

References

Get in the driver's seat. Don't drink and drive! Happy 2016!

 

 

Dirk De Grave

Dirk is on Twitter as