|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
Table of Contents
IntroductionSeeing the words "Build succeeded" is always nice, but seeing "Build succeeded" after adding thousands of lines of new code in a matter seconds is a thing of beauty. Code generation has saved me countless hours of work over the past few years, but it has also caused some headaches. I've used some templates that generate code which requires post-generation changes in order to compile. Other templates I've used generate a complete class, with "space reserved" for custom code, so you have to be careful to not overwrite the custom code when you regenerate the object. Overall, the times when I can just run a template, compile, and go have been few and far between. As a result, my #1 goal with the business layer templates for my current project was to have "one-click code generation". In other words, I wanted to run a program that regenerates all my business entities based on the current database state, doesn't affect any of my custom code, and doesn't break anything, except as the result of database schema changes. In addition, I wanted the business entities to be clean and intuitive, so my teammates could jump right in and code without needing any help from me. I ended up using a tool called MyGeneration for code generation in combination with the EntitySpaces business layer framework (and templates). This article addresses issues that aren't specific to EntitySpaces, but the templates I customized and am using in my examples are EntitySpaces templates. The EntitySpaces templates in combination with MyGeneration address the one-click requirement right out of the box. MyGeneration allows you to save settings from previous runs and input them into the template, so you can regenerate your business layer in a single click. In addition, the EntitySpaces templates create separate files for generated and custom code using partial classes, so you never have to worry about overwriting custom code when you regenerate business entities. If you're using a template that doesn't use partial classes and multiple files, you really need to find something else. Generated code does not belong in the same file with custom code--it causes unnecessary problems. With requirement #1 out of the way, I spent most of my time modifying the templates to suit personal preferences and feature needs. I wanted to generate enums for certain tables, a way to organize generated files into sub-folders, so they would be easier to browse in Visual Studio, and a way to change the names of certain foreign reference properties, but foremost on my list was a way to name classes something other than the tables names. I prefer plural table names ( SQL Extended Properties PrimerThe easiest way to manage extended properties in your database is just to right-click on a database, table, or column in Management Studio's object explorer, select "Properties", then select "Extended Properties" within the Properties dialog. This interface allows you to easily add and update extended property values on individual objects. If you want to manipulate property values programmatically, you can access them via several system stored procedures and functions: sp_addextendedproperty
@name, @value,
@level0type, @level0name,
@level1type, @level1name,
@level2type, @level2name
sp_updateextendedproperty
@name, @value,
@level0type, @level0name,
@level1type, @level1name,
@level2type, @level2name
sp_dropextendedproperty
@name,
@level0type, @level0name,
@level1type, @level1name,
@level2type, @level2name
fn_listextendedproperty
@name,
@level0type, @level0name,
@level1type, @level1name,
@level2type, @level2name
To add an extended property to a table, use the following parameters: exec sp_addextendedproperty
'[PropertyName]',
'[PropertyValue]',
'user',
'dbo',
'table',
'[TableName]',
null,
null
To add an extended property to a column, use the following parameters: exec sp_addextendedproperty
'[PropertyName]',
'[PropertyValue]',
'user',
'dbo',
'table',
'[TableName]',
'column',
'[ColumnName]'
-- Get all table-level extended properties in the database
Select * From
fn_listextendedproperty (null, 'user', 'dbo', 'table', null, null, null)
-- Get all column-level extended properties for the Employees table
Select * From
fn_listextendedproperty (null, 'user', 'dbo', 'table', 'employees', 'column', null)
-- Get the value of the "Biz.Class" property for every table in the database
Select objname, value From
fn_listextendedproperty ('Biz.Class', 'user', 'dbo', 'table', null, null, null)
-- Get all extended properties defined on the database itself
Select * From
fn_listextendedproperty (null, null, null, null, null, null, null)
The update procedure works exactly the same as add. The only difference with the syntax of the drop procedure is that it doesn't seem to allow nulls for the property, table, or column name, so you can only delete one property at a time. Apparently Microsoft wants you to be very specific about what to drop. Putting it all TogetherSo now that you know how to use extended properties, it's time to put it all together. The next step is to modify your code generation templates to run the queries above against your database, retrieve the property values, and generate code based on those values. You could really go any number of directions with the properties you choose to implement, based on the needs of your business layer. I decided to stick to the following properties. You can find the exact values for each table in the attached NorthwindExtendedProperties.csv file.
Modifying the templates themselves was fairly straightforward. I created a method to load the extended properties for a given table or view and cache them, then return them as a System.Collections.Generic.Dictionary<object,KeyValueCollection> props =
new System.Collections.Generic.Dictionary<object,KeyValueCollection>();
private KeyValueCollection GetExtendedProperties(ITable tbl,IView vw)
{
object key = ((object)tbl) ?? ((object)vw);
if( !props.ContainsKey(key) )
{
MyMeta.Sql.DatabaseSpecific dbs =
new MyMeta.Sql.DatabaseSpecific();
KeyValueCollection kvc;
if( tbl != null )
kvc = dbs.ExtendedProperties(tbl);
else
kvc = dbs.ExtendedProperties(vw);
props.Add(key,kvc);
}
return props[key];
}
private string EntityName(ITable tbl, IView vw)
{
KeyValueCollection kvc = GetExtendedProperties(tbl,vw);
foreach(KeyValuePair kvp in kvc)
if(kvp.Key == "Biz.Class")
return kvp.Value;
return "MissingEntityName";
}
private string EntityPath(ITable tbl,IView vw)
{
string path = "";
KeyValueCollection kvc = GetExtendedProperties(tbl,vw);
foreach(KeyValuePair kvp in kvc)
if(kvp.Key == "Biz.Path")
path = kvp.Value;
return path.Replace(".","");
}
Below are a few examples of how I modified the templates to use extended property values in the generated code. public partial class <%=EntityName(table,view)%> : <%=esEntityName(table,view)%>
// was formerly
public partial class <%=esPlugIn.Entity(source)%> : <%=esPlugIn.esEntity(source)%>
string objName = EntityName(tr.ForeignTable, null) + "By" + tr.PrimaryColumns[0].Name;
// was formerly
string objName = esPlugIn.EntityRelationName(tr.ForeignTable, tr.PrimaryColumns[0], tr.IsSelfReference);
// Modification to write business entity files to sub-folders based on Biz.Path setting
string filename = input["txtPath"].ToString();
if (!filename.EndsWith("\\") )
filename += "\\";
if(!String.IsNullOrEmpty(EntityPath(table,view)))
filename += EntityPath(table,view);
I tweaked a few other minor things in the EntitySpaces templates to suit my purposes, such as removing the "UpTo" prefix from foreign key properties that reference parent rows, but I don't actually recommend doing that if you are an EntitySpaces user. While the "UpTo" prefix can make intellisense/auto-complete a little less helpful (typing "OrdersBy", doesn't find the Building Enums From TablesWhen required to represent a data type that's made up of a fixed set of options such as the private System.Collections.Generic.List<string> EnumValues(ITable tbl)
{
if( tbl == null )
return null;
System.Collections.Generic.List<string> values =
new System.Collections.Generic.List<string>();
string nameColumn = EnumNameColumn(tbl);
string valueColumn = EnumValueColumn(tbl);
if( nameColumn == null || valueColumn == null )
return null;
string strSQL = string.Format(
"SELECT {0}, {1} FROM {2} ORDER BY {1}",
nameColumn, valueColumn, tbl.Name);
IDatabase database = MyMeta.Databases[databaseName];
ADODB.Recordset rs = database.ExecuteSql(strSQL);
if( rs != null && rs.RecordCount > 0 )
{
while( !rs.EOF )
{
values.Add(string.Format("{0} = {1}",
System.Text.RegularExpressions.Regex.Replace(
rs.Fields[0].Value.ToString().Trim(),
"[^A-Za-z0-9]", "_"),
rs.Fields[1].Value.ToString()));
rs.MoveNext();
}
rs.Close();
rs = null;
}
return values;
}
<%if (EntityEnum(table) != null) {%>
/// <summary>
/// Value enumeration for <%=table.Name%> table.
/// </summary>
public enum <%=EntityEnum(table)%>
{<%
System.Collections.Generic.List<string> enumValues = EnumValues(table);
for(int index = 0; index < enumValues.Count - 1; index++ ) {%>
<%=enumValues[index]%>,<%}%>
<%=enumValues[enumValues.Count - 1]%>
};
<%}%>
The code template snippet above generated the following enum for the Northwind ///
Improving NCover Coverage AnalysisOn an unrelated note to database-driven code templates, I thought I'd point out another helpful feature in my modified code templates. My team has a performance metric based on code coverage that requires us to cover a certain percentage of our custom business layer code. With so much generated code, it was very difficult to determine the actual numbers for coverage of custom code, because the generated was also being measured. As a result, I added Room for ImprovementOne thing I would improve about my modified templates if I had time would be to add extended property support for columns. I considered it early on, but never had a problem with the column names and types the EntitySpaces templates generated, so I didn't change anything. The framework is there to make it easy to get extended property values from columns. It would just take a while to go through all the template files and replace all instances of I hope you enjoyed this brief look building dynamic code templates using extended properties. If you have any difficulties implementing similar functionality using MyGeneration, EntitySpaces, or another similar technology, I'd be happy to assist in any way that I can. About the Source FilesI didn't include a client with the source code because it wouldn't really illustrate anything relevent to this article. You'll need a recent version of EntitySpaces in order to compile the business layer, but compilation isn't necessary; the code is mostly just there to show the results of the code generation. The most useful pieces are the modified MyGeneration templates in the Templates folder, and the Northwind extended property values in NorthwindExtendedProperties.csv. Revision History2008.01.03 - Initial Version Licence notesThe code related to EntitySpaces may contain usage terms in the download files themselves. All other code in this article is covered by the license below.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||