Writing Code Generators in .NET - ' Writing Code Generators for ' (
Page 2 of 3 )
Entities">
Archimedes is quoted as having said, "Give me a lever and a place to stand and I can move the earth." This statement endures because it introduces a concept so powerful and simple that it can profoundly change lives. A lever and fulcrum can move a much greater mass than can direct force of an equal amount. Thus, Archimedes essentially introduced what we refer to in general terms as leverage.
Politicians get dirt on opponents to have leverage against them. Husbands and wives play politically games to lever power back and forth (unless they want to stay married). But, striving to keep my better-self centered, I like to think Archimedes intended leverage to be used for good. In that vein, this article is about leveraging a developer's time.
ADVERTISEMENT
Suppose the average programmer writes 9,000 lines of code per year. A 90,000 line application would take ten staff years. Now, suppose that the average programmer leverages his time, and writes 9,000 lines of code generators that produce 9,000 lines of new code each time it is run. A 9,000 line code generator might run in 20 seconds. How many times could such a generator be run in a year? Consequently, how many hundreds of thousands of lines of code could be generated in one staff year of programming? Do the math: 90,000 lines of code could be generated in under four minutes. In effect, the average programmer would have leveraged his time and could be 10, 100, or even 1,000 times more productive.
Microsoft's .NET contains a comprehensive and powerful CODEDOM namespace that makes writing code generators relatively straight forward, albeit a tad verbose. Why then are programmers not spending their time writing code generators? Maybe the answer is that no one is sure what to write the generators to do.
In this article, I demonstrate how to write code generators, and show you what could easily be adopted as an enterprise pattern for reliably building entity and database objects. I also demonstrate how to factor out reusable bits to avoid generating too much redundant code. And, while this generator does not produce an application for you, if you write generators for common patterns, use snippets, macros, and tools like CodeRush, I am certain you will significantly leverage your time and dramatically improve your productivity.
Note: This article does not talk about CodeRush, but it is one of the best productivity tools I have seen in years. It is worth investigating.
Writing Code Generators for Entities
Knowing what to write really is more than half the battle. If you can decide on an architectural pattern for building a class of applications — for example, database applications — then you can spend your time writing code generators for things that are similar in a category of applications.
Design patterns give us an idea of how to write the glue that makes programs behave and create in an orderly fashion. Perhaps because a huge percentage of applications include a database, the first category of application a large number of generators are written for is the database application. That's how I picked which generators I'd write: the ones that replace the need to manually write a lot of tedious and boring code, entity class and data access classes.
Generally, ADO.NET and DataSets help one get a quick start on building database applications. I very quickly find DataSets wanting. First of all, dragging ADO through all levels of an application adds overhead, and DataSets are not very OOPy because all operations are data-centric. Hence, I find myself writing custom entity classes for most applications and custom data access. However useful, powerful, and flexible custom entity classes are boring and tedious to write. Thus, writing an entity class generator is a no-brainer.
All a custom entity class generator needs to do is read the schema of a table and spit out a constructor and properties. Very straight forward code, and consequently a pretty straight forward code generator. Listing 1 shows a FieldDefinition class that basically represents a table's schema; Listing 2 shows a simple schema reader, and Listing 3, given a generic list of FieldDefinitions, spits out a perfect entity class every time. The code run against the Customer table of the Northwind database is shown in Listing 4.
Listing 1 and Listing 2 contain pretty straight forward C# code. Listing 3 contains the generated entity class. A partial class is generated so we can put custom code in a separate file in case we need to re-generate the entity class, and the class is made Serializable in case we use this class in ASP.NET and want to store it in Session. Finally, the code generator (in Listing 3) can generate Nullable types for value types. Using nullable types — as in int? — permits us to assign null to a field while still treating that field like an intrinsic type. Nullable fields are useful because sometimes database fields are null. Let's explore Listing 3 next.
Listing 1: A FieldDefinition class condenses a database field down to its essence, the name and type.
using System;
using System.Collections.Generic;
using System.Text;
namespace codegen
{
public class FieldDefinition
{
public FieldDefinition(string fieldName, Type type)
{
this.propertyName = fieldName;
this.fieldName = "_" + fieldName;
this.type = type;
}
private Type type;
public Type Type
{
get
{
return type;
}
}
private string fieldName;
public string FieldName
{
get
{
return fieldName;
}
}
private string propertyName;
public string PropertyName
{
get
{
return propertyName;
}
}
}
}
Listing 2: The SchemaReader class is used to initialize a List objects representing a single table.
using System;
using System.Collections.Generic;
using System.Text;
using codegen.Properties;
using System.Data;
using System.Data.SqlClient;
namespace codegen
{
public class SchemaReader
{
public static List<FieldDefinition> ReadSchema(string tableName)
{
return ReadSchema(Settings.Default.SQL, tableName);
}
public static List<FieldDefinition>
ReadSchema(string connectionString, string tableName)
{
List<FieldDefinition> fields = new List<FieldDefinition>();
using(SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand command = new SqlCommand("SELECT * FROM " + tableName, connection);
DataTable table = new DataTable();
SqlDataAdapter adapter = new SqlDataAdapter(command);
adapter.FillSchema(table, SchemaType.Source);
foreach(DataColumn c in table.Columns)
fields.Add(new FieldDefinition(c.ColumnName, c.DataType));
}
return fields;
}
}
}
Listing 3: The EntityGenerator
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.CodeDom;
using System.CodeDom.Compiler;
using codegen.Properties;
using Microsoft.CSharp;
using System.Windows.Forms;
using System.IO;
namespace codegen
{
public class EntityGenerator
{
public static string Run(List<FieldDefinition> fields, string name)
{
using(CodeDomProvider provider = new CSharpCodeProvider())
{
CodeCompileUnit unit = new CodeCompileUnit();
CodeNamespace codeNamespace = new CodeNamespace("MyNameSpace.BusinessObjects");
codeNamespace.Imports.Add(new CodeNamespaceImport("System"));
unit.Namespaces.Add(codeNamespace);
CodeTypeDeclaration type = new CodeTypeDeclaration();
type.Name = name;
type.IsClass = true;
type.IsPartial = true;
type.CustomAttributes.Add(new CodeAttributeDeclaration("Serializable"));
codeNamespace.Types.Add(type);
CodeConstructor ctor = new CodeConstructor();
ctor.Attributes = MemberAttributes.Public
| MemberAttributes.Final;
ctor.Comments.Add(
new CodeCommentStatement("Default constructor supports serialization"));
type.Members.Add(ctor);
List<FieldDefinition> list = SchemaReader.ReadSchema(name);
WriteFields(list, type);
WriteProperties(list, type);
WriteConstructor(list, type);
CodeGeneratorOptions options = new CodeGeneratorOptions();
options.BlankLinesBetweenMembers = true;
options.BracingStyle = "C";
options.IndentString = " ";
TextWriter writer = new StringWriter();
try
{
provider.GenerateCodeFromCompileUnit(unit, writer, options);
string code = writer.ToString();
Clipboard.SetDataObject(code, false);
return code;
}
finally
{
writer.Close();
}
}
}
private static void WriteFields(List<FieldDefinition> list,
CodeTypeDeclaration type)
{
foreach(FieldDefinition f in list)
WriteField(f, type);
}
private static void WriteField(FieldDefinition definition,
CodeTypeDeclaration type)
{
type.Members.Add(WriteField(
definition.FieldName, definition.Type));
}
private static CodeMemberField WriteField(
string fieldName, Type type)
{
CodeMemberField field = null;
if(type.IsValueType)
{
CodeTypeReference t2 = new CodeTypeReference(typeof(System.Nullable));
t2.TypeArguments.Add(type);
field = new CodeMemberField(t2, fieldName);
}
else
{
field = new CodeMemberField(type, fieldName);
}
field.Attributes = MemberAttributes.Private;
return field;
}
private static void WriteProperties(List<FieldDefinition> list,
CodeTypeDeclaration type)
{
foreach(FieldDefinition f in list)
WriteProperty(f, type);
}
private static void WriteProperty(FieldDefinition definition,
CodeTypeDeclaration type)
{
type.Members.Add(GetClassProperty(
definition.PropertyName, definition.FieldName,
definition.Type));
}
public static CodeMemberProperty GetClassProperty(
string propertyName, string fieldName, Type type)
{
CodeMemberProperty property = null;
if(type.IsValueType)
{
CodeTypeReference t2 = new CodeTypeReference(typeof(System.Nullable));
t2.TypeArguments.Add(type);
property = new CodeMemberProperty();
property.Type = t2;
}
else
{
property = new CodeMemberProperty();
property.Type = new CodeTypeReference(type);
}
property.Name = propertyName;
property.Attributes = MemberAttributes.Public |
MemberAttributes.Final;
property.GetStatements.Add(
new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(
null, fieldName)));
property.SetStatements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(
null, fieldName),
new CodeArgumentReferenceExpression("value")));
return property;
}
private static void WriteConstructor(List<FieldDefinition> list,
CodeTypeDeclaration type)
{
if( list.Count == 0 ) return;
// define initialization constructor
CodeConstructor ctor = new CodeConstructor();
ctor.Attributes = MemberAttributes.Public |
MemberAttributes.Final;
foreach(FieldDefinition definition in list)
{
//assign parameters
ctor.Parameters.Add(
new CodeParameterDeclarationExpression(
definition.Type, definition.FieldName));
// do fields assignments
ctor.Statements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(), definition.FieldName),
new CodeArgumentReferenceExpression(definition.FieldName)));
}
type.Members.Add(ctor);
}
}
}
Listing 4: A Customer class generated as a partial class.
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:2.0.50727.42
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace MyNameSpace.BusinessObjects
{
using System;
[Serializable()]
public partial class Customers
{
private string _CustomerID;
private string _CompanyName;
private string _ContactName;
private string _ContactTitle;
private string _Address;
private string _City;
private string _Region;
private string _PostalCode;
private string _Country;
private string _Phone;
private string _Fax;
// Default constructor supports serialization
public Customers()
{
}
public Customers(string _CustomerID, string _CompanyName, string _ContactName,
string _ContactTitle, string _Address, string _City, string _Region,
string _PostalCode, string _Country, string _Phone, string _Fax)
{
this._CustomerID = _CustomerID;
this._CompanyName = _CompanyName;
this._ContactName = _ContactName;
this._ContactTitle = _ContactTitle;
this._Address = _Address;
this._City = _City;
this._Region = _Region;
this._PostalCode = _PostalCode;
this._Country = _Country;
this._Phone = _Phone;
this._Fax = _Fax;
}
public string CustomerID
{
get
{
return _CustomerID;
}
set
{
_CustomerID = value;
}
}
public string CompanyName
{
get
{
return _CompanyName;
}
set
{
_CompanyName = value;
}
}
public string ContactName
{
get
{
return _ContactName;
}
set
{
_ContactName = value;
}
}
public string ContactTitle
{
get
{
return _ContactTitle;
}
set
{
_ContactTitle = value;
}
}
public string Address
{
get
{
return _Address;
}
set
{
_Address = value;
}
}
public string City
{
get
{
return _City;
}
set
{
_City = value;
}
}
public string Region
{
get
{
return _Region;
}
set
{
_Region = value;
}
}
public string PostalCode
{
get
{
return _PostalCode;
}
set
{
_PostalCode = value;
}
}
public string Country
{
get
{
return _Country;
}
set
{
_Country = value;
}
}
public string Phone
{
get
{
return _Phone;
}
set
{
_Phone = value;
}
}
public string Fax
{
get
{
return _Fax;
}
set
{
_Fax = value;
}
}
}
}