Jamie Townsend is a Principal Engineer and Umbraco Master at Shout. He spoke on this topic at our Umbraco Newcastle Meet-up — slides and all code examples are available on GitHub over at:
Getting started
I'm going to skip the "why upgrade" pitch. If you're reading this, you've been tasked with the job. What you need is the practical guidance to actually do it. For context: Umbraco 17 and .NET 10 are the current and next LTS versions respectively, so this upgrade is coming for most of us sooner or later.
Upgrade to the latest v13 first
Strictly speaking, this isn't always necessary (I've skipped it without issue) but the official recommendation is to upgrade to the latest v13 patch before moving to v17. It's worth following for peace of mind.
Back everything up
Do a full backup of both media and database, then restore locally before you start. In our experience, the upgrade migration itself takes around 30–40 minutes for an average-sized site.
Review your Umbraco packages
Check every installed Umbraco package for a .NET 10 / Umbraco 17 compatible version. Most popular packages have one. If something you depend on doesn't, raise it with the maintainer or consider a PR.
Review your NuGet packages
Do a full audit of your other NuGet dependencies and check compatibility with .NET 10. This is worth doing methodically — surprises here can cost significant time later.
Check for custom backoffice extensions
If your project has any custom backoffice extensions built with AngularJS, they will no longer work and will need to be rewritten. Depending on their complexity, and whether there's a skill gap around TypeScript, Lit, or Web Components, this can be a significant piece of work in its own right.
Deprecated property editors and macros
We'll cover these in detail below, but flag them early. If you have heavy usage of deprecated property editors or macros, consider migrating those before upgrading to v17 (it keeps the upgrade itself cleaner).
Make a plan
Once you know what gaps exist — unresolved packages, incompatible NuGet dependencies, custom backoffice work — log it, task it and plan for it. Breaking changes from package updates deserve their own testing time.
Getting the solution building
With your plan in hand, the next priority is simply getting the project to build. There will be errors. Likely quite a few. My approach? Fix the straightforward ones first, then comment out anything more involved with a TODO comment so you can work through each issue in isolation alongside the v13 branch.
Update packages
Update packages to versions compatible with Umbraco 17 and .NET 10. For anything still unresolved, get the project building first and return to it. Trying to tackle everything at once makes it harder to isolate what's actually breaking.
Razor runtime compilation
Razor runtime compilation and the Models Builder InMemoryAuto mode have both moved to a separate NuGet package:
Umbraco.Cms.DevelopmentMode.Backoffice
Package
https://www.nuget.org/packages/Umbraco.Cms.DevelopmentMode.Backoffice/
That said, Microsoft has marked Razor runtime compilation as Obsolete, and it was also what was blocking Umbraco's hot-reload support. Hot-reload now works out of the box in v17, so this is a good opportunity to move away from it if you haven't already.
Smidge
Smidge is no longer included in the core. If you want to keep using it, install the community package:
TinyMCE → TipTap
Umbraco has moved from TinyMCE to TipTap as its rich text editor. The reason: TinyMCE changed its licence from MIT to GPLv2, which requires any code built on top of it to also be open-sourced unless you purchase a commercial licence — which also comes with a cap on editor load counts.
For most projects, the migration to TipTap is largely automatic on upgrade. The one gap we've encountered is that TipTap doesn't yet appear to support the contextual styles dropdown that TinyMCE offered (for example, showing table-specific styles when a table is selected). We're still working through this and may need custom TipTap extensions — worth factoring into your planning.
If you'd rather stay on TinyMCE for now, ProWorks have released a package that prevents the TipTap migration and restores the TinyMCE dependencies. Install it before upgrading:
Delete bin and obj directories
'Clean Solution' alone isn't always sufficient. Several developers have reported issues that were resolved only after manually deleting all bin and obj folders across every project in the solution before running the upgrade.
Fixing things up
Hybrid chache
Introduced in Umbraco 15, Hybrid Cache replaces the previous approach of loading everything into memory at startup. The old approach caused long boot times on content-heavy sites; Hybrid Cache uses a lazy-loading model instead, only loading content into the cache on demand. The practical impact: methods like .Descendants() or .Children() may no longer behave as expected if the content they need isn't in cache. You can control what gets pre-cached via appsettings:
"Umbraco": {
"CMS": {
"Cache": {
"ContentTypeKeys": ["yourContentTypeAlias"],
"DocumentBreadthFirstSeedCount": 100,
"MediaBreadthFirstSeedCount": 50
}
}
}
Umbraco ships with two seed key providers out of the box:
ContentTypeSeedKeyProvider — caches all content of types specified in ContentTypeKeys
BreadthFirstKeyProvider — seeds N items via breadth-first traversal, as configured in DocumentBreadthFirstSeedCount and MediaBreadthFirstSeedCount
You can also implement your own ISeedKeyProvider for more granular control.
XPATH removal
XPATH support has been removed. If you commented out XPATH-based code during the build step, now's the time to address it. The code changes themselves are generally straightforward — the trickier problem is the Multinode Tree Picker.
If any of your Multinode Tree Picker data types were using XPATH for filtering, the upgrade will have silently wiped that configuration. You'll need to query the v13 database to retrieve the original settings, then reconfigure each data type to use Dynamic Root instead.
My preferred approach: compare v13 and v17 side by side, reconfigure the pickers in v17 to use Dynamic Root, then use uSync to commit those changes to your repository so they're included in your deployment. Alternatively, write custom migration scripts — though these are easier to run before the upgrade while the original config is still accessible.
Newtonsoft.Json → System.Text.Json
This one caught me out more than I expected. The documentation describes it well:
"Although this sounds like it's not a big change, it's one of the most breaking changes on the backend. Whereas Newtonsoft.Json was flexible and error-tolerant by default, System.Text.Json is strict but more secure by default."
That strictness shows up in practice. Custom data types, property editors, API controllers, and Property Value Converters are all potential problem areas. Plan for either migrating these to System.Text.Json or, at minimum, thorough regression testing to surface any serialisation issues.
Umbraco Flavored Markdown (UFM)
With AngularJS removed, any advanced labels in Blocks or Collection Views will be broken. The fix is to update their syntax to UFM. Joe Glombek has written a particularly useful post on this — linked below. We had a large number of these across the project, so I've also included a SQL script in the examples repo that helps you find them all efficiently.
UFM documentation | Joe Glombek's guide | Full block element label example
Macros
Macros have been removed. There are two main scenarios to handle:
Macro partial views (called directly from templates)
These are the more straightforward case. For each macro:
Create an element type with the same properties as the macro
Create a new Partial View that accepts this element as its model
Copy the content from the old Macro Partial View into the new partial
Update your templates to call @Html.PartialAsync() with the new model
RTE macros (inserted into rich text fields)
These are more involved. The initial setup is the same — create a matching element type — but you also need to:
Edit the RTE data type to allow a Block using your new element
Create a Partial View in Partials/richtext/Components/ and update the content
Update the RTE field data itself — the stored content uses macro syntax that must be replaced with Block syntax
If you only have a small number of RTE macro instances, manual cleanup may be the fastest route. For larger volumes, tools like uSync.Migrations, Umbraco Deploy, or custom migration scripts are worth considering — ideally run before the v17 upgrade so you're working with the original data.
Migrating macros — official docs | Migrating RTE macros — Joe Glombek
Packages and backoffice extensions
The backoffice rewrite is substantial — enough for its own dedicated talk, honestly. The shift is from server-side C# extensions to client-side Web Components. The learning curve is real, but you get used to it quickly. Running the v13 site alongside v17 and working through issues one at a time is the most effective approach.
Registering extensions via manifest
The recommended approach is a umbraco-package.json manifest file. My preferred alternative is to implement IPackageManifestReader in C# — there's limited documentation on this (I found it by accident while debugging), but it has two advantages: it aligns more closely with how things worked in v13, and it lets you set the version number dynamically. IPackageManifestReader example | Full extension type reference
Validating extensions
Umbraco v17 includes a new Extensions section in the backoffice where you can view all registered extensions. This is invaluable for debugging — you can compare your custom extensions against Umbraco's built-in ones as a reference.
Full end-to-end example
I've put together a complete working example that covers a lot of ground in a compact package:
A package defined via manifest
A Management API Controller that returns appsettings as JSON
A client-side Web Component that generates a Bearer token and calls the API
Client-side logic that conditionally hides Sections and Dashboards based on the response
Deprecated property editors
Legacy property editors have been formally removed in v14 (and therefore v17). The migrations to plan for:
Nested Content → Block List or Block Grid
Grid → Block Grid
MediaPicker → MediaPicker3
If you have significant usage of these, consider migrating before the v17 upgrade.
uSync Migrations
In my experience, uSync.Migrations is the easiest path for migrating legacy property editors. Most are handled out of the box. For anything not covered, you can write your own Migrator — the framework makes this reasonably straightforward.
Custom Package Migrations
If you want more control, custom package migrations let you modify data types directly via IDataTypeService. The key benefit: Umbraco tracks which migrations have run, so only new ones are executed on each deployment — a clean and reliable approach.
API controllers
Umbraco 14 removed several base classes that were widely used for building APIs.
UmbracoApiController and PluginController
Both have been removed. The recommended replacement is to base your controllers on the standard ASP.NET Core Controller class. One key difference: previously, UmbracoApiController auto-routed to /umbraco/api/[ControllerName]/[ControllerAction]. Going forward, you define your routes explicitly using the [Route] attribute.
UmbracoAuthorizedApiController and UmbracoAuthorizedJsonController
These have also been removed. The replacement is ManagementApiControllerBase, which plugs into Umbraco's Management API. It's a bit more involved to set up, but well worth it for backoffice-authenticated endpoints.
Umbraco Forms
In our experience, the Forms upgrade from v13 to v17 is relatively smooth. Most customisation was done in C# and remains largely unchanged — custom workflows, field types, and so on all follow the same patterns.
The exception is backoffice presentation. If you have custom field types with bespoke preview or editing UIs, these now need to be implemented as client-side Lit/Web Components. For simpler field types where backoffice preview and in-office editing aren't a priority, you may be able to defer this.
Adding a custom field type (including Lit/Web Component examples)
Property UIs
The value used to define a field or setting property view has changed as a breaking change. Any custom field types or workflow settings you have defined will need updating.
Themes
Given we're jumping several major versions, I'd recommend re-aligning with the default theme, render partials, and script partials to make sure nothing has been missed. Umbraco Forms now uses RCL (Razor Class Libraries), so you can't just browse the default theme in-project — download it from the documentation instead.
Umbraco licence changes
Umbraco has moved all its commercial products to a subscription licensing model. For most projects, the biggest impact is on Umbraco Forms: clients who previously had a one-off licence installed as a .lic file need to delete that file and move to the new subscription model. The configuration has also changed — you now use a licence key in appsettings rather than a .lic file. If you've worked with Umbraco Engage you'll already be familiar with this pattern, though the appsettings path has changed.
Umbraco 17 release blog post (licence details) | Official licence configuration docs