Website Development on Umbraco CMS
Umbraco is open-source CMS on ASP.NET Core with SQL Server or SQLite, supports clustering. Well suited for corporate sites, portals, multisite infrastructures — where client is bound to Microsoft stack or needs headless CMS with enterprise capabilities.
Project Architecture
MyProject/
├── MyProject.Web/ # main web project
│ ├── Controllers/ # Surface and Render Controllers
│ ├── Models/ # strongly-typed models
│ ├── Views/ # Razor Views
│ │ ├── Partials/
│ │ └── Shared/
│ ├── Composers/ # data injection to views
│ ├── NotificationHandlers/ # Umbraco events
│ ├── wwwroot/
│ └── Program.cs
├── MyProject.Core/ # business logic
│ ├── Services/
│ └── Models/
└── MyProject.Tests/
Host Application Setup
// Program.cs
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi() // headless API
.AddComposers()
.Build();
WebApplication app = builder.Build();
await app.BootUmbracoAsync();
app.UseUmbraco()
.WithMiddleware(u =>
{
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseInstallerEndpoints();
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
await app.RunAsync();
Strongly-Typed Models
Umbraco generates models via ModelsBuilder. After configuring content types in backoffice:
dotnet run -- umbraco-models-builder generate
Usage in controller:
using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
public class ArticleController : RenderController
{
private readonly ILogger<ArticleController> _logger;
public ArticleController(
ILogger<ArticleController> logger,
ICompositeViewEngine viewEngine,
IUmbracoContextAccessor umbracoContextAccessor)
: base(logger, viewEngine, umbracoContextAccessor)
{
_logger = logger;
}
[HttpGet]
public IActionResult Index()
{
if (CurrentPage is not ContentModels.Article article)
{
return NotFound();
}
var viewModel = new ArticleViewModel
{
Title = article.Title,
Body = article.Body,
PublishDate = article.PublishDate,
Author = article.Author?.Name,
Tags = article.Tags?.Split(',').Select(t => t.Trim()).ToArray() ?? [],
};
return CurrentTemplate(viewModel);
}
}
Razor View
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ArticleViewModel>
@{
Layout = "_Layout.cshtml";
}
<article class="article">
<header>
<h1>@Model.Title</h1>
@if (Model.PublishDate.HasValue)
{
<time datetime="@Model.PublishDate.Value.ToString("yyyy-MM-dd")">
@Model.PublishDate.Value.ToString("dd.MM.yyyy")
</time>
}
@if (!string.IsNullOrEmpty(Model.Author))
{
<span class="author">@Model.Author</span>
}
</header>
<div class="article__body">
@Html.Raw(Model.Body)
</div>
@if (Model.Tags?.Length > 0)
{
<footer class="tags">
@foreach (var tag in Model.Tags)
{
<a href="/[email protected](tag)">@tag</a>
}
</footer>
}
</article>
Composers — DI Setup
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
public class SiteComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddScoped<IArticleService, ArticleService>();
builder.Services.AddScoped<ISitemapService, SitemapService>();
// register event handlers
builder.AddNotificationHandler<ContentPublishedNotification, SearchIndexHandler>();
builder.AddNotificationHandler<ContentSavedNotification, CacheInvalidationHandler>();
}
}
Headless via Delivery API
Umbraco 12+ includes built-in Delivery API:
// appsettings.json
{
"Umbraco": {
"CMS": {
"DeliveryApi": {
"Enabled": true,
"PublicAccess": true,
"ApiKey": "your-api-key",
"DisallowedContentTypeAliases": ["settings", "internalPage"],
"RichTextOutputAsJson": false
}
}
}
}
API requests:
GET /umbraco/delivery/api/v2/content?contentType=article&sort=createDate:desc&take=10
Authorization: Api-Key your-api-key
GET /umbraco/delivery/api/v2/content/item/my-article-slug
Response is typed, includes content type properties, media, relations.
Surface Controller for Forms
public class ContactFormController : SurfaceController
{
private readonly IMailService _mailService;
public ContactFormController(
IUmbracoContextAccessor contextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
ILogger<ContactFormController> logger,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IMailService mailService)
: base(contextAccessor, databaseFactory, services, appCaches,
logger, profilingLogger, publishedUrlProvider)
{
_mailService = mailService;
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Submit(ContactFormModel model)
{
if (!ModelState.IsValid)
{
return CurrentUmbracoPage();
}
await _mailService.SendContactEmailAsync(model);
TempData["FormSuccess"] = true;
return RedirectToCurrentUmbracoPage();
}
}
Multisite
// one Umbraco instance, multiple sites
// Configured in backoffice: Content → right-click → "Allow as root"
// Each root node gets own domain in Domains
// Get current site in template:
@inject IUmbracoContextAccessor UmbracoContext
@{
var root = UmbracoContext.GetRequiredUmbracoContext()
.Content?
.GetAtRoot()
.First(n => n.IsAncestorOrSelf(Model.Content!));
var siteName = root?.Value<string>("siteName");
}
Performance
// appsettings.json — cache
{
"Umbraco": {
"CMS": {
"Runtime": {
"Mode": "BackofficeDevelopment"
},
"WebRouting": {
"DisableAlternativeTemplates": false,
"DisableFindContentByIdentifierPath": false
},
"NuCache": {
"BTreeBlockSize": 4096
}
}
}
}
Umbraco uses NuCache (in-memory + disk) for published content — no DB hits when reading public pages.
Development Timelines
Corporate site 10–15 content types with custom templates: 3–5 weeks. With headless API, external integrations, multisite: 6–10 weeks. Setup existing instance for new design — from 2 weeks.







