Thoughts and tips on moving to Umbraco 9 from Umbraco 8
Niels Swimberghe - - Umbraco
Follow me on Twitter, buy me a coffee
.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.