Introduction
We believe in the power of a template based approach to code generation. But, we believe in much more. We believe you need to fully control the structure and behavior of your code. We believe you need to easily manage exceptions, business rules, and other special rules and presentations. We believe you need the ability to easily define and maintain your coding best practices. We believe you need full support in maintaining, not just creating your generated code.
We believe that evolving a template based code generation approach into a model driven and model oriented approach is the best way to manage high quality generated code in conjunction with your custom code.
Object oriented programmers know the benefits of object oriented languages over procedural languages in creating large and complex software, benefits such as encapsulation, reusability, modularity, clarity, etc. Why shouldn't you have these same benefits from a code generation perspective? You do with Mo+! The Mo+ approach to code generation is the only one that is template based, model driven, truly model oriented, and focuses on code maintenance as well as initial code generation.<o:p>
In this article, we will compare two template based code generators (CodeSmith, and T4) with Mo+. We will use each approach to create a set of simple data access layer classes for each table in the Northwind SQL Server database. We will compare the end results, and the templates, in particular how the templates can be used as building blocks for more complex tasks.
Background
The Mo+ model oriented programming language and the Mo+ Solution Builder IDE for model oriented development was introduced in this Code Project article.
The Mo+ open source technology is available at moplus.codeplex.com. Video tutorials and other materials are also available at this site. The Mo+ Solution Builder also contains extensive on board help.
If you are following this article while using Mo+ Solution Builder and need to know how to load a solution model from a database, watch this tutorial on Loading a Model from SQL Server. You can use the templates attached to this article.
The Problem To Solve
We want to generate a set of simple data access layer C# classes, one for each table in the Northwind SQL Server database. The class will only include get/set properties for each table column. The following Category
class is an example of the desired output that corresponds with the Categories
table:
using System;
namespace Test3.DAL
{
public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public byte[] Picture { get; set; }
}
}
Solving It With CodeSmith
CodeSmith is a particularly powerful and flexible template based code generator. To solve this problem, we created a Generator project within the Visual Studio project where we wanted the DAL class files. The Generator project sets up the properties for, and calls the following Master
template:
<%@ CodeTemplate Src="TemplateBase.cs" Inherits="Lib.Templates.TemplateBase" OutputType="None" Language="C#" TargetLanguage="Text" Debug="False" %>
<%@ Property Name="SourceDatabase" Type="SchemaExplorer.DatabaseSchema" Optional="False" %>
<%@ Assembly Name="SchemaExplorer" %>
<%@ Property Name="ResultsFolder" Type="System.String" Default="DAL" Optional="False" %>
<%@ Import Namespace="System.IO" %>
<%@ Register Name="DALEntity" Template="DALEntity.cst" %>
<script runat="template">
public override void Render(TextWriter writer)
{
if (!Directory.Exists(ResultsFolder)) Directory.CreateDirectory(ResultsFolder);
foreach (TableSchema table in SourceDatabase.Tables)
{
CreateDALEntity(table);
}
}
public void CreateDALEntity(TableSchema table)
{
DALEntity dalEntity = this.Create<DALEntity>();
dalEntity.SourceTable = table;
dalEntity.ClassName = GetClassName(table.Name.ToCSharpIdentifier().ToPascalCase());
OutputFile outputFile = new OutputFile(GetClassFileName(ResultsFolder, dalEntity.ClassName));
dalEntity.RenderToFile(outputFile, true);
}
</script>
For each database table, the CreateDALEntity
method is called to create a DAL class file. This method creates an instance of the <code>DALEntity
template, sets up that template's properties, and then renders that template's content to an output DAL class file.
The Master
template inherits this base class that provides reusable methods to get best practice information for a class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CodeSmith.Engine;
namespace Lib.Templates
{
public partial class TemplateBase : CodeTemplate
{
public string GetClassName(string name)
{
string className = name;
if (className.EndsWith("ies") == true)
{
className = className.Substring(0, className.Length - 3) + "y";
}
else if (className.EndsWith("xes") == true
|| className.EndsWith("ses") == true)
{
className = className.Substring(0, className.Length - 2);
}
else if (className.EndsWith("as") == true
|| className.EndsWith("is") == true
|| className.EndsWith("os") == true
|| className.EndsWith("us") == true)
{
}
else if (className.EndsWith("s") == true)
{
className = className.Substring(0, className.Length - 1);
}
return className;
}
public string GetClassFileName(string directory, string className)
{
return String.Format("{0}/{1}.cs", directory, className);
}
}
}
The DALEntity
template builds the content for the DAL class file:
<%@ Template Language="C#" TargetLanguage="C#" Description="An example on creating a class with properties from a database table." %>
<%@ Property Name="SourceTable" Type="SchemaExplorer.TableSchema" Category="DataSource" Optional="False" %>
<%@ Property Name="Namespace" Type="System.String" Default="Test.DAL" Optional="False" %>
<%@ Property Name="ClassName" Type="System.String" Optional="False" %>
<%@ Assembly Name="SchemaExplorer" %>
<%@ Import Namespace="SchemaExplorer" %>
<%@ Import Namespace="CodeSmith.Core.Extensions" %>
using System;
namespace <%= Namespace %>
{
public class <%= ClassName %>
{
<% foreach (var column in SourceTable.Columns) { %>
/* this property gets/sets <%= column.Name.ToCSharpIdentifier().ToPascalCase() %> */
public <%= column.SystemType.FullName %> <%= column.Name.ToCSharpIdentifier().ToPascalCase() %> { get; set; }
<% } %>
}
}
This template utilizes the Namespace
and ClassName
properties set up by the Master
template, and then produces property content for each column in the table.
The Category
class generated by these templates looks like:
using System;
namespace Test.DAL
{
public class Category
{
public System.Int32 CategoryID { get; set; }
public System.String CategoryName { get; set; }
public System.String Description { get; set; }
public System.Byte[] Picture { get; set; }
}
}
Solving It With T4
T4 is also an effective template based code generator. To solve this problem, we created a Master
template within the Visual Studio project where we wanted the DAL class files. The Master
template is the high level driver in generating the code:
<#@ template language="C#" hostspecific="true" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="Microsoft.SqlServer.ConnectionInfo" #>
<#@ assembly name="Microsoft.SqlServer.Smo" #>
<#@ assembly name="Microsoft.SqlServer.Management.Sdk.Sfc" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="Microsoft.SqlServer.Management.Common" #>
<#@ import namespace="Microsoft.SqlServer.Management.Smo" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#
string connectionString = @"Server=INCODE-1;Trusted_Connection=True;";
string databaseName = "Northwind";
string resultsFolder = @"\DAL\";
if (!Directory.Exists(resultsFolder)) Directory.CreateDirectory(resultsFolder);
Server server = new Server(new ServerConnection(new SqlConnection(connectionString)));
Database database = server.Databases[databaseName];
foreach (Table table in database.Tables)
{
string tableName = table.Name;
if (!tableName.Equals("sysdiagrams"))
{
MasterTemplateHelper.CreateDALEntity(Host, connectionString, databaseName, tableName, resultsFolder);
}
}
#>
<#+
public class MasterTemplateHelper
{
public static void CreateDALEntity(ITextTemplatingEngineHost host, string connectionString,
string databaseName, string tableName, string resultsFolder)
{
string projectNamespace = "Test2.DAL";
string className = tableName.Replace(" ", "");
string classFileName;
string relativeOutputFilePath = null;
className = GetClassName(className);
classFileName = GetClassFileName(resultsFolder, className);
string templateFile = host.ResolvePath("DALEntity.tt");
string templateContent = File.ReadAllText(templateFile);
TextTemplatingSession session = new TextTemplatingSession();
session["Namespace"] = projectNamespace;
session["ClassName"] = className;
session["ConnectionString"] = connectionString;
session["DatabaseName"] = databaseName;
session["TableName"] = tableName;
var sessionHost = (ITextTemplatingSessionHost) host;
sessionHost.Session = session;
Engine engine = new Engine();
string generatedContent = engine.ProcessTemplate(templateContent, host);
relativeOutputFilePath = resultsFolder + className + ".cs";
WriteTemplateOutputToFile(relativeOutputFilePath, host, generatedContent);
}
public static string GetClassName(string name)
{
string className = name;
if (className.EndsWith("ies") == true)
{
className = className.Substring(0, className.Length - 3) + "y";
}
else if (className.EndsWith("xes") == true || className.EndsWith("ses") == true)
{
className = className.Substring(0, className.Length - 2);
}
else if (className.EndsWith("as") == true
|| className.EndsWith("is") == true
|| className.EndsWith("os") == true
|| className.EndsWith("us") == true)
{
}
else if (className.EndsWith("s") == true)
{
className = className.Substring(0, className.Length - 1);
}
return className;
}
public static string GetClassFileName(string directory, string className)
{
return String.Format("{0}/{1}.cs", directory, className);
}
public static void WriteTemplateOutputToFile(
string relativeOutputFilePath,
Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost Host,
string templateText)
{
string outputPath = System.IO.Path.GetDirectoryName(Host.TemplateFile);
string outputFilePath = outputPath + relativeOutputFilePath;
System.IO.File.WriteAllText(outputFilePath, templateText);
}
}
#>
For each database table, the CreateDALEntity
method is called to create a DAL class file. This method accesses the <code>DALEntity
template, sets up global TextTemplatingSession
variables (this could also be done with CallContext
), and then renders the <code>DALEntity
template's content to an output DAL class file.
The Master
template also includes MasterTemplateHelper
methods to get best practice information for a class, and for generating an output file.
The DALEntity
template builds the content for the DAL class file:
<#@ template language="C#" hostspecific="true" #>
<#@ parameter name="Namespace" Type="System.String" Default="MyProject.DAL" Optional="False" #>
<#@ parameter name="ClassName" Type="System.String" Optional="False" #>
<#@ parameter name="ConnectionString" Type="System.String" Optional="False" #>
<#@ parameter name="DatabaseName" Type="System.String" Optional="False" #>
<#@ parameter name="TableName" Type="System.String" Optional="False" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="Microsoft.SqlServer.ConnectionInfo" #>
<#@ assembly name="Microsoft.SqlServer.Smo" #>
<#@ assembly name="Microsoft.SqlServer.Management.Sdk.Sfc" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="Microsoft.SqlServer.Management.Common" #>
<#@ import namespace="Microsoft.SqlServer.Management.Smo" #>
<#
Server server = new Server(new ServerConnection(new SqlConnection(ConnectionString)));
Database database = server.Databases[DatabaseName];
Table sourceTable = database.Tables[TableName];
#>
using System;
namespace <#= Namespace #>
{
public class <#= ClassName #>
{
<#
foreach (Column column in sourceTable.Columns)
{
#>
/* this property gets/sets <#= column.Name #> */
public <#= DALTemplateHelper.GetClrType(column.DataType.ToString()) #> <#= column.Name #> { get; set; }
<#
}#>
}
}
<#+
public class DALTemplateHelper
{
public static string GetClrType(string sqlType)
{
switch (sqlType)
{
case "bigint":
return "long";
case "binary":
case "image":
case "timestamp":
case "varBinary":
return "byte[]";
case "bit":
return "bool";
case "char":
case "nchar":
case "ntext":
case "nvarchar":
case "text":
case "varchar":
case "xml":
return "string";
case "datetime":
case "smalldatetime":
case "date":
case "time":
case "datetime2":
return "DateTime";
case "decimal":
case "money":
case "smallmoney":
return "decimal";
case "float":
return "double";
case "int":
return "int";
case "real":
return "float";
case "uniqueidentifier":
return "Guid";
case "smallint":
return "short";
case "tinyint":
return "byte";
case "variant":
case "udt":
return "object";
case "structured":
return "DataTable";
case "datetimeoffset":
return "DateTimeOffset";
default:
return "object";
}
}
}
#>
This template utilizes the Namespace
, ClassName
, ConnectionString
, DatabaseName
, and TableName
properties set up by the Master
template, and then produces property content for each column in the table. A DALTemplateHelper
method is also provided to get the CLR type from the SQL Server type.
The Category
class generated by these templates looks like:
using System;
namespace Test2.DAL
{
public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public byte[] Picture { get; set; }
}
}
Solving It With Mo+
To solve this problem with Mo+, we created a Solution
model in the same directory as the Visual Studio project where we wanted the DAL class files. Using the MDLSqlModel
SQL Server spec template (attached), this model is loaded with basic Entity
and Property
information that corresponds to tables and columns in the Northwind database. Unlike with CodeSmith or T4, with Mo+, you have ready access to model information that will make it much easier on the code generation side. You also have the ability to define and augment your model programatically or manually with the information you need to make it easier to generate code.
The Solution
level Master
template is the high level driver in generating the code. This template has no content, and its output area is as follows (images are shown next to code blocks to show proper syntax highlighting):
<%%:
foreach (Entity)
{
<%%>DALEntity%%>
}
%%>
This template goes through each Entity
(table) in the solution, and calls the Entity
level DALEntity
template to output its content. At line 4, the DALEntity
template is embodied within output tags to signify the template to produce its output.
With model information and model oriented templates, creating master templates is dead simple. There are no additional hoops to jump through in setting up properties or setting up calls to other templates. Instead of needing to pass in all of the relevant data in a procedure like manner, the templates being called encapsulate all of the information they need to produce their content AND output from the model and/or other templates (if necessary, templates can also have parameters to pass in additional data).
The content area of the DALEntity
template produces the content for the DAL class:
<%%-using System;
namespace %%><%%=Solution.Namespace%%><%%-
{
public class %%><%%=DALClassName%%><%%-
{%%>
<%%:
foreach (Property)
{
<%%=DALProperty%%>
}
%%><%%-
}
}%%>
The information to build the DAL class content is pulled directly from the model, in a model oriented way based on which instance of Entity
this template call is made. The namespace is retrieved from the Solution
. The class name is retrieved from an Entity
level template called DALClassName
. Then, the template goes through each Property
for the Entity
, and gets the property content from the Property
level DALProperty
template.
Unlike with CodeSmith or T4 where you have to pass data into a template with a procedural approach, with Mo+, the encapsulated nature of templates encourages natural building blocks. In fact, Mo+ templates can be used like any built in model oriented property (or method if the template has parameters) to retrieve information to be used in expressions, or to build up content within property tags (as above).
With CodeSmith or T4 , you have little support over where, when, or how to output your generated documents. Your documents generally get regenerated every time. With Mo+ however, full control over output decision making is built in. The output area of the DALEntity
template embodies the decision making for writing the DAL class content to disk:
<%%=Solution.SolutionDirectory%%><%%-\DAL\%%><%%=DALClassFileName%%>
<%%:
if (File(Path) != Text)
{
update(Path)
}
%%>
Line 1 builds up the Path
where the DAL class file is to be stored. At line 3, if the contents of the file on disk is different than the Text
(template content), then the update
statement is called to update the Text
of the file at Path
. Encapsulating the output decision making here at the template level improves the reusability, since this decision making does not have to be replicated by each calling template.
Instead of using a procedure to get class name information, the Entity
level DALClassName
template is a reusable building block:
<%%:
var className = EntityName.CapitalCase().Replace(" ", "").Replace("_", "")
if (className.EndsWith("ies") == true)
{
className = className.Substring(0, className.Length - 3) + "y"
}
else if (className.EndsWith("xes") == true
|| className.EndsWith("ses") == true)
{
className = className.Substring(0, className.Length - 2)
}
else if (className.EndsWith("as") == true
|| className.EndsWith("is") == true
|| className.EndsWith("os") == true
|| className.EndsWith("us") == true)
{
}
else if (className.EndsWith("s") == true)
{
className = className.Substring(0, className.Length - 1)
}
%%>
<%%=className%%>
For further simplification, the above logic could be used to create the name for the Entity
when loading the model from the database.
Finally, the Property
level DALProperty
template builds up the content for the get/set property:
<%%-
public %%><%%=CSharpDataType%%><%%- %%><%%=PropertyName%%><%%- { get; set; }%%>
Two other templates used in this example, DALClassFileName
and CSharpDataType
are attached.
The Category
class generated by these templates looks like:
using System;
namespace Test3.DAL
{
public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public byte[] Picture { get; set; }
}
}
Scaling Your Templates
Clearly, you can solve the above code generation problem easily with CodeSmith , T4, or Mo+, How well do these methods scale as the complexity and scope of your code generation needs increase?
How can you scale to manage exceptions? Say for example that certain properties in your DAL classes needed to be read only. This would be difficult to do in CodeSmith or T4, if you cannot glean from the database schema directly which properties should be read only. But it is easy with Mo+. You can easily tag properties or other model elements with any number of free form tag keywords such as READ_ONLY
. An updated DALProperty
template to manage the read only property exceptions could look something like:
<%%-
%%>
<%%:
if (Tags.Contains("READ_ONLY") == true)
{
<%%-
private %%><%%=CSharpDataType%%><%%- %%><%%=PropertyName.UnderscoreCase()%%><%%-
public %%><%%=CSharpDataType%%><%%- %%><%%=PropertyName%%><%%- { get { return %%><%%=PropertyName.UnderscoreCase()%%><%%-;) }%%>
}
else
{
<%%-
public %%><%%=CSharpDataType%%><%%- %%><%%=PropertyName%%><%%- { get; set; }%%>
}
%%>
How can you scale to wire together distant elements in the same or different layers? Say for example that you want to augment your DAL classes to include List
properties of other DAL classes. This would be cumbersome to do in CodeSmith or T4, where you would need to retrieve your distant table, and call the GetClassName
function to retrieve the name of the class. But again, it is easy with Mo+. Just reuse the DALClassName
template with an Entity
from a model search, or directly from a related Entity
in the model. In the updated DALEntity
template,, property information is created for each related Collection
for the Entity
:
<%%-using System;
namespace %%><%%=Solution.Namespace%%><%%-
{
public class %%><%%=DALClassName%%><%%-
{%%>
<%%:
foreach (Property)
{
<%%=DALProperty%%>
}
foreach (Collection)
{
<%%-
List<%%><%%=ReferencedEntity.DALClassName%%><%%-> %%><%%=ReferencedEntity.DALClassName%%><%%-List { get; set; }%%>
}
%%><%%-
}
}%%>
In Summary
Hopefully this article has given you a good idea on how the Mo+ model oriented approach compares with other template based approaches, and how a model oriented approach is ideally suited to scale and address more complex code generation requirements. Please ask questions if anything is not clear or should be expanded upon.
Also, please let me know if there are better or more scalable ways to solve the above problem using either CodeSmith or T4. I will gladly update the article to show the best use of those methods.
Finally, I hope you try Mo+ and the Mo+ Solution Builder. The free, open source product is available at moplus.codeplex.com. In addition to the product, this site contains sample packs for building more complete models and generating complete working applications.
Become a Member!
The Mo+ community gets additional support, and contributes to the evolution of Mo+ via the web site at https://modelorientedplus.com. Being a member gives Mo+ users additional benefits such as additional forum support, member contributed tools, and the ability to vote on and/or contribute to the direction of Mo+. Also, we will be running monthly contests for members, where you can win $ for developing solutions using Mo+.
If you are the least bit interested in efficient model oriented software development, please sign up as a member. It's free, and you won't get spam email!