Swimburger

Thoughts and tips on moving to Umbraco 9 from Umbraco 8

Niels Swimberghe

Niels Swimberghe - - Umbraco

Follow me on Twitter, buy me a coffee

.NET Bot on a scooter riding from Umbraco 8 to 9. Title: Thoughts and tips on moving to Umbraco 9

.NET Core was a groundbreaking change to the .NET platform. It is blazing fast, open-source, and cross-platform across Windows, Linux, and macOS. 

Windows is great, but sometimes an afterthought or outright not supported by other technology ecosystems. With Linux being a supported platform now, an entire new ecosystem of tools and technology opens up to us .NET developers.

In theory, .NET developers were never bound to using Visual Studio and could use MSBuild using the command line to build .NET Framework applications. But in practice, Visual Studio was the only option to get your work done. Don't get me wrong, I love Visual Studio, but now we have more than a single choice. We have cross-platform alternatives like VS Code and JetBrains Rider, although only for the .NET Core platform.

This evolution of the .NET ecosystem has been a liberation in my opinion. You have a choice in operating systems, a choice in IDEs and editors, and the entire platform has become open and much faster.

Unless you're stuck on .NET Framework of course. Many products have made large investments into .NET Framework and it is a difficult task to move from .NET Framework to .NET Core. One of those products was the open-source CMS, Umbraco. Until now. 

Umbraco 9 on .NET 5 #

The newest version of Umbraco released in September 2021, Umbraco 9, has been built on top of ASP.NET Core (.NET 5)!
Developing websites using Umbraco 9 feels very familiar, but also very different. You see, the Unicore team has done a great job preserving how Umbraco works while also taking advantage of the new capabilities and innovations from ASP.NET Core.
Practically, this means you don't have to "relearn" Umbraco, but you do have to learn ASP.NET Core.

Now that Umbraco is built on ASP.NET Core, you get all the previously mentioned benefits from the platform upgrade like:

  • Projects build faster
  • Instead of 30 seconds, Umbraco sites start in single digits
  • Develop and deploy on other operating systems
  • Develop using other IDEs and editors
  • Use new innovations from .NET Core and ASP.NET Core

Upgrade from Umbraco 8 to 9 #

This is not a definitive guide on how to move from Umbraco 8 to 9, but instead is an overview of my observations based on my experience upgrading a specific Umbraco 8 website. 

Upgrade strategy #

There are multiple tools that can help you upgrade from .NET Framework to .NET Core. Microsoft can help you with the Upgrade Assistant and AWS has the Porting Assistant for .NET.

These tools will not get you all the way but are supposed to help.

Instead of using these porting tools, you can also manually upgrade which is the approach I took. 

I moved the existing projects into a folder called 'v8'. Then I created a new Umbraco 9 instance.
To do this, I had to first install the templates into the .NET CLI:

dotnet new -i Umbraco.Templates::*

This introduces two new templates: umbraco and umbracopackage.
Using the .NET CLI I created a new Umbraco project:

dotnet new umbraco

I updated the umbracoDbDSN connectionstring inside of appSettings.json file with the previous connectionstring and ran the project using the .NET CLI:

dotnet run

When you browse to your new Umbraco 9 website, it'll prompt you to upgrade the database from 8 to 9.

After upgrading the database, I stopped the website.

I started copying all the custom code and templates from the 'v8' projects over to the new Umbraco 9 project.

The custom code wouldn't compile because of the changes made to .NET and Umbraco.
This is the hard part of the upgrade. You have to update the code from ASP.NET Framework APIs to ASP.NET Core APIs and from Umbraco 8 APIs to Umbraco 9 APIs.

Code changes to Umbraco #

NuGet packages #

The first thing you should take note of is that the names of the Umbraco NuGet package have changed. The Umbraco 9 packages all start with "Umbraco.Cms" whereas previous packages started with "Umbraco" or "UmbracoCms". Pay attention to the added dot between "Umbraco" and "Cms" for the new packages. 

Here's an example of the Umbraco 8 NuGet packages in my packages.config file:

<package id="Umbraco.ModelsBuilder" version="8.1.6" targetFramework="net48" />
<package id="Umbraco.ModelsBuilder.Api" version="8.1.6" targetFramework="net48" />
<package id="Umbraco.ModelsBuilder.Ui" version="8.1.6" targetFramework="net48" />
<package id="Umbraco.SqlServerCE" version="4.0.0.1" targetFramework="net48" />
<package id="UmbracoCms" version="8.16.0" targetFramework="net48" />
<package id="UmbracoCms.Core" version="8.16.0" targetFramework="net48" />
<package id="UmbracoCms.Web" version="8.16.0" targetFramework="net48" />

Compare that with the Umbraco 9 NuGet packages from the project files:

<PackageReference Include="Umbraco.Cms" Version="9.0.0" />
<PackageReference Include="Umbraco.Cms.Core" Version="9.0.0" />
<PackageReference Include="Umbraco.Cms.Infrastructure" Version="9.0.0" />
<PackageReference Include="Umbraco.Cms.Web.Website" Version="9.0.0" />

Namespaces #

As the names of NuGet packages have changed, so have the namespaces. Although not all namespaces can be mapped one on one. Some APIs have moved into new namespaces and some namespaces are gone.
Here's an overview of the namespaces I had to update/add:

Umbraco 8 Namespace

Umbraco 9 Namespace

Umbraco.Core

Umbraco.Cms.Core

Umbraco.Core.Composing

Umbraco.Cms.Core.Composing

Umbraco.Web

Umbraco.Cms.Core.Web

Umbraco.Web.Routing

Umbraco.Cms.Core.Routing

Umbraco.Core.Models.PublishedContent

Umbraco.Cms.Core.Models.PublishedContent

Umbraco.Core.Logging

Microsoft.Extensions.Logging

Umbraco.Web.Mvc

Umbraco.Cms.Web.Common.Controllers

Umbraco.ModelsBuilder

Umbraco.Cms.Infrastructure.ModelsBuilder

Umbraco.ModelsBuilder.Umbraco

Umbraco.Cms.Infrastructure.ModelsBuilder

Umbraco.Web.Macros.PartialViewMacroPage

Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage

 

Umbraco.Cms.Core.Services

 

Umbraco.Cms.Web.Common.UmbracoContext

 

Umbraco.Extensions

 

Umbraco.Cms.Core.DependencyInjection

 

Umbraco.Cms.Core.PublishedCache

 

Umbraco.Cms.Core.Services

Umbraco API Changes #

Even though a lot of the APIs have moved to new namespaces, many of them haven't changed. Some of them have though, so here's an overview of the ones I encountered during my upgrade.

Umbraco Extensions #

Lots of methods are now gone from the content model classes. Instead, you can now find them as extension methods in the Umbraco.Extensions namespace. There are many more static extension classes available, but I've only needed to use the extension methods from the static FriendlyPublishedContentExtensions class so far.

Logging #

In Umbraco 8, Serilog was wrapped by Umbraco's own logging API. Umbraco 9 still uses Serilog, but instead of using its own logging API to wrap it, Umbraco now uses ASP.NET Core's built-in logging APIs. 

That's why the namespaces table shows a mapping from Umbraco.Core.Logging to Microsoft.Extensions.Logging.

Here's a sample showing how to log info and warning:

Umbraco 8:

using Umbraco.Core.Logging;

public class LoggingExample
{
    private readonly ILogger logger;

    public LoggingExample(ILogger logger)
    {
        this.logger = logger;
        logger.Info<LoggingExample>("Info log");
        logger.Warn<LoggingExample>("Warning log");
    }
}

Umbraco 9:

using Microsoft.Extensions.Logging;
public class LoggingExample
{
    private readonly ILogger<LoggingExample> logger;

    public LoggingExample(ILogger<LoggingExample> logger)
    {
        this.logger = logger;
        logger.LogInformation("Info log");
        logger.LogWarning("Warning log");
    }
}
RenderMvcController to RenderController #

The RenderMvcController has been renamed to RenderController. There are some additional changes with RenderController:

  • You have to implement a constructor and pass in the three mandatory parameters.
  • Instead of having your ContentModel injected into your action, you can have your typed content model injected.
  • Many properties from RenderMvcController have been removed.

Here's a sample to demonstrate the change:

Umbraco 8:

using System.Web.Mvc;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;

public class SampleController : RenderMvcController
{
    [HttpGet]
    public ActionResult Index(ContentModel model)
    {
        var sampleTypedContent = (SampleTypedContent)model.Content;
        var sampleViewModel = new SampleViewModel();
        // do stuff
        return View("SampleView", sampleViewModel);
    }
}

Umbraco 9:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Common.Controllers;

public class SampleController : RenderController
{
    public SampleController(
        ILogger<SampleController> logger,
        ICompositeViewEngine compositeViewEngine,
        IUmbracoContextAccessor umbracoContextAccessor
    )
    : base(logger, compositeViewEngine, umbracoContextAccessor)
    {
    }

    [HttpGet]
    public ActionResult Index()
    {
        var sampleTypedContent = (SampleTypedContent)CurrentPage;
        var sampleViewModel = new SampleViewModel();
        // do stuff
        return View("SampleView", sampleViewModel);
    }
}

The Services property from the RenderMvcController class is no longer available in the RenderController class. If you need to get an instance of a service, you can rely on the built-in dependency injection. For example, here's how you can get access to an instance of IDataTypeService:

Umbraco 8:

using System.Web.Mvc;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;

public class SampleController : RenderMvcController
{
    [HttpGet]
    public ActionResult Index(ContentModel model)
    {
        var dataTypeService = Services.DataTypeService;
        // do stuff
        return View();
    }
}

Umbraco 9:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Common.Controllers;

namespace Blog.Core.Controllers
{
    public class BlogOverviewPageController : RenderController
    {
        private readonly IDataTypeService dataTypeService;

        public BlogOverviewPageController(
            ILogger<BlogOverviewPageController> logger,
            ICompositeViewEngine compositeViewEngine,
            IUmbracoContextAccessor umbracoContextAccessor,
            IDataTypeService dataTypeService
        )
        : base(logger, compositeViewEngine, umbracoContextAccessor)
        {
            this.dataTypeService = dataTypeService;
        }

        [HttpGet]
        public ActionResult Index()
        {
            // do stuff
            return View();
        }
    }
}
UrlProvider #

In a similar fashion, the UrlProvider property has been removed from the UmbracoContext class. I tried using dependency injection to get an instance of IUrlProvider, but that didn't work. Having the DefaultUrlProvider injected does work though, which was the URL provider I needed anyway.

Umbraco events #

In Umbraco 8, there were static events on the ContentService class which you could use to subscribe to when specific actions were taken on Umbraco content. For example, you could run a piece of code whenever content is published like this:

Umbraco.Core.Services.Implement.ContentService.Published += 
    (IContentService sender, ContentPublishedEventArgs e) => /* do stuff */ ;

In Umbraco 9, you have to use new APIs to achieve the same result.
In the Startup.cs file, Umbraco's services are configured in the ConfigureServices method like this:

services.AddUmbraco(_env, _config)
    .AddBackOffice()
    .AddWebsite()
    .AddComposers()
    .Build();

The AddUmbraco method returns an instance of IUmbracoBuilder. You can use the AddNotificationHandler method on IUmbracoBuilder to hook into the same events.
In the example below, the DummyNotificationHandler will be invoked when content is published:

services.AddUmbraco(env, config)
    .AddBackOffice()
    .AddWebsite()
    .AddComposers()
    .AddNotificationHandler<ContentPublishedNotification, DummyNotificationHandler>()
    .Build();

The DummyNotificationHandler looks like this:

using Umbraco.Cms.Core.Notifications;

public class DummyNotificationHandler : INotificationHandler<ContentPublishedNotification>
{
    public void Handle(ContentPublishedNotification notification)
    {
        // do stuff
    }
}

Alternatively, you can also use composers to register the notification handler:

using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;

public class DummyComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.AddNotificationHandler<ContentPublishedNotification, DummyNotificationHandler>();
    }
}

By implementing the IComposer interface, Umbraco will invoke this composer during startup and will pass in the same IUmbracoBuilder instance as in the Startup.cs file.

Umbraco Context #

The Umbraco context isn't available in all places it used to be. If you don't have access to the Umbraco context, you can use dependency injection to inject an instance of IUmbracoContextAccessor. Using IUmbracoContextAccessor you can request the UmbracoContext using the GetRequiredUmbracoContext method or the TryGetUmbracoContext method.

Model Builders #

Model Builders also changed in Umbraco 9. Previously, I used the API mode combined with the Visual Studio plugin in Umbraco 8, but it looks like that's no longer available (for now).
The reason I was using the API mode is that I'm storing the models in a separate class library. Luckily, there are new configuration options available to still achieve the same result.


This is the configuration I use to generate typed models in a class library (with irrelevant options omitted):

{
  "$schema": "./umbraco/config/appsettings-schema.json",
  "Umbraco": {
    "CMS": {
      "ModelsBuilder": {
        "ModelsDirectory": "~/../MyClassLibrary/Models",
        "AcceptUnsafeModelsDirectory": true,
        "ModelsNamespace": "MyClassLibrary",
        "ModelsMode": "SourceCodeAuto"
      }
    }
  }
}

After configuring this, you can go to the back office and click a button to generate the models.

Umbraco Configuration #

In Umbraco 8 you used multiple XML config files to configure the CMS. In Umbraco 9, the configuration uses the new configuration system from ASP.NET Core. You can pass in the configuration using multiple ways by default. Most commonly, you'll use the appSettings.json file for configuration, but you can also override those settings using command-line arguments, environment variables, user secrets, and more.
The configuration is mostly the same although some of the structure changed. Luckily it is well documented at our Umbraco

Umbraco StorageProviders #

In older versions of Umbraco, there was a community project to use Azure Storage to store files with Umbraco. For Umbraco 9, the Umbraco team themselves provide a project to integrate with Azure Storage. This new project does require you to put all your files into a subfolder named 'media' inside your Azure Storage container. The community project for previous Umbraco versions stored the files at the root of the Azure Storage container.

This discrepancy means you'll either have to move all your files. Instead of moving files, I created an issue and submitted a pull request to resolve this with some configuration options.

Changes from ASP.NET Framework to ASP.NET Core #

There is a long laundry list of changes between ASP.NET Framework and ASP.NET Core. Many features were added, some stayed the same, some slightly changed, and some were completely removed.
Here's a short list of changes I had to make for my upgrade:

  • Instead of using web.config with ConfigurationManager, you have to use the new configuration system which allows you to configure your app using command line arguments, appSettings.json files, environment variables, user secrets, and more.
  • User secrets in .NET framework uses a different API in combination with a secrets.xml file. In .NET Core, a secrets.json file is used.
  • The ChildAction feature has been removed in ASP.NET Core. You have to move your code to an alternative API. I replaced ChildActions with ViewComponents.
  • In ASP.NET Framework, you could place Razor helpers in App_Code and they would be available in all views. This is not available in ASP.NET Core, so you have to create .NET-based HTML helpers instead of razor code.

The last two changes were the biggest hurdle to get over. The amount of work to adjust from .NET Framework to .NET Core was significantly larger than adjusting to the updated Umbraco APIs.

Summary #

To upgrade from Umbraco 8 to Umbraco 9, you will have to make a lot of changes depending on the amount of custom code in your Umbraco website. Some of Umbraco's API changed, but they are relatively straightforward to implement. The biggest challenge in upgrading is due to the major changes from ASP.NET Framework to ASP.NET Core.
The improved speed, choice of OS, and choice of IDE make it all worth it though.

Related Posts

Related Posts

Umbraco, Azure, and Linux logo

Deploying Umbraco 9 to Azure App Service for Linux

- Umbraco
Learn how to create the Azure infrastructure using the Azure CLI to host an Umbraco 9 website using Azure SQL and Azure App Service for Linux, and how to deploy your Umbraco 9 site.
Spider Web with Umbraco logo

Crawling through Umbraco with Sitemaps

- Umbraco
Websites come in all shapes and sizes. Some are fast, some are beautiful, and some are a complete mess. Whether it's a high-quality site is irrelevant if people can’t find it, but search engines are here to help. Though the competition to get on first page is tough, this series will dive into some common practices to make your website crawlable.
Robot with Umbraco logo

Crawling through Umbraco with Robots

- Umbraco
The robots.txt file’s main purpose is to tell robots (Google Bot, Bing Bot, etc.) what to index for their search engine, and also what not to. Usually you want most of your website crawled by Google, such as blog posts, product pages, etc., but most websites will have some pages/sections that shouldn’t be indexed or listed in search engines.
Phone/Tablet/Laptop displaying Umbraco logo

Implementing Responsive Images in Umbraco

- Umbraco
The web platform has responsive image capabilities such as the srcset-attribute, sizes-attribute, and the picture-element. These capabilities may seem daunting sometimes. We'll learn how to make them available and maintainable to Umbraco content editors.
Umbraco logo

Introducing Umbraco's KeepAlive Ping configuration

- Umbraco
Umbraco 8.6 introduces the new keepAlive configuration inside of `umbracoSettings.config` which allows you to change "keepAlivePingUrl" and "disableKeepAliveTask"