How to Use Leprechaun to Auto Generate Glass Mapper Models when using Unicorn

Published:

So I’ve been on the lookout for a good way to auto-generate Glass Mapper Models when I’m using Unicorn to sync Sitecore items for quite awhile. I’ve finally found a mechanism that allows you to create these models relatively easily. The thing about Leprechaun besides it’s awesome name, is that you can use it in conjunction with Rainbow (which is used by Unicorn) to Auto generate any type of file. For example, in this article I’m going to show how you can create the models with decorators vs. the fluent API approach that you can also use with Glass Mapper, but you can definitely use Leprechaun to auto-generate fluent api models instead.

In order to get started with Leprechaun, you will need to download the latest release from Github: https://github.com/blipson89/Leprechaun/releases. At the time of this writing, I am using Version 1.0. There is also a Nuget Package for this as well, but I found downloading the files so I could pick where it placed the files, the better approach. Also this tool is really useful only at the developer level, and although Nuget makes it easy to pull down the code you need, I think downloading and manually installing it the better route, which I will demonstrate in this article.

Once you download the latest release you should unzip the contents and then you should have a folder that contains the source for Leprechaun. If you open that folder you will see a bunch of dll’s and an exe called Leprechaun.Console.exe. You should also see a folder called Scripts, this folder, contains a small sampling of scripts that can be used with the rosyln compile to auto generate a file. By default, this includes files for Synthesis. Instead of Synthesis, we will need to create a new file in this folder and we will call it GlassMapper.csx. It will contain the following code:

// Generates Synthesis models

Log.Debug($"Emitting GlassMapper templates for {ConfigurationName}...");

public string RenderTemplates()
{
    var localCode = new System.Text.StringBuilder();

    foreach (var template in Templates)
    {
        localCode.AppendLine($@"
namespace {template.Namespace}.Models
{{
    using System;
	using System.CodeDom.Compiler;
	using System.Collections.Generic;
    using Helix.Foundation.ORM.Models;
    using Glass.Mapper.Sc.Configuration.Attributes;
    using Glass.Mapper.Sc.Fields;
	/// <summary>Controls the appearance of the inheriting template in site navigation.</summary>
	///[RepresentsSitecoreTemplateAttribute(""{{{template.Id}}}"", """", ""{ConfigurationName}"")]
    [SitecoreType(TemplateId = {template.Namespace}.Templates.{template.CodeName}.TemplateIdString)]
	public partial interface I{template.CodeName} : {GetBaseInterfaces(template)}
	{{
		{RenderInterfaceFields(template)}
	}}
}}"
        );
    }

    return localCode.ToString();
}

Code.AppendLine($@"
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
// ReSharper disable All
{RenderTemplates()}
");

public string GetBaseInterfaces(TemplateCodeGenerationMetadata template)
{
    var bases = new System.Collections.Generic.List<string>(template.BaseTemplates.Count + 1);

    foreach (var baseTemplate in template.BaseTemplates)
    {
        bases.Add($"{baseTemplate.Namespace}.I{baseTemplate.CodeName}");
    }

    if (bases.Count == 0)
    {
        // IStandardTemplateItem only needed when no other bases exist otherwise irrelevant by transitive inheritance
        bases.Add("IGlassBase");
    }

    return string.Join(", ", bases);
}

public string RenderInterfaceFields(TemplateCodeGenerationMetadata template)
{
    var localCode = new System.Text.StringBuilder();

    foreach (var field in template.OwnFields)
    {
        localCode.AppendLine($@"
		/// <summary>{field.HelpText}</summary>
        [SitecoreField(FieldName = {template.Namespace}.Templates.{template.CodeName}.Fields.{field.CodeName}_FieldName)]
		{GetFieldType(field)} {field.CodeName} {{ get; }}");
    }

    return localCode.ToString();
}

public string GetFieldType(TemplateFieldCodeGenerationMetadata field)
{
    switch (field.Type.ToLower())
    {
        case "tristate":
            return "TriState";
        case "checkbox":
            return "bool";

        case "date":
        case "datetime":
            return "DateTime";

        case "number":
            return "float";

        case "integer":
            return "int";

        case "treelist":
        case "treelistex":
        case "treelist descriptive":
        case "checklist":
        case "multilist":
            return "IEnumerable<Guid>";

        case "grouped droplink":
        case "droplink":
        case "lookup":
        case "droptree":
        case "reference":
        case "tree":
            return "Guid";

        case "file":
            return "File";

        case "image":
            return "Image";

        case "rich text":
        case "html":
            return "string";

        case "general link":
            return "Link";

        case "single-line text":
        case "multi-line text":
        case "frame":
        case "text":
        case "memo":
        case "droplist":
        case "grouped droplist":
        case "valuelookup":
            return "string";
        default:
            return "string";
    }
}

We will also want to update the Constants.csx file. This is a file that mimics the Template.cs file that’s used by Habitat. We will just extend that functionality so that it contains the Template Id as a string and to change the formatting just slightly to match more to the type of constants file you might be used to seeing if you’ve ever worked with TDS and Glass Mapper, with it’s T4 templates that auto-generate the glass models. These changes to the Constants.csx file is also required so that items in the GlassMapper.csx work correctly. The CSX files are super easy to modify c# sharp files. You will just be dealing with string manipulation to make you changes.

// Generates Glass Constants File

Log.Debug($"Emitting constants templates for {ConfigurationName}...");

public string RenderFields(TemplateCodeGenerationMetadata template)
{
	if (template.OwnFields.Length == 0)
	{
		return string.Empty;
	}

	var localCode = new System.Text.StringBuilder();

	localCode.Append($@"
			public struct Fields
			{{");

	foreach (var field in template.OwnFields)
	{
		localCode.AppendLine($@"
				public static readonly ID {field.CodeName} = new ID(""{field.Id}"");
				public const string {field.CodeName}_FieldName = ""{field.Name}"";");
	}

	localCode.Append(@"
			}");

	return localCode.ToString();
}
public string RenderTemplates()
{
	var localCode = new System.Text.StringBuilder();
	
	foreach(var template in Templates)
	{
		localCode.AppendLine($@"
		public struct {template.CodeName}
		{{
            public const string TemplateIdString = ""{template.Id}"";
			public static readonly ID TemplateId = new ID(TemplateIdString);
			{RenderFields(template)}
		}}");
	}

	return localCode.ToString();
}

Code.AppendLine($@"
namespace {GenericRootNamespace}
{{
	using global::Sitecore.Data;
	public struct Templates
	{{
		{RenderTemplates()}
	}}
}}");

Finally there is a Diagnostics file in that scripts folder as well. Typically I like to leave that file in place, so that if any issues come up, I can use that as a point of reference to track down the issues.

Solution Setup

Now that we have the csx files configured with the changes we will need to enable Glass model generation with the use of Leprechaun, the next step is to get Leprechaun up and working in our solution. I have already configured Leprechaun in two different open source projects I have been working on listed below, if you find that it’s easier to just copy what I’ve done:

  • Helix Starter Kit (w/ Unicorn and Glass)
  • Helix Start Bootstrap Agency (w/ Unicorn and Glass)

To get started we need to copy the root folder we were working on earlier into our working solution. It’s the folder that contains the Scripts folder and all of the dll’s. We need to move that into a folder that sits in the root of our solution. I’ve typically created a Tools folder to place this folder inside of that folder, but you can place it anywhere appropriate.

Solution Leprechaun Structure

Once you have it placed, I typically will include the Scripts into the solution, but you don’t need to do that, if you don’t plan to customize the auto generation scripts. You can do this by creating a new Solution Folder, in this example I called this folder CodeGen and inside I included the files from the scripts folder.

Additionally, jumping a little ahead here, but we can also pull in the Leprechaun.config that we will be describing what it is and how to configure it in the next section. In this example, I have included it in the Configuration folder, so that it’s easy to access from the solution.

You don’t need to include any of these in the solution as well, just providing an option in case you are making frequent changes to any of these files.

The next step related to installing Leprechaun in your solution, is to configure the Leprechaun.config file. You can pull the file from the contents you downloaded, it should exist inside the root. Copy this file and place in the folder that is the root to the code in your solution. It might sound a little strange to not place these configurations underneath a modules App_Config/Include, but keep in mind the Leprechaun is a Build tool and doesn’t work directly with Sitecore. Instead it’s going to work with your Rainbow .yml files only, which Unicorn is dealing with converting the templates in your Sitecore instance and turning them into Rainbow .yml files in the file system so that they can be committed to source control.

In my solution, I placed the Leprechaun.config under ~/Helix-Starter-Kit/src/Leprechaun.config, if you want to place it somewhere else, such as under the root of the entire source, you will need to make some customization’s to the gulp script and the Leprechaun configuration, so it can find everything.

Once you have placed the Leprechaun file in the correct location, the next step is to start editing that file. You can refer to the Leprechaun Github page for information about editing this file. I have included my specific configurations below, that you can use in your configuration.

<?xml version="1.0" encoding="utf-8" ?>
<leprechaun>
	<configurations import=".\*\*\code\CodeGen.config">
		<configuration name="Helix.Base" abstract="true">
			<codeGenerator scripts="Scripts/GlassMapper.csx, Scripts/Constants.csx, Scripts/Diagnostics.csx" outputFile="$(configDirectory)\$(layer)\$(module)\code\Models\$(layer).$(module).Model.cs" />
      			<dataStore physicalRootPath="$(configDirectory)\$(layer)\$(module)\Serialization" />
      			<templatePredicate rootNamespace="Helix.$(layer).$(module)" />
		</configuration>
	</configurations>

	<!-- Config shared across all configurations -->
	<shared name="Shared">
		<metadataGenerator type="Leprechaun.MetadataGeneration.StandardTemplateMetadataGenerator, Leprechaun" singleInstance="true" />
		<architectureValidator type="Leprechaun.Validation.StandardArchitectureValidator, Leprechaun" singleInstance="true" />
		<architectureValidatorLogger type="Leprechaun.Validation.StandardArchitectureValidatorLogger, Leprechaun" singleInstance="true" />
		<logger type="Leprechaun.Console.ConsoleLogger, Leprechaun.Console" singleInstance="true" />
		
		<rainbowSettings type="Leprechaun.Console.LeprechaunRainbowSettings, Leprechaun.Console"
						 serializationFolderPathMaxLength="110"
						 maxItemNameLengthBeforeTruncation="30"
						 singleInstance="false" />
	</shared>

	<!-- Defaults all configurations inherit unless overridden -->
	<defaults>
		<codeGenerator type="Leprechaun.CodeGen.Roslyn.CSharpScriptCodeGenerator, Leprechaun.CodeGen.Roslyn" singleInstance="true" />
		<dataStore physicalRootPath="$(configDirectory)\Unicorn\$(configurationName)" useDataCache="true" type="Rainbow.Storage.SerializationFileSystemDataStore, Rainbow" singleInstance="true"/>
		<templatePredicate type="Leprechaun.Filters.StandardTemplatePredicate, Leprechaun" rootNamespace="$(layer).$(module)" singleInstance="true">
			<include name="$(layer).$(module).Templates" path="/sitecore/templates/$(layer)/$(module)" />
		</templatePredicate>

		<fieldFilter type="Leprechaun.Filters.StandardFieldFilter, Leprechaun" singleInstance="true">
			<!-- 
				Excludes can either be field names (wildcards supported) or field IDs (e.g. <exclude fieldId="guid" />) 
				Note that these are TEMPLATE FIELD excludes (ignore on code generation), not excluding reading serialized item fields.
			-->
			<exclude name="__*" />
		</fieldFilter>

		<typeNameGenerator type="Leprechaun.MetadataGeneration.StandardTypeNameGenerator, Leprechaun" singleInstance="true" namespaceRootPath="/sitecore/templates/$(layer)/$(module)" />
		<templateReader type="Leprechaun.TemplateReaders.DataStoreTemplateReader, Leprechaun" singleInstance="true" />

		<!-- This should match up with the Unicorn/Rainbow configuration -->
		<serializationFormatter type="Rainbow.Storage.Yaml.YamlSerializationFormatter, Rainbow.Storage.Yaml" singleInstance="true">
			<fieldFormatter type="Rainbow.Formatting.FieldFormatters.MultilistFormatter, Rainbow" />
			<fieldFormatter type="Rainbow.Formatting.FieldFormatters.XmlFieldFormatter, Rainbow" />
			<fieldFormatter type="Rainbow.Formatting.FieldFormatters.CheckboxFieldFormatter, Rainbow" />
		</serializationFormatter>

		<!-- Tells Rainbow to let all fields that are serialized through; we do our own field filtering as we need to filter by template fields not items -->
		<rainbowFieldFilter type="Leprechaun.Filters.RainbowNullFieldFilter, Leprechaun" singleInstance="true" />

		<logger type="Leprechaun.Console.ConsoleLogger, Leprechaun.Console" singleInstance="true" />
	</defaults>
</leprechaun>

I’ve removed the lengthy comments from that example, but in your instance you should leave the comments to edit the configuration easier for those that are not familiar with the syntax elements of this xml document.

The last part that needs to be configured, is that each project (module in Helix) that needs code generation enabled, needs to include a CodeGen.config, by default, this configuration will be empty in most cases, and define the Layer followed by the Module name, assuming you are building using the Helix pattern. Then the configuration will extend from the base configuration. This is all part of the configuration enhancements that were built into Unicorn and it’s supported by Leprechaun as well. An example below, is a CodeGen.config I’ve configured for my Navigation Feature.

<?xml version="1.0" encoding="utf-8" ?>

<configuration name="Feature.Navigation" extends="Helix.Base">
</configuration>

One thing important to note, is that is all you need, you don’t need to wrap that in a or xml element. I struggled with that, when I first worked with this, but leaving them exactly the structure you see above, is all you need. Keep in mind this might change in future versions, and I can confirm this is the case with Version 1.0.

Build Process

So there are a lot of different ways you can run Leprechaun. I chose to include it as a task in my Gulp configuration, so I could run it as needed, but I can also add the task to the VSTS Build to run that task specifically (if you are not committing the models being generated to source control). Some of the options you have include:

You can integrate directly into the build, using a build event. Use a Gulp Task (which is what I’m demonstrating below) You can also create a project target. There are examples on the source main page that provides examples to the other methods, which I will not demonstrate in this tutorial. Below is an example of the gulp script I have configured. Make sure you define the npm packages you will need such as gulp-exec so you can run this in your gulp file.

var gulp = require("gulp");
var exec = require("child_process").exec;

// Run Leprechaun to Code Generate Models
gulp.task('_Code-Generation', function(x) {
  exec('.\\Tools\Leprechaun-1.0.0\\Leprechaun.console.exe /c .\\src\\Leprechaun.config', function (err, stdout, stderr) {
    console.log(stdout);
    console.log(stderr);
    x(err);
  });
});

I plan to do a video/article soon on the build process using VSTS and I will showcase the configuration needed to build these on the build server, so you wouldn’t need to commit the models in your solution.

Once you’ve completed the steps above, you should be able to refresh your Task Runner Explorer, and you should now see the task for _Code-Generation. When you run that task, it will spit out the results. You should see that it found x number of configurations and then it will tell you it ran x number of templates. If you are not seeing that, then you might have an issue with your source. Please feel free to reach out to me if you have issues!

General

Home
About

Stay up to date


© 2024 All rights reserved.