Having now worked through the upgrade process on a large Umbraco implementation, we've encountered changes across the .NET application, Umbraco APIs, database migrations, property editors, templates, custom extensions, Forms, build pipelines and, importantly, existing content.
This post captures the main lessons we learned along the way and some of the things we would recommend checking before starting a similar upgrade.
1. Treat it as a migration, not a package upgrade
The biggest lesson is that an Umbraco 13 → 17 upgrade should be approached as a migration project.
At first glance, the process sounds relatively straightforward: update .NET, update Umbraco and update the project's NuGet packages.
In practice, several generations of changes are crossed during the upgrade. APIs have disappeared, AngularJS has been removed from the backoffice, old property editors are no longer supported, macros have gone, routing has changed and third-party packages may require their own migrations.
Our upgrade ultimately involved work across five broad areas:
Database and content migration
Backend/.NET code
Templates and frontend rendering
Backoffice extensions and property editors
Build and deployment infrastructure
The important implication is that the amount of work cannot be estimated simply by looking at the number of NuGet packages that need updating.
2. Get Umbraco 13 completely up to date first
One of the most useful steps was upgrading the existing application to the latest available Umbraco 13 patch versions before attempting the move to 17.
We restored copies of the production Umbraco and Umbraco Engage databases locally and ran the site using the latest compatible versions of Umbraco CMS, Engage, Forms, Workflow and the other dependencies.
This allowed Umbraco to migrate the databases to the latest v13 schemas before introducing the much larger v17 changes.
For Engage, there was an additional step: running its post-migration schema alignment scripts. Because our implementation stores Engage in a separate database, the scripts also needed to be configured for separate database mode.
The lesson is simple: start a major upgrade from the cleanest and most current version of your existing platform that you can.
It removes one source of uncertainty when later migrations fail.
3. Prepare your content before upgrading
One thing we would do wherever possible is fix known content and configuration problems while the site is still running Umbraco 13.
We discovered several examples.
Vary by Culture
Some blocks had "Vary by Culture" enabled even though their parent blocks or content did not.
Umbraco 17 handles this configuration more strictly and could no longer display or edit the affected content.
Rather than trying to repair this after migrating, we changed the relevant configuration on the existing v13 site before performing the final upgrade.
Legacy Media Pickers
Properties using the old Umbraco.MediaPicker also needed moving to Media Picker 3.
Again, making this change in v13 had a significant advantage: we could confirm that the existing media selections were preserved before introducing the additional complexity of v17.
This became a useful general principle:
If a compatibility problem can safely be fixed in the old version first, do it there.
It makes the eventual upgrade considerably easier to reason about.
4. Database migrations can take a long time
When we first ran the application against Umbraco 17, the site remained in its "Upgrading" state for a surprisingly long time.
This wasn't necessarily a problem.
Some of the intermediate migrations make extensive alterations to existing content, and on a substantial production database the migration process can take more than 30 minutes.
Rather than assuming the application has stalled, watch the Umbraco logs.
For example, during local development we tailed the log from PowerShell:
Get-Content C:\path\to\umbraco\Logs\UmbracoTraceLog.json -WaitBeing able to see migration activity makes it much easier to distinguish a slow migration from a failed one.
We also added application logic to prevent background synchronisation jobs from running while an upgrade is taking place. On applications with scheduled or background processes, this is worth considering before starting migrations.
5. Permissions can break an otherwise successful migration
One particularly awkward issue involved the Umbraco Super User.
A Workflow migration required permissions that the Super User did not have. The result wasn't simply a failed feature: the entire application became stuck in its upgrading state, preventing access to both the website and backoffice.
The solution was to ensure that the Super User belonged to the Administrators group before attempting the upgrade.
This is exactly the kind of small configuration issue that can consume a disproportionate amount of debugging time.
Our takeaway is to check administrator and migration permissions before beginning, particularly where packages such as Workflow perform their own migrations.
6. Expect substantial API changes
A large part of the development effort was replacing APIs that either no longer exist or work differently.
Some examples from our project included:
GetAtRoot()
GetById()
GetByXPath()
ChildrenForAllCultures()
SaveAndPublish()
UseInstallerEndpoints()For example, calls to GetAtRoot() needed moving to the newer ContentAtRoot() approach.
We also had code relying on the old Image generated model. That model is no longer available in the same way, so image handling needed to move towards types such as MediaWithCrops or project-specific models.
There were additional changes around language IDs and accessing site settings for the current site.
A useful preparation exercise is therefore to search the entire solution for removed and deprecated APIs before starting implementation. It won't find every problem, but it provides a much more realistic picture of the work ahead.
7. API controllers need special attention
Custom Umbraco API controllers are another area where simply fixing compilation errors isn't enough.
Older controller types such as UmbracoApiController and UmbracoAuthorizedApiController have changed or disappeared.
Depending on their purpose, controllers need moving to the appropriate modern ASP.NET/Umbraco controller base, such as ControllerBase or ManagementApiControllerBase.
Routing also needs reviewing.
Custom endpoints can no longer simply assume that old /umbraco/... routes will continue working. We had to review controller attributes, remove attributes that were no longer required and explicitly add missing routes.
And because APIs are consumed somewhere, changing a server route may also mean updating JavaScript or other clients.
The lesson here is to test APIs end-to-end, rather than considering them fixed as soon as the C# compiles.
8. AngularJS removal is one of the biggest backoffice changes
For projects with a heavily customised backoffice, the removal of AngularJS is likely to represent a significant proportion of the upgrade effort.
Our application contained several custom App_Plugins, including custom dropdowns and restricted fields.
These don't simply need updating. In many cases they need rewriting using the current extension architecture.
For example, we rebuilt our custom Theme Dropdown as a Vite-based Umbraco 17 extension.
This change also affected smaller pieces of backoffice configuration.
Block editor labels that previously used syntax such as:
{{heading}}now use Umbraco Flavoured Markdown (UFM) syntax:
{=heading}Custom dashboard permission logic also stopped compiling. Previously this had been controlled through C# composers. We replaced that implementation with a new DashboardRestrictions extension for v17.
The practical lesson is that the size of your App_Plugins directory is a useful indicator of upgrade complexity.
9. Don't automatically rebuild every customisation
A major upgrade is also an opportunity to ask whether older custom functionality is still necessary.
We had a custom restricted-dropdown implementation controlling which users could access a particular setting.
Umbraco 17 provides more fine-grained permissions, meaning the old custom property editor was no longer the best solution.
Instead, the property could become a standard Umbraco Dropdown with appropriate permissions applied.
Similarly, we removed an unused Local Site Builder and unused Atlassian services rather than investing time migrating them.
This is an important distinction:
Compatibility does not always mean rebuilding the old implementation.
Before porting a custom extension, check whether newer Umbraco functionality has made it redundant.
10. Macros need to go
Macros are no longer supported, so applications that make extensive use of them will require template work.
Our project used macro partials for functionality including:
Body content
Hero carousels
Meta information
Google Tag Manager
Emergency banners
Breadcrumbs
Footer navigation
Social sharing and social links
Logos
Site, office and job search
Contact details and opening hours
These calls were rewritten using approaches supported by Umbraco 17.
Macros embedded inside Rich Text Editors required some additional thought because this wasn't purely a template problem — the macro existed inside stored content.
For Forms, we created a new Form Block component and migration code that finds existing form macros inside rich-text content and replaces them with the new block representation.
That leads to another important lesson: don't search only your source code for obsolete functionality. Search your content too.
11. Property editors can preserve data even when their UI disappears
One initially alarming message in the v17 backoffice was:
The configured property editor UI could not be found.
Fortunately, a missing editor doesn't necessarily mean the underlying content has disappeared.
We encountered several legacy/custom editors that were no longer available, including JSON-based editors, a responsive-image editor and restricted dropdowns.
In some cases we could change the data type to a supported editor while preserving its existing value. Our responsive image functionality, for example, was moved to Media Picker 3 and the associated image-processing code updated.
This is an area where migration scripts can be extremely valuable. We created migrations to populate replacement properties across existing pages rather than expecting editors to repair content manually.
The priority should therefore be preserving and transforming the data, rather than preserving the exact editor that originally created it.
12. Forms deserve their own testing plan
Umbraco Forms introduced several separate upgrade tasks.
We needed to:
Update a workflow using
SendRazorEmailFix Forms scripts and CSS that were no longer loading
Resolve compilation problems in custom form templates
Replace form macros embedded in rich-text content
Create new Form Block components
Fix settings that were no longer editable in custom workflows
Forms can touch the backoffice, frontend, email, stored content and custom code simultaneously.
For sites where lead generation or enquiries are business-critical, Forms should therefore have a dedicated regression test plan rather than being treated as just another NuGet dependency.
13. Compilation is only the beginning
Once the application compiled and Umbraco started, we still found a range of frontend regressions.
These included:
Layouts such as full-width, 50/50 and three-column components rendering incorrectly
Spotlight images disappearing
Theme information not loading
Page titles and meta descriptions disappearing
Search-result images not rendering
Forms assets not loading
We also removed Smidge from the frontend templates as part of the upgrade.
None of these problems necessarily presents itself as a compiler error.
A successful build therefore isn't a successful migration. Visual and functional regression testing across representative content is essential.
14. Don't forget the build pipeline
The application isn't upgraded if it only works on a developer's machine.
Moving the project to .NET 10 meant the build and release pipeline also needed updating.
We replaced deprecated pipeline steps, updated the .NET build configuration and fixed the frontend build process.
Infrastructure should be part of the upgrade plan from the beginning rather than something addressed after development is supposedly complete.
What we would do again
Based on the experience, our preferred sequence for another Umbraco 13 → 17 migration would be:
Audit the existing application for deprecated APIs, packages, macros, custom property editors and AngularJS extensions.
Restore production databases locally so the upgrade is tested against realistic content.
Upgrade everything to the latest compatible v13 versions and allow all v13 database migrations to complete.
Run any package-specific migration scripts, including products such as Engage.
Fix known content/configuration incompatibilities in v13 wherever possible.
Verify administrator and migration permissions.
Move the application and packages to v17 and perform the database migration.
Watch the migration logs rather than assuming a long-running migration has failed.
Resolve backend API and controller changes.
Replace or redesign obsolete backoffice extensions.
Migrate macros and other legacy content representations.
Run migrations for existing content where necessary.
Regression-test templates, Forms, search, media, metadata and backoffice editing.
Update and test the complete build and release pipeline.
The biggest lesson
Perhaps the biggest takeaway from the whole process is that an Umbraco major-version upgrade is as much about existing content and customisations as it is about code.
The obvious work is updating .NET, NuGet packages and APIs. The less obvious work is discovering assumptions accumulated over years: an AngularJS editor somebody built for one field, a macro embedded inside hundreds of rich-text values, an old media picker, a culture setting that newer Umbraco versions enforce more strictly, or a user permission that prevents a migration completing.
The more of those assumptions you identify before pressing "upgrade", the more predictable the migration becomes.
For projects moving from Umbraco 13 to 17, we'd recommend budgeting time not just for getting the application compiling, but for content migration, backoffice redevelopment and thorough regression testing.
The good news is that the process also provides an opportunity to remove years of accumulated technical debt. We didn't simply make the old application run on Umbraco 17: we removed obsolete services, retired unnecessary customisations, adopted newer native functionality and replaced legacy approaches with implementations that should be easier to maintain going forward.
That, ultimately, is where the value of a major upgrade lies.