In my previous post, I’ve described the benefits of composition pattern over inheritance in code generation and mentioned that it simplifies restoration of models from reflection. From my point, this is a huge plus as it enables the creation of developer-friendly modular architecture in Sitecore.

I do not think that someone still has problems with a creation of an update package with Sitecore config transforms, but it is not possible to install an update package in your Visual Studio solution. Of cause, you can install it in Sitecore, grab all libraries from the bin folder and copy them to your source, then serialize installed items into your project, generate your models based on “everything” you have in Sitecore.

To me, it doesn’t sound like a good approach, moreover, there are many solutions like NuGet, npm, bower that drastically simplify package management and installation for development. Also, when referencing items from packages, you don’t expect to modify DLL coming with them, so I do not think that you should have to serialize Sitecore items to use them in your model generation.

Most likely you already have information about your templates compiled into your module DLLs. Below you would see how you could use it.

Approach

Module Preparation

Reflection in .NET gives you the ability to discover information about classes defined in DLLs through its metadata. You could define custom attributes that would mark your classes with template IDs and properties with fields IDs, or if you using GlassMapper you could use attributes defined in it.

1
2
3
4
5
6
7
[SitecoreType(TemplateId="b820da23-c7f6-4e04-b159-7d804a84a3d8")]
public partial class BasePage : GlassBase, IBasePage
{
   ...
   [SitecoreField("Title")]
   public virtual string Title {get; set;}
   ...

Once your attributes are set, you can compile them and pack your module. Details of packing are not relevant as of now, but for the sake of simplicity, let’s assume that you will just reference this DLL from a folder (in real case scenario it should be done via NuGet)

Reference Module

For code generation, you cannot reference an assembly as you would do this standard C# project. As T4 templates are used, we need to pass a location of the assembly to the T4 template somehow.

The best place for that would be a project file, however, once you dug into TDS, that I’m using to generate models, I found out that the project file was not available in an item.tt. So I ended up adding project information into a template as a string variable (in a separate post I will show how this could be done automatically).

Once I got the project file location, I could read this file and get custom properties stored there.

Search for this reference

In the project file, I’ve added few elements like CodeGenerationLocations and *CodeGenerationFilters. *In those elements, I store paths where DLLs might be located separated by pipe and mask for file filtering.

1
2
3
4
5
6

<!--Set paths saparated by pipe-->
<CodeGenerationLocations>d:\sln\project\Debug\bin\|d:\sln\packages\</CodeGenerationLocations>
<!--Set DLL filter separated by pipe. e.g.: Sample1.*.dll|Sample2.*.dll-->
<CodeGenerationFilters>sln.*.dll</CodeGenerationFilters>

Using those variables I iterate over files and get required DLLs.

Identify model files

Once assembly is loaded from a file, it is possible to get all classes or interfaces defined in this assembly and filter required for your base on attributes e.g.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

foreach (var type in allTypes)
{
  if (type.IsInterface &&
    type.GetCustomAttributes()
      .Any(
        attr =>
          attr.GetType()
            .FullName.Contains(
              "Glass.Mapper.Sc.Configuration.Attributes.SitecoreTypeAttribute")))
  {
    // create dictionary using types metadata
  }
}

Once type information is gathered, it is possible to use it in template generation.

**Generate models and restore base templates **

To generate code, you need to wrap the code above to a singleton implementation that you call on each item and header templates. Singleton pattern, in this case, will ensure that you initialize code once and not slowing down the generation.

1
2
3
4

string projectPath = @"path to .scproj file";
CodeGeneration.Manager.Init(projectPath);

The following code pattern will generate a class header, constants for a template, and fields declared on the template as well as field properties themselves.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

<# if (!template.Name.IsInterfaceWord()){ #>
    ///
<summary>
    /// <#= template.Name.AsClassName() #>
    /// <para>Path: <#= template.Path #></para>
    /// <para>ID: <#= template.ID.ToString() #></para>
    /// </summary>

    [SitecoreType(TemplateId="<#= template.ID.ToString() #>")]
    public partial class <#= template.Name.AsClassName() #>  : GlassBase, <#=template.Name.AsInterfaceName()#>
    {

        public const string TemplateIdString = "<#= template.ID.ToString() #>";
        public static readonly ID TemplateId = new ID(TemplateIdString);
        public const string TemplateName = "<#= template.Name #>";
<#   foreach(SitecoreField field in template.GetFieldsForTemplate(false)){#>
        public static readonly ID <#= field.GetPropertyName() #>FieldId = new ID("<#=field.ID.ToString()#>");
        public const string <#= field.GetPropertyName() #>FieldName = "<#=field.Name#>";

<#   }#>
<#   foreach(SitecoreField field in template.GetFieldsForTemplate(false)){#>
    ///
<summary>
    /// The <#=field.Name#> field.
    /// <para>Field Type: <#=field.Type#></para>
    /// <para>Field ID: <#=field.ID.ToString()#></para>
<#     if(!string.IsNullOrEmpty(field.Data)) { #>
    /// <para>Custom Data: <#=field.Data#></para>
<#     }#>
    /// </summary>

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("<#=Tool#>", "<#=ToolVersion#>")]
    [SitecoreField("<#=field.Name#>")]
    public virtual <#=field.GetGlassFieldType()#> <#= field.GetPropertyName() #>  {get; set;}

<#   }#>

After that, we generate properties from base templates known in this TDS project. Those properties are marked with SitecoreSelf attribute, that map the same item as mapped to the model to a property, while the property is a type of another autogenerated model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

<#  foreach(var baseTemplate in template.BaseTemplates) { #>
    ///
<summary>
    /// The <#=baseTemplate.Name#> composition field.
    /// </summary>

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("<#=Tool#>", "<#=ToolVersion#>")]
    [SitecoreSelf]
        public virtual <#= baseTemplate.GetNamespace(DefaultNamespace)#>.<#=baseTemplate.Name.AsClassName()#> <#= baseTemplate.Name.AsClassName() #>Comp  {get; set;}

<#  }#>

The final section will use the extension method that will load templates that do not exist in the current TDS project from a previously prepared dictionary and generate properties base on that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

<#  foreach(var tuple in Model.BaseTemplates()) { #>
    ///
<summary>
    /// The <#=tuple.Item2#> composition field.
    /// <para>Generated from referenced assembly meta information.</para>
    /// </summary>

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("<#=Tool#>", "<#=ToolVersion#>")]
    [SitecoreSelf]
    public virtual <#=tuple.Item1#> <#=tuple.Item2#>Comp  {get; set;}

<#  }#>
  }
<#  }#>

Summary

The approach above allows you to generate code in your project using only meta-information from existing precompiled DLLs with included autogenerated models, constants for templates and fields. It helps to be compliant with an open/close SOLID principle creating modules, by eliminating the need to carry serialized Sitecore or TDS items in packages and preventing changes to the functionality of this package.


Follow me on Twitter @true_shoorik. Would be glad to discuss the ideas above in the comments.