Part 6 - Generating Some Classes (and an Interface)
In this installation of my blog series I'll cover creating integrated InterfaceObject, DataAccess and ApplicationObject (BusinessObject) classes based on the definition file that use these newly created stored procedures and provide an API to the users of our classes.
The Interface, Data Access and Application objects will be created using the field values from the CodeGeneratorPropertyList. The interface will define the basic property signatures for each field. The Data Access class will perform CRUD and list retrieval based on the stored procedures just created. The Application Object will interact with the Data Access class using primitive values where possible and the generated interface when not. So, when a new item is created the DataAccess would be expecting an instance of a class that implements the interface to be passed to it. The Application Object will pass itself as it is just such a class.
The Application Object will also handle keeping track of whether or not it is dirty (a value has changed since instantiation) and whether or not it is new (the value does not yet exist in the database), s well as check some business rules. It does this by inheriting from a base class that is standard within the code generator. This will reduce unnecessary calls to persist unchanged items to the database or to try and delete items that do not yet exist. The CheckRules method will initially just check that required fields are present and that fields with a given length (varchar, char, etc.) are not longer than allowed.
The Application Object and the Data Access classes will both use partial classes and partial methods to take advantage of the Code Generator's ability to generate new code while at the same time not losing any modified code. The CheckRules method is a partial method. The first time the file is generated the generator creates a method signature in the upper code generated class and then implements the actual method in the custom code section. The generator does not do this if the file already exists so as not to lose any custom code. Unfortunately, this means that any new rules for required fields or field length need to be implemented manually.
This will be changed in the future to make the CheckRules method a standard code method and add a call to a new partial method named CheckCustomRules. This is a great example of one of the enormous advantages in creating and using a custom, code generator -the ability to incrementally improve it as time goes on. This allows the developer to deliver more robust, re-usable code in less time.
The code to generate the interface is very straightforward. It writes the using statements, the namespace and interface name using the CreateNameSpaceHeader method:
private static string CreateNameSpaceHeader(string Namespace, string ClassName, IEnumerable<CodeGeneratorProperty> PropertyList)
{
StringBuilder retVal = new StringBuilder();
retVal.Append(string.Format("using System;{0}", CodeGenerationHelper.GetEndOfLine()));
foreach (CodeGeneratorProperty property in PropertyList)
{
if (property.DataType.EndsWith("List"))
{
retVal.Append(string.Format("using System.Collections.Generic;{0}", CodeGenerationHelper.GetEndOfLine()));
break;
}
}
retVal.Append(CodeGenerationHelper.GetEndOfLine());
retVal.Append(string.Format("namespace {0}{1}", Namespace, CodeGenerationHelper.GetEndOfLine()));
retVal.Append(string.Format("{{{0}", CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}///<summary>{1}", CodeGenerationHelper.GetTabIndent(1),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}///The public interface for the {1} object{2}", CodeGenerationHelper.GetTabIndent(1),
ClassName, CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}///</summary>{1}", CodeGenerationHelper.GetTabIndent(1),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(string.Format("\tpublic interface I{0} " + CodeGenerationHelper.GetEndOfLine(), ClassName));
retVal.Append(string.Format("\t{{{0}", CodeGenerationHelper.GetEndOfLine()));
return retVal.ToString();
}
and then iterates through the CodeGeneratorPropertyList to create the property signatures using the CreateAccessorSignatures method:
private static string CreateAccessorSignatures(IEnumerable<CodeGeneratorProperty> PropertyList)
{
StringBuilder retVal = new StringBuilder();
retVal.Append(
string.Format("{0}#region Property Signatures {1}", CodeGenerationHelper.GetTabIndent(2),
CodeGenerationHelper.GetEndOfLine()));
foreach (CodeGeneratorProperty property in PropertyList)
{
retVal.Append(
string.Format("{0}/// <summary>{1}",
CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}/// {1}{2}",
CodeGenerationHelper.GetTabIndent(2), property.Description, CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}/// </summary>{1}",
CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));
if (property.DataType.EndsWith("List"))
{
retVal.Append(
string.Format("{0}List<{1}> {2}{3}",
CodeGenerationHelper.GetTabIndent(2),
property.DataType.Substring(0, property.DataType.LastIndexOf("List")),
property.PropertyName,
CodeGenerationHelper.GetEndOfLine()));
}
else
{
retVal.Append(
string.Format("{0} {1} {2}{3}",
CodeGenerationHelper.GetTabIndent(2), property.DataType, property.PropertyName,
CodeGenerationHelper.GetEndOfLine()));
}
retVal.Append( CodeGenerationHelper.GetStandAloneOpeningBracket(2));
if (property.IncludeGet)
{
retVal.Append( string.Format("{0} get;{1}",
CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));
}
if (property.IncludeSet)
{
retVal.Append( string.Format("{0} set;{1}",
CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));
}
retVal.Append( CodeGenerationHelper.GetStandAloneClosingBracket(2));
retVal.Append( CodeGenerationHelper.GetEndOfLine());
}
retVal.Append(
string.Format("{0}#endregion Property Signatures{1}", CodeGenerationHelper.GetTabIndent(2),
CodeGenerationHelper.GetEndOfLine()));
return retVal.ToString();
}
This method also handles the use of List classes that are typically foreign key based collections (e.g UserAddresses is a UserAdressList that has the UserId as the Foreign Key).
The Data Access generation is also very straightforward but with a few more moving parts. It also creates the header in the same way but then generates the constants that reference the stored procedures created for the CRUD operations, it then uses the CodeGeneratorPropertyList to create Data Access methods (Find, Fetch, Add, Change and Remove) - these methods will be called by the Application Object methods as follows:
|
Application Object Method |
Data Access Method |
|
Exists |
Find |
|
Select |
Fetch |
|
Save - depending on the IsNew property - Add if IsNew; Change if not IsNew |
Add |
|
Change |
|
Delete |
Remove |
A good example of the code generator method for the Data Access class would be CreateAddMethod:
private static string CreateAddMethod(string ClassName, IEnumerable<CodeGeneratorProperty> PropertyList)
{
StringBuilder retVal = new StringBuilder();
retVal.Append(
string.Format("{0}#region Add Data {1}", CodeGenerationHelper.GetTabIndent(2),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}// called to add new data into the database{1}", CodeGenerationHelper.GetTabIndent(2),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}internal static bool Add(ref int insertedId, I{1} {1}Object){2}",
CodeGenerationHelper.GetTabIndent(2), ClassName, CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}using (SqlConnection cn = ConnectionManager.OpenConnection()){1}",
CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}try{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}using (SqlCommand cm = cn.CreateCommand()){1}", CodeGenerationHelper.GetTabIndent(5),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(5), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}cm.CommandType = CommandType.StoredProcedure;{1}",
CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}cm.CommandText = INSERT_SPROC;{1}", CodeGenerationHelper.GetTabIndent(6),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}cm.Parameters.Add(\"returnValue\", SqlDbType.Int);{1}",
CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format(
"{0}cm.Parameters[\"returnValue\"].Direction = ParameterDirection.ReturnValue;{1}",
CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(CreateParameterList(ClassName, PropertyList, 6));
retVal.Append(
string.Format("{0}cm.ExecuteNonQuery();{1}", CodeGenerationHelper.GetTabIndent(6),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}insertedId = SafeData.ConvertInt(cm.Parameters[\"@{1}{2}\"].Value);{3}",
CodeGenerationHelper.GetTabIndent(6),
CommonHelper.GetVariableAbbreviation(CommonHelper.GetPrimaryKey(PropertyList)),
CommonHelper.GetPrimaryKeyName(PropertyList), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}if (Convert.ToInt16(cm.Parameters[\"returnValue\"].Value) != 0){1}",
CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}return false;{1}", CodeGenerationHelper.GetTabIndent(7),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}else{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}return true;{1}", CodeGenerationHelper.GetTabIndent(7),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(5), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}catch (Exception ex){1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}SimpleLogger.LogError(ex.Message);{1}", CodeGenerationHelper.GetTabIndent(5),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}return false;{1}", CodeGenerationHelper.GetTabIndent(5),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}#endregion Add Data {1}", CodeGenerationHelper.GetTabIndent(2),
CodeGenerationHelper.GetEndOfLine()));
return retVal.ToString();
}
Each of the methods works in the same fashion, creating the method and where necessary calling a helper method named CreateParameterList to generate each Property as it is needed. The List class has the same process applied to it but only for the SelectList and SelectListByForeignKey methods. The SelectListByForeignKey are generated for each Foreign Key individually, a future enhancement might be to define multiple key foreign keys in the code generator and have those methods generated as well. Currently this has to be done manually.
Finally the Application Object generator works in a fashion that combines both of the previous generators. It creates The Namespace Header as before, it creates the Property accessors, much like the Interface generator, except this time it implements the actual accessor not just the signature and it implements the corresponding public methods listed in the table above. It implements constructors which as well. Additionally, the Application Object performs a rules check before allowing the object to be persisted. The CheckRules method is generated as follows:
private static string CreateCheckRules(string input, IEnumerable<CodeGeneratorProperty> PropertyList)
{
StringBuilder retVal = new StringBuilder();
retVal.Append(
string.Format("{0}/// <summary>{1}", CodeGenerationHelper.GetTabIndent(2),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}/// All business rules for this class are checked in this method{1}",
CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}/// </summary>{1}", CodeGenerationHelper.GetTabIndent(2),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append(
string.Format("{0}public void CheckRules(){1}", CodeGenerationHelper.GetTabIndent(2),
CodeGenerationHelper.GetEndOfLine()));
retVal.Append( CodeGenerationHelper.GetStandAloneOpeningBracket(2));
// Walk through property list and create rules
retVal.Append(CommonHelper.GetRulesForRequiredProperties(3, PropertyList));
retVal.Append(CodeGenerationHelper.GetStandAloneClosingBracket(2));
return (input + retVal);
}
As one can see, the work for this method is mostly performed in the GetRuleForRequiredProperties helper method:
public static string GetRulesForRequiredProperties(int TabIndent, IEnumerable<CodeGeneratorProperty> PropertyList)
{
string retVal = string.Empty;
foreach (CodeGeneratorProperty property in PropertyList)
{
if (property.IsRequired)
{
switch (property.DataType)
{
case "int":
retVal += GetRequiredInteger(TabIndent, property);
break;
case "Boolean":
retVal += "**** NOT YET IMPLEMENTED IN METHOD GetRulesForRequiredProperties() **** \n";
break;
case "string":
retVal += GetRequiredString(TabIndent, property);
break;
case "byte[]":
retVal += GetRequiredByteArray(TabIndent, property);
break;
case "Guid":
retVal += GetRequiredGuid(TabIndent, property);
break;
case "DateTime":
retVal += GetRequiredDate(TabIndent, property);
break;
case "Double":
retVal += "**** NOT YET IMPLEMENTED IN METHOD GetRulesForRequiredProperties() **** \n";
break;
case "Decimal":
retVal += GetRequiredDecimal(TabIndent, property);
break;
default:
retVal += "**** NOT YET IMPLEMENTED IN METHOD GetRulesForRequiredProperties() **** \n";
break;
}
}
}
return retVal;
}
This in turn uses other helper methods, such as GetRequiredString:
private static string GetRequiredString(int TabIndent, CodeGeneratorProperty property)
{
StringBuilder retVal = new StringBuilder();
retVal.Append(
string.Format("{0}if(String.IsNullOrEmpty(_{1})){2}", CodeGenerationHelper.GetTabIndent(TabIndent),
property.PropertyName,
CodeGenerationHelper.GetEndOfLine()));
retVal.Append( CodeGenerationHelper.GetStandAloneOpeningBracket(TabIndent);
retVal.Append(
string.Format("{0}BrokenRules.Add(\"{1} is a required field and cannot be null or empty.\");{2}",
CodeGenerationHelper.GetTabIndent(TabIndent + 1), property.PropertyName,
CodeGenerationHelper.GetEndOfLine()));
retVal.Append( CodeGenerationHelper.GetStandAloneClosingBracket(TabIndent));
// Greater than max length
retVal.Append(
string.Format("{0}if(_{1}.Length > {2}){3}", CodeGenerationHelper.GetTabIndent(TabIndent),
property.PropertyName, property.Size,
CodeGenerationHelper.GetEndOfLine()));
retVal.Append( CodeGenerationHelper.GetStandAloneOpeningBracket(TabIndent));
retVal.Append(
string.Format("{0}BrokenRules.Add(\"{1} has exceeded its maximum length of {2} characters.\");{3}",
CodeGenerationHelper.GetTabIndent(TabIndent + 1), property.PropertyName, property.Size,
CodeGenerationHelper.GetEndOfLine()));
retVal.Append( CodeGenerationHelper.GetStandAloneClosingBracket(TabIndent));
return retVal.ToString();
}
As you can see not every DataType is handled, again, this being a personal code generator it can evolve as needed, instead of having to cover every scenario.
That wraps it up for this installation. We now have the ability to generate a table, its related stored procedures, a related interface, a related data access class and an application object class that exposes an API for any user. In the next and final installation of this posting we'll look at pulling it all together and writing a little "calorie counter" application to see how it works.