May The Tools Be With You
Heads Up!
This article is several years old now, and much has happened since then, so please keep that in mind while reading it.
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;
}
}
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();
}
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>
{"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"}
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>");
}
}
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
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
- Chauffeur.Runner on GitHub and Nuget
- Vorto on our.umbraco.org and Nuget
Get in the driver's seat. Don't drink and drive! Happy 2016!
Dirk De Grave
Dirk is on Twitter as @netaddicts