Umbraco v8 to v17 Migration Patterns Reference¶
A before/after reference for developers migrating Razor views and C# code from Umbraco v8 to v17.
1. XPath Queries to Dependency-Injected Services¶
v8 used XPath to traverse the content tree and find configuration nodes. v17 replaces this with scoped services registered in the DI container and injected into views.
v8:
@{
var settings = Umbraco.ContentSingleAtXPath("//websiteConfiguration");
var logo = settings.GetPropertyValue<IPublishedContent>("logo");
var siteName = settings.GetPropertyValue<string>("siteName");
}
v17:
@inject ISiteSettingsService SiteSettings
@{
var website = SiteSettings.Website;
var logo = website.Value<MediaWithCrops>("logo");
var siteName = website.Value<string>("siteName");
}
XPath queries are slow, uncacheable, and fragile when the content tree changes. The SiteSettingsService resolves settings once per request and caches the result in RuntimeCache, eliminating repeated tree traversals.
2. ClientDependency to Static Tags with asp-append-version¶
v8 used the ClientDependency framework to bundle and version CSS/JS. v17 uses plain <link> and <script> tags with ASP.NET Core's built-in asp-append-version tag helper for cache busting.
v8:
@{
Html.RequiresCss("~/vendor/bootstrap/css/bootstrap.min.css", 0);
Html.RequiresCss("~/content/CommonV2.css", 10);
Html.RequiresJs("~/Scripts/progressCore.js", 13);
}
v17:
<link rel="stylesheet" href="~/vendor/bootstrap/css/bootstrap.min.css" asp-append-version="true" />
<link rel="stylesheet" href="~/content/CommonV2.css" asp-append-version="true" />
<script src="~/Scripts/progressCore.js" asp-append-version="true"></script>
ClientDependency was removed in Umbraco v9+. The asp-append-version="true" tag helper appends a hash query string (e.g., ?v=abc123) that changes when the file content changes, providing automatic cache busting without a bundling framework.
3. CachedPartial to CachedPartialAsync¶
v8's synchronous CachedPartial is replaced by the async version in v17. The cache duration parameter changes from seconds (int) to a TimeSpan.
v8:
v17:
@await Html.CachedPartialAsync("SiteLayout/header", Model, TimeSpan.FromHours(1), cacheByPage: true)
ASP.NET Core Razor uses async rendering throughout. Synchronous partial rendering is not available. The named parameter cacheByPage: replaces the positional boolean.
4. Grid Layout to BlockGrid¶
v8 Grid views used @Html.GetGridHtml() or custom grid renderers. v17 uses the BlockGrid editor with dedicated component views per element type.
v8 page template:
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage
@{
Layout = "master.cshtml";
Html.GetGridHtml(Model, "grid", "BootstrapGrid")
}
v17 page template:
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@{
Layout = "master.cshtml";
}
<div class="container page-content">
@await Html.GetBlockGridHtmlAsync(Model, "grid")
</div>
v8 grid editor partial (e.g., headline.cshtml):
v17 BlockGrid component (e.g., headline.cshtml):
@using Umbraco.Cms.Core.Models.Blocks
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<BlockGridItem>
@{
var content = Model.Content;
var heading = content.Value<string>("heading");
}
<h2>@heading</h2>
The Grid editor was removed in Umbraco v14. Each grid editor becomes a dedicated element type with its own BlockGrid component view. Content is accessed through Model.Content (the element type) and Model.Settings (the settings element type).
5. Nested Content to BlockList¶
v8 Nested Content properties become BlockList properties in v17. The iteration pattern changes from IPublishedContent items to BlockListItem items.
v8:
@{
var items = Model.Value<IEnumerable<IPublishedContent>>("slides");
foreach (var item in items)
{
var title = item.GetPropertyValue<string>("title");
<h3>@title</h3>
}
}
v17:
@using Umbraco.Cms.Core.Models.Blocks
@{
var blockList = Model.Content.Value<BlockListModel>("slides");
if (blockList != null)
{
foreach (var block in blockList)
{
var title = block.Content.Value<string>("title");
<h3>@title</h3>
}
}
}
Nested Content was removed in Umbraco v14. BlockList provides the same repeatable-content capability with a structured block model. Each item has .Content and .Settings properties, mirroring BlockGrid.
6. IPublishedContent Media to MediaWithCrops¶
v8 media pickers returned IPublishedContent. v17's MediaPicker3 returns MediaWithCrops for single-pick or IEnumerable<MediaWithCrops> for multi-pick.
v8:
@{
var image = Model.Value<IPublishedContent>("heroImage");
if (image != null)
{
<img src="@image.Url" alt="@image.Name" />
}
}
v17 (single-pick):
@{
var image = content.Value<MediaWithCrops>("heroImage");
if (image != null)
{
<img src="@image.MediaUrl()" alt="@image.Content.Name" />
}
}
v17 (multi-pick):
@{
var images = content.Value<IEnumerable<MediaWithCrops>>("gallery");
foreach (var img in images ?? Enumerable.Empty<MediaWithCrops>())
{
<img src="@img.MediaUrl()" alt="@img.Content.Name" />
}
}
Using the wrong type silently returns null. When "multiple": false is set on the data type, you must use MediaWithCrops (singular), not IEnumerable<MediaWithCrops>. The URL is accessed via .MediaUrl() method, not a .Url property.
7. Surface Controllers to ViewComponents¶
v8 Surface Controllers that rendered child actions are replaced by ASP.NET Core ViewComponents in v17.
v8:
// Controller
public class SearchSurfaceController : SurfaceController
{
[ChildActionOnly]
public ActionResult RenderSearchForm()
{
return PartialView("SearchForm", new SearchModel());
}
}
// In Razor view
@Html.Action("RenderSearchForm", "SearchSurface")
v17:
// ViewComponent
public class SearchFormViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View(new SearchModel());
}
}
// In Razor view
@await Component.InvokeAsync("SearchForm")
@Html.Action and [ChildActionOnly] do not exist in ASP.NET Core. ViewComponents are the direct replacement for child actions that render HTML fragments. Surface Controllers that handle form posts should migrate to regular ASP.NET Core controllers or Umbraco API controllers.
8. web.config Rewrite Rules to Database-Driven Rewrites¶
v8 used IIS web.config rewrite rules. v17 uses a custom ProgressUrlRewrites database table with ASP.NET Core middleware.
v8 (web.config):
<system.webServer>
<rewrite>
<rules>
<rule name="Old Page Redirect" stopProcessing="true">
<match url="^old-page$" />
<action type="Redirect" url="/new-page" redirectType="Permanent" />
</rule>
</rules>
</rewrite>
</system.webServer>
v17 (database table + middleware):
ProgressUrlRewrites table:
| OldUrl | NewUrl | RedirectType |
|------------- |------------|--------------|
| /old-page | /new-page | 301 |
Rewrites are managed through the CMS backoffice or directly in the database, making them editable without deployments. ASP.NET Core middleware reads the table and issues redirects at runtime.
9. Razor @inherits Directive Changes¶
The base page class changed namespace and name between v8 and v17.
v8 template page:
v17 template page:
v8 typed view:
v17 typed view (BlockGrid component):
@using Umbraco.Cms.Core.Models.Blocks
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<BlockGridItem>
v17 typed view (BlockList component):
@using Umbraco.Cms.Core.Models.Blocks
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<BlockListItem>
The UmbracoTemplatePage class was removed. All views now use UmbracoViewPage or UmbracoViewPage<T>. The namespace moved from Umbraco.Web.Mvc to Umbraco.Cms.Web.Common.Views.
10. Property Access Patterns¶
v8 had multiple ways to read property values. v17 standardises on Value<T>().
v8:
// Multiple patterns existed:
var title = Model.GetPropertyValue<string>("title");
var title = Model.Content.GetPropertyValue("title");
var title = Model.GetPropertyValue("title", "Default");
var flag = Model.GetPropertyValue<bool>("isEnabled");
// Dynamic access was also common:
var title = CurrentPage.title;
v17:
// Single consistent pattern:
var title = Model.Value<string>("title");
var title = content.Value<string>("title") ?? "Default";
var flag = content.Value<bool>("isEnabled");
// Inside BlockGrid/BlockList components:
var title = Model.Content.Value<string>("title");
GetPropertyValue was removed. Dynamic access via CurrentPage is no longer supported. The Value<T>() extension method is the only way to read property values. Always use null-coalescing (??) for defaults since Value<T> returns default(T) when the property is empty.
11. Bootstrap 4 to Bootstrap 5 Attribute Changes¶
Bootstrap 5 renamed all data- attributes to use the data-bs- prefix. jQuery is no longer a Bootstrap dependency (though it may still be used by other libraries).
v8 (Bootstrap 4):
<button data-toggle="modal" data-target="#myModal">Open</button>
<div class="modal" id="myModal">
<div class="modal-dialog">
<div class="modal-content">
<button data-dismiss="modal">Close</button>
</div>
</div>
</div>
<div class="carousel" data-ride="carousel" data-interval="5000">
<ol class="carousel-indicators">
<li data-target="#carousel" data-slide-to="0" class="active"></li>
</ol>
</div>
<div class="accordion">
<div data-toggle="collapse" data-target="#section1" data-parent="#accordion"></div>
</div>
v17 (Bootstrap 5):
<button data-bs-toggle="modal" data-bs-target="#myModal">Open</button>
<div class="modal" id="myModal">
<div class="modal-dialog">
<div class="modal-content">
<button data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
<div class="carousel" data-bs-ride="carousel" data-bs-interval="5000">
<div class="carousel-indicators">
<button data-bs-target="#carousel" data-bs-slide-to="0" class="active"></button>
</div>
</div>
<div class="accordion">
<div data-bs-toggle="collapse" data-bs-target="#section1" data-bs-parent="#accordion"></div>
</div>
Key Bootstrap 5 Changes Summary¶
| Bootstrap 4 | Bootstrap 5 |
|---|---|
data-toggle |
data-bs-toggle |
data-target |
data-bs-target |
data-dismiss |
data-bs-dismiss |
data-parent |
data-bs-parent |
data-ride |
data-bs-ride |
data-slide |
data-bs-slide |
data-slide-to |
data-bs-slide-to |
data-interval |
data-bs-interval |
data-offset |
data-bs-offset |
data-spy |
data-bs-spy |
<ol> carousel indicators |
<div> with <button> indicators |
.sr-only |
.visually-hidden |
.float-left / .float-right |
.float-start / .float-end |
.ml-* / .mr-* |
.ms-* / .me-* |
.pl-* / .pr-* |
.ps-* / .pe-* |
.text-left / .text-right |
.text-start / .text-end |
jQuery required |
jQuery optional |
Quick Reference: Common Using Statements¶
// BlockGrid/BlockList types
@using Umbraco.Cms.Core.Models.Blocks
// Content model types
@using Umbraco.Cms.Core.Models.PublishedContent
// Media helpers
@using Umbraco.Extensions
// Project services
@using Progress.Baseline.Core.Services
Quick Reference: Null-Safe Patterns¶
// Strings - always coalesce
var title = content.Value<string>("title") ?? string.Empty;
// Collections - always coalesce
var items = content.Value<IEnumerable<MediaWithCrops>>("images")
?? Enumerable.Empty<MediaWithCrops>();
// Booleans - safe (defaults to false)
var flag = content.Value<bool>("isEnabled");
// Rich text - check before rendering
var body = content.Value<IHtmlEncodedString>("bodyText");
if (body != null)
{
@body
}