Ministry of Technology
Show Menu

Migrating Umbraco 5 to Umbraco 4.11

It's taken nearly a year, but the Umbraco 4 codebase had finally reached a point where I felt that migration of this site off of the, now dead, Umbraco 5 product line and back into a line that would continue to grow.

I stopped adding content to and maintaining the site about 6 months ago, as the Umbraco 5 platform on which it was built struggled more and more and the site slowed progressively as I added content. My attempt to add new elements to the site was halted when the development copy of the system started intermittently failing to spin up at all. At this point I knew that I needed to migrate but I needed to both have the time to invest in making the switch and I wanted to minimise my changeover time cost.

The New Year seemed like just the right time to address this; with the business taking on new services (more about this to come) the site needed to be reworked but I couldn't do any of this without switching over the platform first.

The Umbraco 5 platform

I was coming from an Umbraco 5.0.1 platform, after having skipped the releases that came after the announcement that the product line would die (minimum return on investment). Most of my code was set up in ViewModels and this blog was controlled by an isolated blog area.

Easing the Switch

I decided to ease the switch by wrapping certain elements of the underlying Umbraco code up so that my views would be going through an intermediary; I was also aware that Umbraco 6 was round the corner and hoped that the abstraction would aid in the upgrade to U6. To this end I tried to recreate some of the base classes I had created, so rather than use the standard view base class I used my own, here...

/// <summary>
/// An abstract base class for Ministryweb views.
/// </summary>
public abstract class MinistrywebViewPage : UmbracoTemplatePage
{
private Ministryweb ministryweb;

/// <summary>
/// Gets the model in a dynamic form.
/// </summary>
public dynamic DynamicModel { get { return CurrentPage; } }

/// <summary>
/// Entry point for helper functions defined at root site level.
/// </summary>
/// <value>
/// The ministryweb helper object.
/// </value>
public Ministryweb Ministryweb
{
get
{
ministryweb = ministryweb ?? new Ministryweb(Umbraco);
return ministryweb;
}
}
}

/// <summary>
/// An abstract base class for Ministryweb views.
/// </summary>
public abstract class MinistrywebViewPage<TModel> : UmbracoViewPage<TModel>
{
private Ministryweb ministryweb;

/// <summary>
/// Entry point for helper functions defined at root site level.
/// </summary>
/// <value>
/// The ministryweb helper object.
/// </value>
public Ministryweb Ministryweb
{
get
{
ministryweb = ministryweb ?? new Ministryweb(Umbraco);
return ministryweb;
}
}
}

I seperated all of my code into a seperate assembly with the Umbraco.Site project purely holding the items managed by Umbraco itself, potentially, (CSS, Views, scripts etc.) and the Global.asax file. The two view base classes above, that sit between the Umbraco equivalents, both expose access to the Ministryweb class. This class is a mixture of static and instance methods, some of which are app specific and some of which wrap functionality of the UmbracoHelper class. The dynamic variation also providesd DynamicModel as an alternat way to access the CurrentPage object (I believe this was changed in 5.0.2 so may not be relevant to many of you, personally I always felt DynamicModel made more sense to me and I liked the syntactic sugar.

The above base classing approach is really straightforward and doesn't require any changes to the default Umbraco 4.11 routing structure to work, other changes did but that's a different story. Here's a cut-down copy of the Ministryweb wrapper class...

 /// <summary>
/// Elements for the site.
/// </summary>
public class Ministryweb
{
private const int BlogRollId = 1059;
private const int RootAncestorId = 1047;
private const int DevelopmentId = 1118;
private const int ConsultancyId = 1111;
private const int TeamManagementId = 1119;

private readonly IContentRepository contentRepository;
private readonly UmbracoHelper umbraco;

private static IContainer container;

private IPublishedContent rootAncestor;
private IBlogRoll blogRoll;

#region | Construction |

/// <summary>
/// Initializes a new instance of the <see cref="Ministryweb" /> class.
/// </summary>
/// <param name="umbracoHelper">The umbraco helper instance.</param>
public Ministryweb(UmbracoHelper umbracoHelper)
{
umbraco = umbracoHelper;
contentRepository = new ContentRepository();
}

#endregion

/// <summary>
/// Gets the blog roll.
/// </summary>
public IBlogRoll BlogRoll
{
get { return blogRoll ?? (blogRoll = new BlogRoll(umbraco.TypedContent(BlogRollId))); }
}

/// <summary>
/// Gets the root ancestor.
/// </summary>
public IPublishedContent RootAncestor
{
get { return rootAncestor ?? (rootAncestor = umbraco.Content(RootAncestorId)); }
}

/// <summary>
/// Gets the consultancy URL.
/// </summary>
public string ConsultancyUrl
{
get { return ContentUrl(ConsultancyId); }
}

/// <summary>
/// Gets the development URL.
/// </summary>
public string DevelopmentUrl
{
get { return ContentUrl(DevelopmentId); }
}

/// <summary>
/// Gets the team management URL.
/// </summary>
public string TeamManagementUrl
{
get { return ContentUrl(TeamManagementId); }
}

/// <summary>
/// Gets the name of the root ancestor.
/// </summary>
public string RootAncestorName
{
get { return RootAncestor.Name; }
}

/// <summary>
/// Determines if a piece of media exists.
/// </summary>
/// <param name="mediaId">The media id.</param>
/// <returns></returns>
public bool MediaExists(string mediaId)
{
return !String.IsNullOrEmpty(MediaUrl(mediaId));
}

/// <summary>
/// Gets the URL for some media content.
/// </summary>
/// <param name="mediaId">The media id.</param>
/// <returns>A nicely formed Url.</returns>
public string MediaUrl(string mediaId)
{
var imageUrl = !String.IsNullOrEmpty(mediaId)
? contentRepository.GetMediaItem(Convert.ToInt32(mediaId)).Url
: String.Empty;

return imageUrl;
}

/// <summary>
/// Gets the content URL.
/// </summary>
/// <param name="nodeId">The node id.</param>
/// <returns>A nicely formed Url.</returns>
public string ContentUrl(int nodeId)
{
return umbraco.NiceUrl(nodeId);
}

/// <summary>
/// Returns the content at a specific node.
/// </summary>
/// <param name="id">The node id.</param>
/// <returns>Dynamic content.</returns>
public dynamic Content(object id)
{
return umbraco.Content(id);
}

/// <summary>
/// Gets an article with the specified ID.
/// </summary>
/// <param name="id">The id.</param>
/// <returns></returns>
public Article Article(object id)
{
var content = Content(id);
return content.Id == 0 ? null : new Article(content);
}

#region | IoC |

/// <summary>
/// Gets the IoC container.
/// </summary>
public static IContainer IocContainer
{
get { return container ?? (container = BuildContainer()); }
}

/// <summary>
/// Builds the IoC container.
/// </summary>
private static IContainer BuildContainer()
{
var builder = new ContainerBuilder();

//register all controllers found in this assembly
builder.RegisterControllers(typeof(Ministryweb).Assembly);

//add custom class to the container as Transient instance
builder.RegisterType<TwitterCacheRepository>().As<ITwitterRepository>();
builder.RegisterType<TwitterApiGateway>().As<ITwitterApiGateway>();
builder.RegisterType<ConfigReader>().As<IConfigReader>();
builder.RegisterType<WebSession>().As<IWebSession>();
builder.RegisterType<ContentRepository>().As<IContentRepository>();

return builder.Build();
}

#endregion

#region | Routing |

/// <summary>
/// Registers the custom routes for the app.
/// </summary>
/// <param name="routes">The routes.</param>
public static void RegisterCustomRoutes(RouteCollection routes)
{
routes.MapRoute(
null, "blog/page",
new { controller = "blog", action = "index" });

routes.MapRoute(
null, "blog/page{page}",
new { controller = "blog", action = "showpage", page = UrlParameter.Optional },
new { page = @"\d+" });

routes.MapRoute(
null, "blog/rss.xml",
new { controller = "blog", action = "feed" });
}

#endregion
}

As you can see, I have registered static IDs for some parts of the site so I can easily access the content from anywhere (as needed with the blog, for example) and wrapped various elements of the UmbracoHelper class to access content. I then adapted the code so all content requests either come from the model passed to the view or are made through this class.

You can also see here my simple IoC and routing implementations - The routing registration method is called on here from the site's Global.asax file...

/// <summary>
/// Custom Wiring for dependency injections.
/// </summary>
public class MinistrywebApplication : UmbracoApplication
{
  protected override void OnApplicationStarted(object sender, EventArgs e)
  {
    base.OnApplicationStarted(sender, e);

    DependencyResolver.SetResolver(new AutofacDependencyResolver(Ministryweb.IocContainer));
    Ministryweb.RegisterCustomRoutes(RouteTable.Routes);
  }
}

Unfortunately, routing doesn't quite work the way I would like so I had to do some weird and wonderful things to get some of it working. I'm hoping this has a better feel to it in Umbraco 6 - I'm going to look at it again once I've upgraded.

Controllers

For the vast majority of the site I didn't need any custom controllers, you can use the CurrentPage / DynamicModel object to do most things for standard content pages. For the more app specific elements though I wanted to use custom models to have a good OO structure to the blog engine. For the most part this was remarkably simple, using some of the documentation here. As with the Views, though, I decided I wanted the flexibility of an abstraction between the controllers and the standard Umbraco controller you inherit from, the RenderMvcController. This enabled me to ignore certain restrictions for the purpose of unit testing.

public class MinistryWebControllerBase : RenderMvcController
{
private bool enableFileCheck = true;

/// <summary>
/// Gets or sets a value indicating whether file checking is enabled.
/// </summary>
/// <value>
/// <c>true</c> if file checking is enabled; otherwise, <c>false</c>.
/// </value>
public bool EnableFileCheck
{
get { return enableFileCheck; }
set { enableFileCheck = value; }
}

/// <summary>
/// Checks to make sure the physical view file exists on disk
/// </summary>
/// <param name="template"></param>
/// <returns></returns>
protected new bool EnsurePhsyicalViewExists(string template)
{
return !enableFileCheck || base.EnsurePhsyicalViewExists(template);
}

/// <summary>
/// Returns an ActionResult based on the template name found in the route values and the given model.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="model"></param>
/// <returns></returns>
/// <remarks>
/// If the template found in the route values doesn't physically exist, then an empty ContentResult will be returned.
/// </remarks>
protected new ActionResult CurrentTemplate<T>(T model)
{
var template = ControllerContext.RouteData.Values["action"].ToString();
if (!EnsurePhsyicalViewExists(template))
{
return Content("");
}
return View(template, model);
}
}

This does some nasty hiding to some of the inbuild Umbraco code that allows you to prevent the underlying Umbraco code from checking that a template exists when executing a simple controller method that returns a template using a custom model, like this...

public class TeamMemberController : MinistryWebControllerBase
{
/// <summary>
/// Default controller view rendering call.
/// </summary>
/// <param name="model">The model.</param>
/// <returns>The Team Member view.</returns>
public override ActionResult Index(RenderModel model)
{
return CurrentTemplate(new TeamMemberViewModel(new TeamMember(model.Content)));
}
}

Without this intermediary step tests fail when they can't find the view file on the file system. There may be a better way to do this, but this worked for me.

Overall Impressions

I expected a journey fraught with pain and difficulty but I was really impressed with the leaps that the Umbraco codebase has taken since I last looked at 4.x (around 4.7). My unit test coverage is around 90% which would have been unthinkable a year or so ago. The code you can write is nicely structured and, for the most part, hangs together nicely. I do have a couple of gripes I haven't been able to get to the bottom of yet, and I'm hoping 6 will address some of this...

  1. Access to media - This is still missing any kind of abstraction - I created my own to enable unit testing but it wasn't ideal. I have a feeling this may have been adressed in U6 though.
  2. Unit Testing Controllers - This is still not quite there, the dependency to look for files should be able to be switched off without hacking into the class as I have done. I was unable to unit test the surface controller I created as it has a baked in dependency on an underlying UmbracoContext which I can't seem to mock or interrupt in any way.

I'm working on splitting Ministryweb into a seperate ContentRepository and the site functions so that I can access the elements within it from controllers and mock some of the method calls out as I need to for testing purposes.