Umbraco's ModelsBuilder is a fantastic tool that generates strongly-typed models from your document types. But what if you want to customize the generated output? Maybe you want to add property alias constants or other custom code to your models.
In this article, I'll show you how to create a custom ModelsGenerator that post-processes the generated files to add your own customizations.
The Problem
When working with Umbraco, you often need to reference property aliases as strings - for example, when using SetValue() while creating or editing content through the ContentService, or maybe you need the dynamic property access from IPublishedContent. This means scattering magic strings throughout your codebase:
// setting a property value on IContent
var title = content.SetValue("pageTitle", "New Title");
// checking for a property on IPublishedContent
var items = content.Children.Where(x => x.HasValue("featuredImage"));
If a property alias changes, you'll need to hunt down every occurrence. It would be much better to have strongly-typed constants for each property alias, so you get compile-time safety and IntelliSense support.
ModelsBuilder does provide a way to get the property aliases from the document type, but it is rather verbose, and it requires a dependency on IPublishedContentTypeCache:
@using Umbraco.Cms.Core.PublishedCache
@inject IPublishedContentTypeCache _contentTypeCache;
@{
var blocksPropertyAlias = ContentPage.GetModelPropertyType(_contentTypeCache, x => x.Blocks)?.Alias;
}
The Solution
We can create a custom ModelsGenerator that runs after the standard generation and post-processes the output files. This approach:
- Doesn't require modifying Umbraco's source code
- Works with the existing ModelsBuilder pipeline
- Allows for any kind of text manipulation on generated files
Here's the full implementation:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System.Text;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Infrastructure.ModelsBuilder;
using Umbraco.Cms.Infrastructure.ModelsBuilder.Building;
using Umbraco.Extensions;
namespace MyProject.ModelsGenerators;
public class MyOwnModelsGenerator(
UmbracoServices umbracoService,
IOptionsMonitor<ModelsBuilderSettings> config,
OutOfDateModelsStatus outOfDateModels,
IHostEnvironment hostingEnvironment)
: ModelsGenerator(umbracoService, config, outOfDateModels, hostingEnvironment), IModelsGenerator
{
public new void GenerateModels()
{
base.GenerateModels();
var modelsDirectory = config.CurrentValue.ModelsDirectoryAbsolute(hostingEnvironment);
if (!Directory.Exists(modelsDirectory)) return;
foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
{
var fileContents = File.ReadAllText(file);
var newFileContents = AddPropertyAliasConstants(fileContents);
if (newFileContents != fileContents)
{
File.WriteAllText(file, newFileContents);
}
}
}
private static string AddPropertyAliasConstants(string fileContents)
{
var marker = "public new const string ModelTypeAlias =";
var index = fileContents.IndexOf(marker, StringComparison.Ordinal);
if (index == -1) return fileContents;
var pattern = @"\[.*ImplementPropertyType\(""([^""]+)""\)\][\s\S]*?public\s+(?:virtual\s+)?[\w\.:<>]+\s+([A-Za-z0-9_]+)\s*(?:{|=>)";
var matches = Regex.Matches(fileContents, pattern);
var sb = new StringBuilder();
sb.Append("public new static class ModelPropertyAliases\n\t\t{\n");
foreach (Match match in matches)
{
var alias = match.Groups[1].Value;
var propName = match.Groups[2].Value;
sb.Append($"\t\t\tpublic const string {propName} = \"{alias}\";\n");
}
sb.Append("\t\t}\n\n\t\t");
return fileContents.Insert(index, sb.ToString());
}
}
How It Works
The custom generator does a few key things:
1. Inheriting from ModelsGenerator
By inheriting from ModelsGenerator and implementing IModelsGenerator, we can override the generation behavior while still using all the built-in functionality.
public class MyOwnModelsGenerator : ModelsGenerator, IModelsGenerator
2. Post-Processing Generated Files
The GenerateModels() method first calls base.GenerateModels() to run the standard generation, then iterates through all *.generated.cs files to apply our customizations:
public new void GenerateModels()
{
base.GenerateModels();
var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment);
foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
{
// Post-process each file...
}
}
3. Adding Property Alias Constants
The AddPropertyAliasConstants method uses regex to find all properties marked with [ImplementPropertyType] and extracts both the alias and property name. It then generates a nested static class containing constants for each property:
public new static class ModelPropertyAliases
{
public const string PageTitle = "pageTitle";
public const string FeaturedImage = "featuredImage";
public const string Blocks = "blocks";
}
Registering the Custom Generator
To use your custom generator, you need to register it with Umbraco's dependency injection. Add this to your startup configuration:
builder.Services.AddSingleton<IModelsGenerator, MyOwnModelsGenerator>();
Or if you're using a composer:
public class ModelsGeneratorComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IModelsGenerator, MyOwnModelsGenerator>();
}
}
Using the Generated Constants
Once the models are regenerated, you can use the constants throughout your code:
// Before: magic strings
content.SetValue("pageTitle", "New Title");
// After: strongly-typed constants
content.SetValue(HomePage.ModelPropertyAliases.PageTitle, "New Title");
// Works great in LINQ queries too
var featured = content.Children
.Where(x => x.HasValue(Article.ModelPropertyAliases.FeaturedImage));
Now if a property alias changes, you'll get a compile-time error instead of a runtime surprise.
Final Thoughts
Customizing ModelsBuilder output gives you more control over your generated code without fighting against the built-in tooling. By post-processing the generated files, you can add compile-time safety features like property alias constants while keeping all the benefits of automatic model generation.
The approach shown here - inheriting from ModelsGenerator and processing files after generation - is clean, maintainable, and survives Umbraco upgrades since it doesn't modify any core code.
Top comments (0)