Umbraco CMS Development Best Practices

Umbraco CMS Development Best Practices

March 15, 2024
Admin User

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:

  1. Separate concerns: Create distinct layers for presentation, business logic, and data access
  2. Use dependency injection: Leverage Umbraco's built-in DI container for loosely coupled components
  3. 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:

  1. Use composition over inheritance: Create small, focused document types that can be composed together
  2. Implement content element types: Use element types for reusable content blocks
  3. Create logical groupings: Organize properties into tabs and property groups
  4. 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:

  1. Limit custom property editors: Use built-in editors when possible
  2. Configure validation: Set validation rules at the data type level
  3. Use sensible naming conventions: Name data types to indicate their configuration
  4. Consider the editing experience: Choose editors that make content entry intuitive

Performance Optimization

Caching Strategies

Implement effective caching to improve performance:

  1. Use Umbraco's built-in caching: Leverage the various cache layers in Umbraco
  2. Implement output caching: Cache rendered output for anonymous users
  3. Use distributed caching: For load-balanced environments, implement Redis or similar
  4. 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:

  1. Use Examine for content queries: Leverage Examine (Lucene) instead of direct database queries
  2. Implement paging: Don't retrieve more items than needed
  3. Select only required fields: Use projection to select only the fields you need
  4. 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:

  1. Use Surface Controllers for form handling and AJAX requests
  2. Implement RenderMvcController for custom page rendering logic
  3. Create API Controllers for headless/SPA implementations
  4. 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:

  1. Follow Angular best practices: Use components and services
  2. Implement validation: Both client and server-side
  3. Consider the editing experience: Make editors intuitive and user-friendly
  4. Document thoroughly: Provide clear documentation for content editors

Deployment and DevOps

Configuration Management

Manage configuration across environments:

  1. Use uSync for content type and data type synchronization
  2. Implement config transforms for environment-specific settings
  3. Use appsettings.json with environment-specific overrides
  4. Consider Umbraco Deploy for content synchronization in enterprise scenarios

Continuous Integration/Deployment

Implement CI/CD for Umbraco projects:

  1. Automate builds using Azure DevOps, GitHub Actions, or similar
  2. Run automated tests as part of the build process
  3. Use deployment slots for zero-downtime deployments
  4. 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:

  1. Use Umbraco's membership providers or integrate with Identity Server
  2. Implement proper role-based access control
  3. Secure sensitive operations with appropriate permissions
  4. Consider two-factor authentication for admin users

Content Security

Protect against common web vulnerabilities:

  1. Implement Content Security Policy (CSP) headers
  2. Validate and sanitize user input
  3. Protect against CSRF attacks using anti-forgery tokens
  4. 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:

  1. Test business logic independently of Umbraco
  2. Use mocking frameworks to isolate dependencies
  3. Implement integration tests for critical paths
  4. 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:

  1. Stay current with security updates
  2. Plan major version upgrades carefully
  3. Test thoroughly in staging before upgrading production
  4. Maintain documentation of customizations and configurations

Monitoring and Logging

Implement proper monitoring and logging:

  1. Use Application Insights or similar for performance monitoring
  2. Implement structured logging with Serilog
  3. Set up alerts for critical errors
  4. 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.

Related Posts

DevOps CI/CD Pipeline Optimization

DevOps CI/CD Pipeline Optimization

over 1 year ago

Strategies and techniques for optimizing your CI/CD pipelines to improve develop...

Domain-Driven Design in .NET Applications

Domain-Driven Design in .NET Applications

over 1 year ago

A practical guide to implementing Domain-Driven Design principles in .NET applic...

Practical Machine Learning for .NET Developers

Practical Machine Learning for .NET Developers

over 1 year ago

A hands-on guide to implementing machine learning in .NET applications without b...