loading...

Sitecore Code Generation with Unicorn in 2020

kmac23va profile image Kenneth McAndrew ・5 min read

I've been undertaking a "Helix recipe" project for my company of late, looking at some of the best ideas out there to come up with a starter kit of sorts for Helix with Sitecore. Because of the ease in configuration and to reduce the number of projects in Visual Studio, I've been integrating Unicorn into the mix. And while TDS has code generation features built into it (although you still have to provide the T4 templates), Unicorn doesn't have anything "out of the box" for that.

A few years ago, however, someone did take a stab at it with the RainbowCodeGeneration Github project.

GitHub logo heikof / RainbowCodeGeneration

A simple set of utility classes and a sample T4 template that allow easy code generation for Sitecore templates from Rainbow / Unicorn serialized items. Currently, only the YAML serialization format is supported.

It hasn't been updated in three years, but I still see it discussed, and recently an upgrade project crossed my desk that was using it with Sitecore 8. I'm trying to tailor this to 9.3 (the latest as of this writing), and I wanted to see about using it with my recipe project.

I've become a fan of fluent configuration when it comes to Glass, though I know others like the attribute configuration. Some may like good old Sitecore API calls. But in all cases, having the template and field IDs is a big plus because while the names are changeable, the IDs aren't (I mean, unless you delete the field and recreate it, but I can't help everything!). Code generation means the field name would change, sure, but it'd generate a compile error if you need to refactor the name, whereas specifying the ID as a string value yourself could leave junk behind. So while you could take this and generate many scenarios, I'm concentrating on simply getting the IDs for templates and fields.

The Problem

The sample in the RainbowCodeGeneration project was set for Sitecore 9 and Unicorn 4, but the later versions of Sitecore have gotten more complex, and when I tried using the samples and updating the references to 9.3, I kept getting errors where the code was looking for different missing assemblies. In addition, modern Helix projects are switching to the SDK project format and the package references model for NuGet. But the samples rely on references in the old "packages" folder, and the default location for package references is an app data folder in your computer profile...not exactly shareable in source control.

Step One: Configure NuGet

You should already have a NuGet.config file for your project that references the Sitecore feed. You just need to add one section to it, the globalPackagesFolder, as shown below:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <config>
        <clear />
        <add key="globalPackagesFolder" value="c:\packages" />
    </config>
    <solution>
        <add key="disableSourceControlIntegration" value="true" />
    </solution>
    <packageSources>
        <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
        <add key="Sitecore" value="https://sitecore.myget.org/F/sc-packages/api/v3/index.json" />
    </packageSources>
    <activePackageSource>
        <add key="All" value="(Aggregate source)" />
    </activePackageSource>
</configuration>

Now your packages are set in a commonly-accessible location, and you can write your T4 templates accordingly.

Step Two: The T4 Include File

Since this is a Helix solution, we'll have a separate ID file for each module, typically in the format [layer].[module] (for example, Feature.Maps). That usually translates to a Unicorn folder location for [layer].[module].Templates (for example, Feature.Maps.Templates). If we follow this pattern consistently, we can use an include file across the various projects for consistency. But we also have the assembly references, and if Sitecore gets upgraded, these will have to be updated too...they can be in the include file as well. In fact, as you'll see in step three, the real T4 template will have almost nothing in it!

Here's the include file:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<# 
// ASSEMBLIES
// These assemblies need to be changed if there is an upgrade of the specified packages
#>
<#@ assembly name="C:\packages\rainbow.core\2.1.1\lib\net452\Rainbow.dll" #>
<#@ assembly name="C:\packages\rainbow.storage.yaml\2.1.1\lib\net452\Rainbow.Storage.Yaml.dll" #>
<#@ assembly name="C:\packages\rainbowcodegeneration\0.3.0\lib\net452\RainbowCodeGeneration.dll" #>
<#@ assembly name="C:\packages\sitecore.kernel\9.3.0\lib\net471\Sitecore.Kernel.dll" #>
<#@ assembly name="C:\packages\sitecore.logging\9.3.0\lib\net471\Sitecore.Logging.dll" #>
<#@ assembly name="C:\packages\microsoft.extensions.dependencyinjection.abstractions\2.1.1\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll" #>
<#@ assembly name="C:\packages\microsoft.extensions.dependencyinjection\2.1.1\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll" #>
<#@ assembly name="c:\packages\microsoft.extensions.logging\2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.dll" #>
<#@ assembly name="c:\packages\microsoft.extensions.logging.abstractions\2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll" #>
<#@ assembly name="c:\packages\microsoft.extensions.options\2.1.1\lib\netstandard2.0\Microsoft.Extensions.Options.dll" #>
<#@ assembly name="c:\packages\microsoft.extensions.diagnostics.healthchecks.abstractions\2.2.0\lib\netstandard2.0\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.dll" #>
<# 
// CONFIGURATION
var physicalFileStore = @"..\..\serialization"; // the path to your serialization items
var treeName = layerName + "." + moduleName + ".Templates";
var treePath = "/sitecore/templates/" + layerName + "/" + moduleName; // the matching path in Sitecore for the configuration
var templates = RainbowCodeGeneration.RainbowReader.GetTemplates(Host.ResolvePath(physicalFileStore), treeName, treePath);
#>
<#@ import namespace="RainbowCodeGeneration" #>
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated based on the Unicorn serialization items
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

// ReSharper disable InconsistentNaming
using Sitecore.Data;

namespace <#= layerName #>.<#= moduleName #>.Models {
<# try { foreach (var template in templates) { #>
    public static class <#= StringExtensions.AsClassName(template.Item.Name.StartsWith("_") ? "Meta" + template.Item.Name.Substring(1) : template.Item.Name).Replace("_", string.Empty) #>Constants {
        public static readonly ID TemplateId = new ID("<#= template.Item.Id.ToString() #>");
    <#foreach(var field in template.Fields){#>    public static readonly ID <#= StringExtensions.AsPropertyName(field.Name).Replace("_", string.Empty) #>FieldId = new ID("<#=field.Id.ToString()#>");
    <#}#>}
<#  } } catch (Exception ex){ Console.WriteLine(ex); } #>
}

If you've looked at the sample files in the RainbowCodeGeneration tests project, you'll see the biggest change immediately...how many assemblies are referenced! I went through a process of updating the Rainbow and Sitecore assemblies from the original T4, and every time I saved the file, I got another "assembly is not found" error. I just kept adding them on until the compiler stopped complaining to me! As you can see, the location for the assemblies relies on my NuGet.config information, and the file/folder format is in the package references style.

For the serialization location, I'm putting my T4 file in a Models folder, so it goes up two levels to get outside the website folder and to the serialization folder. Then you'll see there are variables in place for the layer and module, which comes into play in step three.

I keep my T4 include file in the root of my solution, and have it included as a solution item just for reference and easy editing if needed.

Step Three: The T4 File

In each Helix modules, I have a Models folder, and this is where I put my actual T4 file. This is going to be a complex file...ready?

<# 
// MODULE CONFIGURATION
var layerName = "Foundation";
var moduleName = "Kernel";
#>
<#@ include file="$(SolutionDir)IDCodeGen.ttinclude" once="true" #>

Pretty straightforward and easy to maintain from project to project! This is where we establish the layer and module variables, which if you use a Helix module generator, you can easily parameterize. Then an include reference to my T4 include from above, and we're good to go.

Conclusion

As noted, you can modify the T4 include to generate whatever you want. If you want to autogenerate full models with Glass attributes, you can do it with this. If you use the templates from the RainbowCodeGeneration project, you can see from this the modifications needed for the assemblies to get them working with the latest Sitecore. As always, when you upgrade Sitecore, upgrade these references appropriately (and be ready if you need to add another assembly or two).

Happy code generating!

Posted on by:

kmac23va profile

Kenneth McAndrew

@kmac23va

Over 20 years in web development, from HTML (remember image maps and frames?) to classic ASP to ASP.NET to .NET CMSes.

Discussion

markdown guide