How I migrated my Umbraco v7 site to v8 - Part 2 - Content

Posted written by Paul Seal on April 28, 2020 Umbraco

In my previous post I showed you how I migrated the media from my v7 site to my new v8 site.

In this post I will show you what I did to get the content from my v7 site to my v8 site.

Before we continue

I just want to be clear on the approach I took and why I took it. When I built CodeShare in v7, it was about 4 or 5 years ago and I feel like I've learned a lot since building it.

I didn't want to have to continue with all of the old document types and property names etc. I wanted a fresh start in Umbraco v8, but I needed to port the content over. That is why I chose to start with a clean install of Umbraco v8 with new document types and properties, port all of the content and media over and implement a new theme whilst I was at it.

v7 -> XML -> v8

In order to get the data from one set of document types to a new set I needed create some POCOs (Plain Old Class Objects) which both my v7 site and v8 site were aware of. I started with the Authors first.

I created a POCO for the author doc type

Author.cs

using System.Collections.Generic;

namespace CodeShare.Core.Models.Import
{
    public class Author
    {
        public string Name { get; set; }
        public string Bio { get; set; }
        public string ImageUrl { get; set; }
        public List<string> SocialLinks { get; set; }
        public List<string> Website { get; set; }
        public string LegacyUrl { get; set; }
    }
}

Then I created a POCO which would have a collection of Authors

Authors.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CodeShare.Core.Models.Import
{
    public class Authors
    {
        public Authors()
        {

        }

        public List<Author> AuthorsToImport { get; set; }
    }
}

I wrote a View to use as an altTemplate in the v7 site, which when I visited the page /?altTemplate=ExportAuthors it ran this script and created the XML file for me.

Here is the code I wrote in the view:

@inherits Umbraco.Web.Mvc.UmbracoTemplatePage

@using CodeShare.Core.Models.Import;
@using Umbraco.Web.Models;
@using System.Linq;

@{
    Layout = null;
    var authorList = Umbraco.TypedContent(1114);
    var authors = authorList.Children;

    var authorsModel = new Authors();
    authorsModel.AuthorsToImport = new List<Author>();

    foreach (var author in authors)
    {
        var authorImage = author.GetPropertyValue<IPublishedContent>("profileImage");

        var authorToExport = new Author
        {
            Name = author.Name,
            Bio = author.GetPropertyValue<string>("bio"),
            ImageUrl = authorImage.Url,
            SocialLinks = author.GetPropertyValue<IEnumerable<RelatedLink>>("socialLinks").Select(x => x.Caption + "|" + x.Link).ToList(),
            Website = author.HasValue("website") ? author.GetPropertyValue<IEnumerable<RelatedLink>>("website").Select(x => x.Caption + "|" + x.Link).ToList() : null,
            LegacyUrl = author.Url
        };
        authorsModel.AuthorsToImport.Add(authorToExport);
    }

    System.Xml.Serialization.XmlSerializer writer =
        new System.Xml.Serialization.XmlSerializer(typeof(Authors));

    var path = @"D:\piess\Code\GitHub\codeshare\Import\Authors.xml";
    System.IO.FileStream file = System.IO.File.Create(path);

    writer.Serialize(file, authorsModel);
    file.Close();
}

This code then generated an XML file which looked like this:

<?xml version="1.0"?>
<Authors xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <AuthorsToImport>
    <Author>
      <Name>Paul Seal</Name>
      <Bio>Umbraco MVP and .NET Web Developer from Derby (UK) who specialises in building Content Management System (CMS) websites using MVC with Umbraco as a framework. Paul is passionate about web development and programming as a whole. Apart from when he's with his wife and son, if he's not writing code, he's thinking about it or listening to a podcast about it.</Bio>
      <ImageUrl>/media/1002/paul-seal.jpg</ImageUrl>
      <SocialLinks>
        <string>twitter|https://twitter.com/CodeSharePaul</string>
        <string>facebook|http://www.facebook.com/codeshare.co.uk/</string>
        <string>google-plus|https://plus.google.com/+paulsealuk</string>
        <string>linkedin|https://uk.linkedin.com/pub/paul-seal/2/489/306</string>
        <string>youtube-play|https://www.youtube.com/channel/UCvWcP8GIYl6l2lJ1Z5-s4ew</string>
        <string>github|https://github.com/prjseal</string>
      </SocialLinks>
      <Website>
        <string>codeshare.co.uk|http://www.codeshare.co.uk/</string>
      </Website>
      <LegacyUrl>/authors/paul-seal/</LegacyUrl>
    </Author>
    <Author>
      <Name>Jamie Taylor</Name>
      <Bio>A .NET developer specialising in ASP.NET MVC websites and services, with a background in WinForms and Games Development.

When not programming using .NET, he is either learning about .NET Core (and usually building something cross platform with it), learning about other programming languages, or writing for his non-dev blog</Bio>
      <ImageUrl>/media/1260/jamie-taylor-author-image.jpg</ImageUrl>
      <SocialLinks>
        <string>twitter|https://twitter.com/dotNetCoreBlog</string>
        <string>google-plus|https://plus.google.com/+JamieGaProgManTaylor</string>
        <string>linkedin|https://www.linkedin.com/profile/view?id=206090730</string>
      </SocialLinks>
      <Website>
        <string>dotnetcore.gaprogman.com|https://dotnetcore.gaprogman.com/</string>
      </Website>
      <LegacyUrl>/authors/jamie-taylor/</LegacyUrl>
    </Author>
  </AuthorsToImport>
</Authors>

Importing the Authors

Now I had my XML, I could import it into the v8 site. So I created a new Template in my v8 site and put some code in that to do the import. You can see how to enable alt templates in v8 by reading part 1.

Why am I writing code in Views?

Some people might be wondering why I am doing this in views instead of controllers. The truth is, it was just quicker and easier for me to do. You don't have to do it the same way as me. You can put your code in controllers if you want, I don't mind, it just worked for me.

This is my ImportAuthors.cshtml file in my v8 project

@using System.Xml.Serialization
@using CodeShare.Core.Models.Import
@using Our.Umbraco.DocTypeGridEditor.Composing
@using Current = Umbraco.Core.Composing.Current
@inherits Umbraco.Web.Mvc.UmbracoViewPage
@{
    Layout = null;


    XmlSerializer serializer =
        new XmlSerializer(typeof(Authors));

    // Declare an object variable of the type to be deserialized.
    Authors i;

    using (Stream reader = new FileStream(@"D:\piess\Code\GitHub\codeshare\Import\Authors.xml", FileMode.Open))
    {
        // Call the Deserialize method to restore the object's state.
        i = (Authors)serializer.Deserialize(reader);
    }

    var contentService = Current.Services.ContentService;

    foreach (var item in i.AuthorsToImport)
    {
        var author = contentService.Create(item.Name, 1084, "author");
        author.SetValue("bio", item.Bio);
        author.SetValue("legacyUrl", item.LegacyUrl);
        contentService.SaveAndPublish(author);
        <p>@item.Name</p>
    }
}

1084 was the id of the author list page where I wanted the authors to be created as children of.

As I only had about 14 authors, I decided to just import the name, bio and legacyUrl. I then went in and updated the links manually as at this stage I hadn't worked out how to create the Link objects for all of the social links etc.

Importing Articles

This was the most important part for me. I had 200+ articles to import.

I created the Article class:

Article.cs

namespace CodeShare.Core.Models.Import
{
    public class Article
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Title { get; set; }
        public string Subtitle { get; set; }
        public string Categories { get; set; } //   ["animate","tree","icons","v8","umbraco"]
        public string BlogPostDate { get; set; } // 11/11/2019 22:59:17
        public string Author { get; set; } // umb://document/8c0c39eee17e41c29acf856085b8c3ee
        public string BodyText { get; set; }
        public string MetaName { get; set; }
        public string MetaDescription { get; set; }
        public string MetaKeywords { get; set; }
        public bool EnableAmp { get; set; }
        public string MainImage { get; set; } // umb://media/73cc461857fb48fb99a4225f8077737b
        public string LegacyUrl { get; set; }
        public string UmbracoUrlAlias { get; set; }
    }
}

I then created a POCO to hold the collection of Articles

Articles.cs

using System.Collections.Generic;

namespace CodeShare.Core.Models.Import
{
    public class Articles
    {
        public Articles()
        { }

        public List<Article> ArticlesToExport { get; set; }
    }
}

As mentioned in part one, I decided to capture the Url from the v7 site and store it in the LegacyUrl property. I was glad I had because I found out that v8 generates the url differently to how v7 does when it comes to apostrophes in the name of the content item.

I created a Template in the v7 site called ExportBlogPosts

@inherits Umbraco.Web.Mvc.UmbracoTemplatePage

@using CodeShare.Core.Models.Import;
@using Umbraco.Web.Models;
@using System.Linq;

@{
    Layout = null;
    var authorList = Umbraco.TypedContent(1085);
    var articles = authorList.Children;

    var articlesModel = new Articles();
    articlesModel.ArticlesToExport = new List<Article>();

    foreach (var article in articles)
    {
        var mainImage = article.GetPropertyValue<IPublishedContent>("pageBannerImage");

        var categories = article.GetPropertyValue<IEnumerable<string>>("categories");
        string newCategories = null;
        if (categories != null &amp;&amp; categories.Any())
        {
            newCategories = "[" + "\"" + string.Join("\",\"", categories) +"\"" + "]";
        }

        var metaKeywords = article.GetPropertyValue<IEnumerable<string>>("metaKeywords");
        string newMetaKeywords = null;
        if (metaKeywords != null &amp;&amp; metaKeywords.Any())
        {
            newMetaKeywords = "[" + "\"" + string.Join("\",\"", metaKeywords) + "\"" + "]";
        }

        var date = article.GetPropertyValue<DateTime>("blogPostDate");

        var articleToExport = new Article
        {
            Id = article.Id,
            Name = article.Name,
            Title = article.GetPropertyValue<string>("pageTitle"),
            Subtitle = article.GetPropertyValue<string>("pageIntro"),
            Author = "umb://document/8c0c39eee17e41c29acf856085b8c3ee",
            Categories = newCategories,
            BlogPostDate = date.ToString("MM/dd/yyyy hh:mm:ss"),
            MetaName = article.GetPropertyValue<string>("metaName"),
            MetaDescription = article.GetPropertyValue<string>("metaDescription"),
            MetaKeywords = newMetaKeywords,
            EnableAmp = article.GetPropertyValue<bool>("enableAmp"),
            MainImage = $"umb://media/{mainImage.GetKey().ToString("N")}",
            LegacyUrl = article.Url,
            UmbracoUrlAlias = article.GetPropertyValue<string>("umbracoUrlAlias"),
            BodyText = article.GetPropertyValue<string>("contentGrid")
        };
        articlesModel.ArticlesToExport.Add(articleToExport);
    }

    System.Xml.Serialization.XmlSerializer writer =
        new System.Xml.Serialization.XmlSerializer(typeof(Articles));

    var path = @"D:\piess\Code\GitHub\codeshare\Import\BlogPosts.xml";
    System.IO.FileStream file = System.IO.File.Create(path);

    writer.Serialize(file, articlesModel);
    file.Close();
}

I decided to cheat with the author because out of the 200+ blog posts only 15 or so were by different authors so I decided to set myself as the author of all the articles on import and then pick those authors manually myself after the import.

Once I ran the above code, I had an XML file which looked like this:

<?xml version="1.0"?>
<Articles xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <ArticlesToExport>
    <Article>
      <Id>1106</Id>
      <Name>How to generate random and realistic data online</Name>
      <Title />
      <Subtitle />
      <Categories>["Data","Testing","Validation"]</Categories>
      <BlogPostDate>09/30/2015 12:00:00</BlogPostDate>
      <Author>umb://document/8c0c39eee17e41c29acf856085b8c3ee</Author>
      <BodyText>{
  "name": "1 column layout",
  "sections": [
    {
      "grid": 12,
      "rows": [
        {
          "name": "Full Width",
          "areas": [
            {
              "grid": 12,
              "hasConfig": false,
              "controls": [
                {
                  "value": "&amp;lt;p&amp;gt;If you want to be able to generate random and realistic data for your program, there is a fantastic free resource called Mockaroo.&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;It lets you create your list of fields and their types.&amp;lt;/p&amp;gt;\n&amp;lt;p&amp;gt;You can also decide, on a field by field basis, what percentage of records will return as blank.&amp;lt;/p&amp;gt;\n&amp;lt;p&amp;gt; &amp;lt;/p&amp;gt;",
                  "editor": {
                    "name": "Rich text editor",
                    "alias": "rte",
                    "view": "rte",
                    "render": null,
                    "icon": "icon-article",
                    "config": {}
                  },
                  "active": false
                },
                {
                  "value": {
                    "focalPoint": {
                      "left": 0.5,
                      "top": 0.5
                    },
                    "id": 1110,
                    "image": "/media/1001/mockaroo_-_random_data_generator_csv_json_sql_excel_-_2015-09-30_134454.png"
                  },
                  "editor": {
                    "name": "Image",
                    "alias": "media",
                    "view": "media",
                    "render": null,
                    "icon": "icon-picture",
                    "config": {}
                  },
                  "active": false
                },
                {
                  "value": "&amp;lt;p&amp;gt;When you have finished, you can press download. This will generate up to 1000 lines of data for free and you can choose from a variety of output formats to export it as. You can even save your schema to use again later, if you sign up for a free account.&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;Have a look at it and let me know if you find it useful.&amp;lt;br /&amp;gt;&amp;lt;br /&amp;gt;&amp;lt;a href=\"https://www.mockaroo.com/\" target=\"_blank\"&amp;gt;https://www.mockaroo.com/&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;\n&amp;lt;p&amp;gt; &amp;lt;/p&amp;gt;\n&amp;lt;p&amp;gt;Thanks to &amp;lt;a href=\"http://www.dotnetrocks.com/\" target=\"_blank\"&amp;gt;.NET Rocks&amp;lt;/a&amp;gt; for letting me know about this site in the first place.&amp;lt;/p&amp;gt;",
                  "editor": {
                    "name": "Rich text editor",
                    "alias": "rte",
                    "view": "rte",
                    "render": null,
                    "icon": "icon-article",
                    "config": {}
                  },
                  "active": false
                }
              ]
            }
          ],
          "hasConfig": false,
          "id": "1ef0352e-aed1-e761-6df3-dfad5cf8339f"
        }
      ]
    }
  ]
}</BodyText>
      <MetaName>Mockaroo - Paul Seal .NET Developer Blog</MetaName>
      <MetaDescription>If you want to be able to generate random and realistic data for your program, there is a fantastic free resource called Mockaroo.</MetaDescription>
      <MetaKeywords>["Mockaroo","Test","Data","Test Data","Free","1000","random","realistic","excel","csv","generator","sql","export"]</MetaKeywords>
      <EnableAmp>false</EnableAmp>
      <MainImage>umb://media/3ea521a48d9d4cf0ad4c9ea17b96f03a</MainImage>
      <LegacyUrl>/blog/how-to-generate-random-and-realistic-data-online/</LegacyUrl>
      <UmbracoUrlAlias>blog/mockaroo</UmbracoUrlAlias>
    </Article>
    <Article>
  </ArticlesToExport>
</Articles>

I then created a Template called ImportArticles in the v8 site and added this code to import the articles.

@using System.Xml.Serialization
@using CodeShare.Core.Models.Import
@using Our.Umbraco.DocTypeGridEditor.Composing
@using Current = Umbraco.Core.Composing.Current
@inherits Umbraco.Web.Mvc.UmbracoViewPage
@{
    Layout = null;


    XmlSerializer serializer =
        new XmlSerializer(typeof(Articles));

    // Declare an object variable of the type to be deserialized.
    Articles i;

    using (Stream reader = new FileStream(@"D:\piess\Code\GitHub\codeshare\Import\BlogPosts.xml", FileMode.Open))
    {
        // Call the Deserialize method to restore the object's state.
        i = (Articles)serializer.Deserialize(reader);
    }

    var contentService = Current.Services.ContentService;

    foreach (var item in i.ArticlesToExport.Skip(10))
    {
        var article = contentService.Create(item.Name, 1078, "article");
        article.SetValue("title", item.Title);
        article.SetValue("subtitle", item.Subtitle);
        article.SetValue("category", item.Categories);
        article.SetValue("articleDate", item.BlogPostDate);
        article.SetValue("author", item.Author);
        article.SetValue("mainContent", item.BodyText);
        article.SetValue("articleDate", item.BlogPostDate);
        article.SetValue("metaName", item.MetaName);
        article.SetValue("metaDescription", item.MetaDescription);
        article.SetValue("metaKeywords", item.MetaKeywords);
        article.SetValue("mainImage", item.MainImage);
        article.SetValue("legacyUrl", item.LegacyUrl);
        article.SetValue("umbracoUrlAlias", item.UmbracoUrlAlias);
        contentService.SaveAndPublish(article);
        <p>@item.Name</p>
    }
}

As you can see, this is similar to how I imported the authors, and you can see how you can apply this sort of approach to your migrations.

What has also worked well here, is that I imported the media previously, using the UDI key from the v7 site and kept the same file paths and urls. So it all mapped correctly when importing the content wiht the UDIs of the picked images.

I hope this post has helped you.