Umbraco CMS Development Best Practices
A comprehensive guide to Umbraco CMS development best practices, covering architecture, performance optimization, and maintainability.
Umbraco CMS Development Best Practices
Umbraco is one of the most popular open-source content management systems built on the .NET framework. As a flexible and extensible platform, it offers developers tremendous freedom—but with that freedom comes the responsibility to implement solutions that are maintainable, performant, and secure. In this comprehensive guide, I'll share best practices for Umbraco development based on my experience leading multiple enterprise implementations.
Architecture and Project Structure
Clean Architecture Principles
When building Umbraco solutions, applying clean architecture principles helps create maintainable and testable code:
- Separate concerns: Create distinct layers for presentation, business logic, and data access
- Use dependency injection: Leverage Umbraco's built-in DI container for loosely coupled components
- Create abstractions: Define interfaces for services to enable unit testing and flexibility
A typical clean architecture for Umbraco might include:
- Core/Domain layer: Models, interfaces, and business logic
- Infrastructure layer: Implementations of repositories and services
- Web layer: Controllers, views, and Umbraco-specific components
Project Organization
For larger Umbraco projects, consider organizing your solution as follows:
YourSolution/
├── YourSolution.Core/
│ ├── Models/
│ ├── Services/
│ └── Interfaces/
├── YourSolution.Infrastructure/
│ ├── Repositories/
│ ├── Services/
│ └── Migrations/
└── YourSolution.Web/
├── App_Plugins/
├── Controllers/
├── Views/
└── uSync/
This structure makes it easier to maintain separation of concerns and facilitates testing.
Content Modeling Best Practices
Document Types and Composition
Effective content modeling is crucial for a successful Umbraco implementation:
- Use composition over inheritance: Create small, focused document types that can be composed together
- Implement content element types: Use element types for reusable content blocks
- Create logical groupings: Organize properties into tabs and property groups
- Define sensible defaults: Set default values and validation rules
For example, instead of creating a monolithic "Page" document type, create smaller composable types:
- SEO Properties: Meta title, description, canonical URL
- Navigation Properties: Navigation title, hide from navigation
- Social Properties: Social images and descriptions
Property Editors and Data Types
Choose appropriate property editors and configure data types carefully:
- Limit custom property editors: Use built-in editors when possible
- Configure validation: Set validation rules at the data type level
- Use sensible naming conventions: Name data types to indicate their configuration
- Consider the editing experience: Choose editors that make content entry intuitive
Performance Optimization
Caching Strategies
Implement effective caching to improve performance:
- Use Umbraco's built-in caching: Leverage the various cache layers in Umbraco
- Implement output caching: Cache rendered output for anonymous users
- Use distributed caching: For load-balanced environments, implement Redis or similar
- Cache expensive operations: Cache results of complex queries or external API calls
Example of implementing a custom cache service:
public class CacheService : ICacheService
{
private readonly IAppPolicyCache _runtimeCache;
public CacheService(AppCaches appCaches)
{
_runtimeCache = appCaches.RuntimeCache;
}
public T GetOrCreate<T>(string cacheKey, Func<T> factory, TimeSpan? slidingExpiration = null)
{
return _runtimeCache.GetCacheItem(cacheKey, () => factory(), slidingExpiration);
}
}
Query Optimization
Optimize database queries to improve performance:
- Use Examine for content queries: Leverage Examine (Lucene) instead of direct database queries
- Implement paging: Don't retrieve more items than needed
- Select only required fields: Use projection to select only the fields you need
- Avoid N+1 query problems: Use eager loading when appropriate
Example of using Examine for efficient queries:
public IEnumerable<IPublishedContent> GetRelatedArticles(IPublishedContent article, int count = 5)
{
var searcher = ExamineManager.Instance.SearchProviderCollection["ExternalSearcher"];
var criteria = searcher.CreateSearchCriteria(IndexTypes.Content);
var query = criteria.Field("nodeTypeAlias", "article")
.And().Field("tags", article.Value<string>("tags"))
.Not().Field("__NodeId", article.Id.ToString())
.OrderByDescending("createDate");
return searcher.Search(query.Compile())
.Take(count)
.Select(result => UmbracoContext.Current.ContentCache.GetById(int.Parse(result.Id)));
}
Custom Development
Custom Controllers and Routing
Implement custom controllers and routing for complex functionality:
- Use Surface Controllers for form handling and AJAX requests
- Implement RenderMvcController for custom page rendering logic
- Create API Controllers for headless/SPA implementations
- Use custom routes when Umbraco's content routing isn't sufficient
Example of a Surface Controller for form handling:
public class ContactSurfaceController : SurfaceController
{
private readonly IEmailService _emailService;
public ContactSurfaceController(IEmailService emailService)
{
_emailService = emailService;
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult SubmitForm(ContactFormModel model)
{
if (!ModelState.IsValid)
{
return CurrentUmbracoPage();
}
_emailService.SendContactNotification(model);
TempData["Success"] = "Your message has been sent successfully.";
return RedirectToCurrentUmbracoPage();
}
}
Custom Property Editors
When building custom property editors:
- Follow Angular best practices: Use components and services
- Implement validation: Both client and server-side
- Consider the editing experience: Make editors intuitive and user-friendly
- Document thoroughly: Provide clear documentation for content editors
Deployment and DevOps
Configuration Management
Manage configuration across environments:
- Use uSync for content type and data type synchronization
- Implement config transforms for environment-specific settings
- Use appsettings.json with environment-specific overrides
- Consider Umbraco Deploy for content synchronization in enterprise scenarios
Continuous Integration/Deployment
Implement CI/CD for Umbraco projects:
- Automate builds using Azure DevOps, GitHub Actions, or similar
- Run automated tests as part of the build process
- Use deployment slots for zero-downtime deployments
- Implement database upgrade scripts for schema changes
Example GitHub Actions workflow for Umbraco:
name: Build and Deploy
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release
- name: Publish
run: dotnet publish --no-build --configuration Release --output ./publish
- name: Deploy to Azure
uses: azure/webapps-deploy@v2
with:
app-name: 'your-umbraco-app'
publish-profile: "AZURE_PUBLISH_PROFILE_SECRET"
package: ./publish
Security Best Practices
Authentication and Authorization
Implement secure authentication and authorization:
- Use Umbraco's membership providers or integrate with Identity Server
- Implement proper role-based access control
- Secure sensitive operations with appropriate permissions
- Consider two-factor authentication for admin users
Content Security
Protect against common web vulnerabilities:
- Implement Content Security Policy (CSP) headers
- Validate and sanitize user input
- Protect against CSRF attacks using anti-forgery tokens
- Implement proper error handling without exposing sensitive information
Example of implementing CSP headers:
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
context.Response.Headers.Add("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"img-src 'self' data: https://*.umbraco.io https://our-umbraco-media.s3.amazonaws.com;");
await _next(context);
}
}
// In Startup.cs
app.UseMiddleware<SecurityHeadersMiddleware>();
Testing and Quality Assurance
Unit Testing
Implement comprehensive testing for Umbraco solutions:
- Test business logic independently of Umbraco
- Use mocking frameworks to isolate dependencies
- Implement integration tests for critical paths
- Consider UI automation for critical user journeys
Example of unit testing a service:
[TestClass]
public class ContentServiceTests
{
[TestMethod]
public void GetRelatedContent_ReturnsCorrectNumberOfItems()
{
// Arrange
var mockRepository = new Mock<IContentRepository>();
mockRepository.Setup(r => r.GetByTags(It.IsAny<string[]>(), It.IsAny<string>()))
.Returns(new List<ContentItem> { /* test data */ });
var service = new ContentService(mockRepository.Object);
// Act
var result = service.GetRelatedContent("test-content", new[] { "tag1", "tag2" }, 3);
// Assert
Assert.AreEqual(3, result.Count());
}
}
Maintenance and Upgrades
Keeping Umbraco Updated
Maintain a healthy Umbraco installation:
- Stay current with security updates
- Plan major version upgrades carefully
- Test thoroughly in staging before upgrading production
- Maintain documentation of customizations and configurations
Monitoring and Logging
Implement proper monitoring and logging:
- Use Application Insights or similar for performance monitoring
- Implement structured logging with Serilog
- Set up alerts for critical errors
- Monitor database performance and growth
Example of configuring Serilog in Umbraco:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.Build();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseUmbraco()
.WithMiddleware(u =>
{
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseInstallerEndpoints();
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
.WriteTo.ApplicationInsights(TelemetryConfiguration.Active, TelemetryConverter.Traces)
.CreateLogger();
}
}
Conclusion
Implementing these best practices in your Umbraco projects will help you create solutions that are maintainable, performant, and secure. Remember that Umbraco's flexibility means there are often multiple ways to solve a problem—the key is to choose approaches that align with your specific requirements while following solid architectural principles.
By focusing on clean architecture, effective content modeling, performance optimization, and proper testing, you'll be well-positioned to deliver successful Umbraco implementations that meet both business and technical requirements.
What Umbraco best practices have you found most valuable in your projects? Share your experiences in the comments below.