T4 Tutorial: Handling Errors in Code Generators


This post is a part of the series that introduces code generation with Text Templates (also known as T4 Templates) in Visual Studio using C# and Visual Basic; explains how to create reusable templates and combine them in complex code generators. In order to follow examples in this article, you need to have Visual Studio 2008 Standard Edition or higher, SQL Server 2005 or later, T4 Toolbox and T4 Editor installed on your computer.

Introduction

Error conditions exist in code generators just like in all other kinds of software. And just like with any other type of software, it is important to handle error conditions appropriately. One one hand, a code generator should handle the expected error conditions and display information that will help its user to identify and correct them. On the other hand, the code generator should not handle the unexpected error conditions in a way that would make troubleshooting them more difficult for its author. Complete discussion of error handling strategy is outside of scope of this article. More information on this subject is available in How to Design Exception Hierarchies by Krzysztof Cwalina and Fail Fast by Jim Shore.

This article reviews examples of error conditions in the code generator created earlier in this series and discusses how to handle them. As you remember, this generator (shown below) uses several templates that retrieve table schema information from a given SQL Server database using SMO and generate DELETE, INSERT and UPDATE stored procedures for each table.

C#
<#@ assembly name=”Microsoft.SqlServer.ConnectionInfo” #>
<#@ assembly name=”Microsoft.SqlServer.Smo” #>
<#@ import namespace=”Microsoft.SqlServer.Management.Smo” #>
<#@ include file=”DeleteProcedureTemplate.tt” #>
<#+
public class CrudProcedureGenerator : Generator
{
  public string DatabaseName;
  public DeleteProcedureTemplate DeleteTemplate = new DeleteProcedureTemplate();  

  protected override void RunCore()
  {
    Server server = new Server();
    Database database = new Database(server, this.DatabaseName);
    database.Refresh();
    foreach (Table table in database.Tables)
    {
      this.DeleteTemplate.DatabaseName = this.DatabaseName;
      this.DeleteTemplate.TableName = table.Name;
      this.DeleteTemplate.RenderToFile(table.Name + “_Delete.sql”);
    }
  }
}
#>
Visual Basic
<#@ assembly name=”Microsoft.SqlServer.ConnectionInfo” #>
<#@ assembly name=”Microsoft.SqlServer.Smo” #>
<#@ import namespace=”Microsoft.SqlServer.Management.Smo” #>
<#@ include file=”DeleteProcedureTemplate.tt” #>
<#+
Public Class CrudProcedureGenerator
  Inherits Generator 

  Public DatabaseName As String
  Public DeleteTemplate As DeleteProcedureTemplate = New DeleteProcedureTemplate() 

  Protected Overrides Sub RunCore()
    Dim server As Server = New Server()
    Dim database As Database = New Database(server, Me.DatabaseName)
    database.Refresh()
    For Each table As Table In database.Tables
      Me.DeleteTemplate.DatabaseName = Me.DatabaseName
      Me.DeleteTemplate.TableName = table.Name
      Me.DeleteTemplate.RenderToFile(table.Name & “_Delete.sql”)
    Next
  End Sub
End Class
#>

Below is an example of using this code generator to produce stored procedures for the Northwind database on the local computer. Complete source code of this example is available for download at the bottom of this article.

C#
<#@ template language=”C#v3.5hostspecific=”Truedebug=”True” #>
<#@ output extension=”txt” #>
<#@ include file=”T4Toolbox.tt” #>
<#@ include file=”CrudProcedureGenerator.tt” #>
<#
    CrudProcedureGenerator generator = new CrudProcedureGenerator();
    generator.DatabaseName = “Northwind”;
    generator.Run();
#>

Visual Basic
<#@ template language=”VBv3.5hostspecific=”Truedebug=”True” #>
<#@ output extension=”txt” #>
<#@ include file=”T4Toolbox.tt” #>
<#@ include file=”CrudProcedureGenerator.tt” #>
<#
    Dim generator As CrudProcedureGenerator = New CrudProcedureGenerator()
    generator.DatabaseName = “Northwind”
    generator.Run()
#>

Usage Errors

A typical error in using a complex code generator is forgetting to specify a required parameter, such as DatabaseName in our example. We can simulate this error by commenting out the line that assigns “Northwind” to generator.DatabaseName in NorthwindStoredProcedures.tt and saving the file which causes Visual Studio to run the code generator and display the following error.

image

As you can see, this error provides complete details about the exception that caused it and will help the author of the code generator to identify the problem quickly. Unfortunately, it does not help the user of the code generator, because it is too verbose and low-level. We can make it a lot more specific and helpful by adding the following code to CrudProcedureGenerator.tt.

C#
<#@ … #>
<#+
public class CrudProcedureGenerator : Generator
{
    // …
    protected override void Validate()
    {
        if (string.IsNullOrEmpty(this.DatabaseName))
        {
            this.Error(
                “DatabaseName property of CrudProcedureGenerator ” +
                “must be assigned”);
        }
    }
}
#>

Visual Basic
<#@ … #>
<#+
Public Class CrudProcedureGenerator
    Inherits Generator 

    ‘ …
    Protected Overrides Sub Validate()
        If String.IsNullOrEmpty(Me.DatabaseName) Then
            Me.Error( _
                “DatabaseName property of CrudProcedureGenerator ” & _
                “must be assigned”)
        End If
    End Sub
End Class
#>

Validate is a protected virtual method defined in the base class - Generator. When overridden in a concrete descendant class, such as CrudProcedureGenerator, this method can use Error and Warning methods of the Generator class to report errors and warnings. The Run method, also defined in the Generator class, will call the Validate method first. If Validate doesn’t report any errors, Run will call RunCore which CrudProcedureGenerator overrides to implement the actual code generation logic. And finally, Run will display any errors or warnings reported during code generation in the Error List window of Visual Studio. With added error-handling code, the following error message is displayed when we try to run the code generator without specifying DatabaseName.

image

Template class of T4 Toolbox follows the same pattern to support error handling. This class also defines virtual Validate method as well as Error and Warning methods which can be used by concrete descendants to report errors and warnings.

Environment Errors

Code generators typically access external resources to obtain metadata for code generation. File system and database are perhaps the most common sources of metadata. In our example, we are accessing a database running on the local computer. While NorthwindStoredProcedures.tt may work perfectly for the developer who originally created it, another developer on the same team, might have SQL Server configured differently, perhaps using a named instance, which can prevent the same code generator from running on her computer. We can simulate this error by stopping the SQL Server service (using the “net stop mssqlserver” in Windows command prompt) and re-running the code generator (by selecting “Run Custom Tool” from the context menu of NorthwindStoredProcedures.tt in Solution Explorer of Visual Studio).

image

Similar to the usage error we discussed earlier, this error is very verbose and low-level. We can make it more precise and useful for the users of the code generator by wrapping the original FailedOperationException, thrown by SMO in a TransformationException defined by the T4 Toolbox.

C#
<#@ … #>
<#+
public class CrudProcedureGenerator : Generator
{
   // …
   protected override void RunCore()
   {
      try
      {
         Server server = new Server();
         Database database = new Database(server, this.DatabaseName);
         database.Refresh();
         // …
      }
      catch (FailedOperationException e)
      {
         throw new TransformationException(
            string.Format(
               “Unable to access {0} database on local SQL Server instance”,
               this.DatabaseName),
            e);
      }
   }
   // …
}
#>

Visual Basic
<#@ template language=”VBv3.5” #>
<#@ … #>
<#+
Public Class CrudProcedureGenerator
   Inherits Generator 

   ‘ …
   Protected Overrides Sub RunCore()
      Try
         Dim server As Server = New Server()
         Dim database As Database = New Database(server, Me.DatabaseName)
         database.Refresh()
         ‘ …
      Catch e As FailedOperationException
         Throw New TransformationException( _
            String.Format( _
               “Unable to access {0} database on local SQL Server instance”, _
               Me.DatabaseName), _
            e)
      End Try
   End Sub
   ‘ …
End Class
#>

Run method of the Generator class will catch TransformationException and displays its Message in the Error List of Visual Studio.

image

In other words, instances of TransformationException class are considered to be expected exceptions, thrown by the code generator itself. Only error message is displayed for expected exceptions. All other exception types are treated as unexpected and will be displayed with complete stack trace.

Render method of the Template class also catches TransformationException instances and reports them as expected errors.

Unexpected Errors

Unexpected errors are, well, unexpected. The author of the code generator does not expect them to occur. When user of  the code generator encounters such an error, having complete details about it will help to troubleshoot and resolve the problem quickly. Allowing unexpected exceptions to simply bubble up unhandled will display their complete stack trace in the Error List of Visual Studio. On the other hand, trying to make the code generator more “robust” and “user-friendly” by catching all exceptions hides the exception stack trace and makes it more difficult to troubleshoot. It is best to catch only those exceptions that indicate error conditions you expect and can make specific recommendations to the user on how to resolve them.

Conclusion

By default, Visual Studio displays complete details for unhandled exceptions that occur during code generation in the Error List window. This makes errors easier to troubleshoot for authors of code generators. Appropriate error handling helps to make code generators easier for the users to use. T4 Toolbox defines TransformationException, which code generators can throw with a user-readable messages to indicate expected error conditions. Template and Generator classes catch exceptions of this type and display only the exception message in the Error List of Visual Studio. Both Template and Generator classes provide virtual Validate methods that descendants can override to implement validation of code generation parameters. Error and Warning methods are available in both base classes to report multiple errors and warnings.

Error handling becomes increasingly important when a code generator gets adopted by multiple teams. It can make a difference between a good code generator and an unusable one. Error-handling code may also be the most challenging to test as it requires simulation of error conditions. This task is best accomplished with automated unit tests which is the topic of the next article in this series.

Download


Write a Comment

Take a moment to comment and tell us what you think. Some basic HTML is allowed for formatting.

Reader Comments

Updated source code to be compatible with version 9.10 of the T4 Toolbox.

Any support for generate code for ODP.NET - Oracle ? Thanks.