How I Use SubSonic, Part 2

In Part 1, I covered building a ControllerBase class to use with SubSonic 2.1’s new RepositoryRecord entity base class. In order to get the hybrid API (RepostoryRecord + Foreign Key lazy-loaded properties) I like, I had to make some changes to the code generation templates. Here’s what I do.

First off, go into Program Files\SubSonic 2.1 Final\src\CodeGeneration\Templates and copy the CS_ templates to a new folder. I put them in the root of my project so that I can keep them in under version control. Then, in your app.config or web.config, add the following to make sure that future generations will use the modified copy of the templates:

   1:  <SubSonicService defaultProvider="NorthwindProvider"
   2:                   templateDirectory="C:\Full_Path_To\SubSonicTemplates">
   3:      <providers>
   4:          <clear/>
   5:          <add ... />
   6:      </providers>
   7:  </SubSonicService>

Another thing you should know before we continue is an important naming convention I make sure to use for foreign key fields. My foreign key fields ALWAYS end in ‘ID’. That’s pretty standard. But when I have a table that has multiple foreign key fields that relate to the same table, I make sure the field name represents what the relation is, even if the field name doesn’t match the foreign table name. For example, my goal with this is to be able to do this:

   1:  // News.Author is a User object
   2:  string authorName = News.Author.DisplayName;
   3:  // News.Editor is also a User object
   4:  string editorName = News.Editor.DisplayName;

The default SubSonic templates don’t handle this situation very well. The first FK it encounters is named News.User and the second gets a funky name I don’t even recall. It can be very confusing to use. Since I always make sure my columns are named a specific way, I decided that I wanted my property names to just lop off the ‘ID’ suffix and use whatever was left. Since a table can’t have two columns with the same name, I never have naming conflicts. Updating the CS_ClassTemplate.aspx template to handle this is straightforward:

   1:  // replace this section
   2:  string fkClass = fk.ClassName;
   3:  string fkClassQualified = provider.GeneratedNamespace + "." + fkClass;
   4:  string fkMethod = fk.ClassName;
   5:  string fkID = tbl.GetColumn(fk.ColumnName).PropertyName;
   6:  string fkColumnID = fk.ColumnName;
   7:   
   8:  // with this section
   9:  string fkClass = fk.ClassName;
  10:  string fkClassQualified = provider.GeneratedNamespace + "." + fkClass;
  11:  string fkID = tbl.GetColumn(fk.ColumnName).PropertyName;
  12:  string fkMethod = fkID.Substring(0, fkID.Length - 2);
  13:  string fkColumnID = fk.ColumnName;

There’s a few code blocks that immediately follow this to check for relations to itself and duplicate method/property names. I remove those checks because my naming convention never allow for those conflicts to exist.

Because I’m using the RepositoryRecord base class, when these FK properties are called, I need to make sure they call the .Get method on the correct controller. Again, SubSonic makes this easy.

Replace this section:

   1:  /// <summary>
   2:  /// Returns a <%=fkClass%> ActiveRecord object related to this <%=className%>
   3:  /// 
   4:  /// </summary>
   5:  public <%=fkClassQualified%> <%=fkMethod%>
   6:  {
   7:      get { return <%=fkClassQualified%>.FetchByID(this.<%=fkID%>); }
   8:      set { SetColumnValue("<%=fkColumnID%>", value.<%=fkTbl.PrimaryKey.PropertyName%>); }
   9:  }

With this:

   1:  /// <summary>
   2:  /// Returns a <%=fkClass%> ActiveRecord object related to this <%=className%>
   3:  /// 
   4:  /// </summary>
   5:  protected <%=fkClassQualified%> m_<%=fkMethod%> = null;
   6:  public <%=fkClassQualified%> <%=fkMethod%>
   7:  {
   8:  <% if (baseClass != "ActiveRecord") { %>
   9:      get { 
  10:          if (m_<%=fkMethod%> == null)
  11:               m_<%=fkMethod%> = <%=fkClassQualified%>Controller.Get(this.<%=fkID%>); 
  12:          return m_<%=fkMethod%>;
  13:      }
  14:  <% } else { %>
  15:      get { return <%=fkClassQualified%>.FetchByID(this.<%=fkID%>); }
  16:  <% } %>
  17:      set { SetColumnValue("<%=fkColumnID%>", value.<%=fkTbl.PrimaryKey.PropertyName%>); }
  18:  }

Not only are we now using the Controller to retrieve the item, we’re also storing it in a member variable so every time we call that property for this instance, we only hit the database once.

The last change you need to make to CS_ClassTemplate.aspx is to remove the checks for ActiveRecord so that foreign key properties are still generated even if you select RepositoryRecord. Download the files at the end of the post to see this in action.

There’s also a small change that needs to be added to CS_StructsTemplate.aspx to support my ControllerBase.Delete(int id) customization. Add this between the existing Delete and Destroy methods:

   1:  public static void Delete<T>(int keyId) where T : RepositoryRecord<T>, new() 
   2:  {
   3:      Repository.Delete<T>(keyId);
   4:  }
   5:  public static void Destroy<T>(int keyId) where T : RepositoryRecord<T>, new() 
   6:  {
   7:      Repository.Destroy<T>(keyId);
   8:  }

If you use GUIDs or something else for your key, you’ll have to adjust accordingly (HOWEVER, I am working on committing some new methods to the SubSonic core to allow deleting by key for all data types…stay tuned).

To use these customizations in your own projects, download my templates here.

Coming soon is Part 3 which will cover using the ControllerBase to easily add request-length (or longer) caching and query performance auditing. If you have any comments or feedback, I’d love to hear it below!

Posted October 26th, 2008 5:46 PM
Read more posts about .NET, SubSonic, Tips.

View Comments
Link

  • Hamed
    Thanks for your nice template

    I was generating code using your proposed template for Repository Pattern against Oracle database 10.

    The problem that I faced was that when you use tbl.ForeighnKeys to get the list of the foreign tables, it was not not working. This was due to a problem in GetTableSchema in OracleDataProvider. To be more specific, in the implementation of GetTableSchema for OracleDataProvider, when columns are retrieved, ForeignKeys property is not initialized and foreignkey tables are not added into it. Thus, when code generation is being done, it cannot find foreignkey tables to make the properties for them in the generated code. I suggest modify the GetTableSchema to the following one:

    public override TableSchema.Table GetTableSchema(string tableName, TableType tableType)
    {
    TableSchema.TableColumnCollection columns = new TableSchema.TableColumnCollection();
    TableSchema.Table tbl = new TableSchema.Table(tableName, tableType, this);
    //tbl.ClassName = Convention.ClassName(tableName);
    //string sql = TABLE_COLUMN_SQL;
    QueryCommand cmd = new QueryCommand(TABLE_COLUMN_SQL, Name);
    cmd.AddParameter(TABLE_NAME_PARAMETER, tableName, DbType.AnsiString);
    TableSchema.TableColumn column;

    using(IDataReader rdr = DataService.GetReader(cmd))
    {
    //get information about both the table and it's columns
    while(rdr.Read())
    {
    tbl.SchemaName = rdr["USER"].ToString();

    column = new TableSchema.TableColumn(tbl);
    column.ColumnName = rdr[OracleSchemaVariable.COLUMN_NAME].ToString();

    string scale = rdr[OracleSchemaVariable.NUMBER_SCALE].ToString();
    string precision = rdr[OracleSchemaVariable.NUMBER_PRECISION].ToString();

    column.NumberScale = 0;
    column.NumberPrecision = 0;

    if(!String.IsNullOrEmpty(scale) && scale != "0")
    column.NumberScale = int.Parse(scale);

    if(!String.IsNullOrEmpty(precision) && precision != "0")
    column.NumberPrecision = int.Parse(precision);

    // column.DataType = GetDbType(rdr[OracleSchemaVariable.DATA_TYPE].ToString().ToLower());
    column.DataType = GetDbTypeOracle(rdr[OracleSchemaVariable.DATA_TYPE].ToString().ToLower(), column.NumberScale, column.NumberPrecision);
    column.AutoIncrement = false;
    int maxLength;
    int.TryParse(rdr[OracleSchemaVariable.MAX_LENGTH].ToString(), out maxLength);
    column.MaxLength = maxLength;
    column.IsNullable = Utility.IsMatch(rdr[OracleSchemaVariable.IS_NULLABLE].ToString(), "Y");
    column.IsReadOnly = false;
    columns.Add(column);
    }
    }

    cmd.CommandSql = INDEX_SQL;
    //cmd.AddParameter(TABLE_NAME_PARAMETER, tableName);
    tbl.ForeignKeys = new TableSchema.ForeignKeyTableCollection();

    using(IDataReader rdr = DataService.GetReader(cmd))
    {
    while(rdr.Read())
    {
    string colName = rdr[OracleSchemaVariable.COLUMN_NAME].ToString();
    string constraintType = rdr[OracleSchemaVariable.CONSTRAINT_TYPE].ToString();
    column = columns.GetColumn(colName);

    if(constraintType == SqlSchemaVariable.PRIMARY_KEY)
    column.IsPrimaryKey = true;
    else if(constraintType == SqlSchemaVariable.FOREIGN_KEY)
    column.IsForeignKey = true;

    //HACK: Allow second pass naming adjust based on whether a column is keyed
    column.ColumnName = column.ColumnName;

    //Hamed

    if (column.IsForeignKey)
    {
    TableSchema.ForeignKeyTable fkTable = new TableSchema.ForeignKeyTable(this);
    TableSchema.Table ftbl = this.GetForeignKeyTable(column, tbl);
    column.ForeignKeyTableName = ftbl.TableName;
    fkTable.ColumnName = column.ColumnName;
    fkTable.TableName = ftbl.TableName;

    //not needed for now
    //fkTable.PrimaryColumnName = drFK[i]["PK_Column"].ToString();
    //fkTable.ForeignColumnName = drFK[i]["FK_Column"].ToString();

    tbl.ForeignKeys.Add(fkTable);
    }

    }
    rdr.Close();
    }
    if(columns.Count > 0)
    {
    tbl.Columns = columns;
    return tbl;
    }
    return null;
    }
  • Hi Hamed. You should submit an issue at
    http://code.google.com/p/subsonicproject for that issue.
blog comments powered by Disqus