SourceMason generated CRUD stored procedures

Introduction

This article will demonstrate the creation of basic CRUD stored procedures using the SourceMason code generation tool. Using a SourceMason code template I will generate a Get, Insert, Update and Delete stored procedure for a selected table. I will also create a template which will call the stored procedure template for each table in a database with an option to run each of the generated scripts.

Sample Image - maximum width is 600 pixels

Installing software

Before you can run the code generation template you need to download the SourceMason Free Edition from www.sourcemason.com. The Free Edition includes the SourceMason Runner application which uses the same template engine as SourceMason Studio (included in Professional Edition).

SchemaExplorer

SchemaExplorer is an addon for SourceMason (included in the install) that allows you to read the schema for any type of database. In this example I will be using Sql Server 2005 and the AdventureWorks sample database.

Below is the SchemaExplorer interface and table classes:

Using the CrudSpSqlServer.smt template

This template generates the stored procedures for a single table.

The following template directives are used:
<%@ CodeTemplate Language="C#" TargetLanguage="Sql" Description="CRUD Stored Procedures." %>
<%@ Property Name="SourceTable" Type="EquatorIT.SchemaExplorer.TableSchema" Description="Table that should be read." %>

The first directive specifies the language for any code in the template.
The second directive specifies a property of type EquatorIT.SchemaExplorer.TableSchema which will provide the table whose schema is to be read.

The following is a sample of the helper functions used in the template:
<script runat="template">
private string CreateKeyParam(int index)
{
  TableColumnSchema primaryKeyColumn = SourceTable.PrimaryKey.Columns[index];
	
  string result = string.Format("@{0}", primaryKeyColumn.Name);
  result += " " + GetSQLTypeForColumn(primaryKeyColumn);

  if (index < SourceTable.PrimaryKey.Columns.Count-1)
    result += ",";

  return result;
}

private string CreateWhereForKey(int index)
{
  string result = string.Format("[{0}] = @{0}", SourceTable.PrimaryKey.Columns[index].Name);
	
  if (index > 0)
    result = "and " + result;
		
  return result;
}

private string CreateColumn(int columnIndex)
{
  string result = string.Format("[{0}]", SourceTable.Columns[columnIndex].Name);
	
  if (columnIndex < SourceTable.Columns.Count-1)
    result += ",";
		
  return result;
}

private bool IsIdentityColumn(TableColumnSchema column)
{
  if ((column.ExtendedProperties[ExtendedPropertyColumnType.IsIdentity.ToString()].Value != null) && 
    ((bool)column.ExtendedProperties[ExtendedPropertyColumnType.IsIdentity.ToString()].Value))
    return true;
  else
    return false;
}

private string CreateSetIndentityColumn()
{
  foreach(TableColumnSchema column in SourceTable.PrimaryKey.Columns)
  {
    if (IsIdentityColumn(column))
    {
      return string.Format("SET @{0} = SCOPE_IDENTITY()", column.Name);	
    }
  }

  return string.Empty;
}

private string GetSQLTypeForColumn(TableColumnSchema column)
{
  string dbFieldType = column.DbType;
	
  if ((column.Type == typeof(System.String)) || (column.Type == typeof(System.Char)))
  {
    if (column.MaxLength > 0)
      dbFieldType += string.Format("({0})", column.MaxLength);
  }
	
  return dbFieldType.ToLower();
}
</script>

The following code/markup from the template body generates a Get stored procedure.

I use the SourceTable object to read all primary key columns for the table. The helper function CreateKeyParam does the formatting of each primary key column to create input parameters for the stored procedure.

<% GenerateDocumentHeader();%>
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go

-- Get Stored Procedure
-- Drop Existing Procedure
IF OBJECT_ID(N'[<%= SourceTable.Owner%>].[usp<%= SourceTable.Name%>_Get]') IS NOT NULL
  DROP PROCEDURE [<%= SourceTable.Owner%>].[usp<%= SourceTable.Name%>_Get]
go
 
CREATE PROCEDURE [<%= SourceTable.Owner%>].[usp<%= SourceTable.Name%>_Get]
<%for (int i = 0; i < SourceTable.PrimaryKey.Columns.Count; i++) %>
<%{ %>
  <%= CreateKeyParam(i)%>	
<%} %>		
AS

SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ COMMITTED

SELECT
<%for (int i = 0; i < SourceTable.Columns.Count; i++) %>
<%{ %> 
  <%= CreateColumn(i)%>
<%} %>	
FROM
  [<%= SourceTable.Owner%>].[<%= SourceTable.Name%>]
WHERE
<%for (int i = 0; i < SourceTable.PrimaryKey.Columns.Count; i++) %>
<%{ %>
  <%= CreateWhereForKey(i)%>	
<%} %>
go

Example Output

This is the output for the Sales.SalesOrderHeader table.
------------------------------------------------------------------------------------------------------------------------
-- Generated By:   Brett using SourceMason 1.0.4.0
-- Template:       CrudSpSqlServer.smt
-- Date Generated: Friday, 11 May 2007
------------------------------------------------------------------------------------------------------------------------

set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go

-- Get Stored Procedure
-- Drop Existing Procedure
IF OBJECT_ID(N'[Sales].[uspSalesOrderHeader_Get]') IS NOT NULL
	DROP PROCEDURE [Sales].[uspSalesOrderHeader_Get]
go

CREATE PROCEDURE [Sales].[uspSalesOrderHeader_Get]
	@SalesOrderID int
AS

SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ COMMITTED

SELECT
	[SalesOrderID],
	[RevisionNumber],
	[OrderDate],
	[DueDate],
	[ShipDate],
	[Status],
	[OnlineOrderFlag],
	[SalesOrderNumber],
	[PurchaseOrderNumber],
	[AccountNumber],
	[CustomerID],
	[ContactID],
	[SalesPersonID],
	[TerritoryID],
	[BillToAddressID],
	[ShipToAddressID],
	[ShipMethodID],
	[CreditCardID],
	[CreditCardApprovalCode],
	[CurrencyRateID],
	[SubTotal],
	[TaxAmt],
	[Freight],
	[TotalDue],
	[Comment],
	[rowguid],
	[ModifiedDate]
FROM
	[Sales].[SalesOrderHeader]
WHERE
	[SalesOrderID] = @SalesOrderID
go

--Update Stored Procedure
-- Drop Existing Procedure
IF OBJECT_ID(N'[Sales].[uspSalesOrderHeader_Update]') IS NOT NULL
	DROP PROCEDURE [Sales].[uspSalesOrderHeader_Update]
go

CREATE PROCEDURE [Sales].[uspSalesOrderHeader_Update]
	@SalesOrderID int,
	@RevisionNumber tinyint,
	@OrderDate datetime,
	@DueDate datetime,
	@ShipDate datetime,
	@Status tinyint,
	@OnlineOrderFlag bit,
	@SalesOrderNumber nvarchar(25) output,
	@PurchaseOrderNumber nvarchar(25),
	@AccountNumber nvarchar(15),
	@CustomerID int,
	@ContactID int,
	@SalesPersonID int,
	@TerritoryID int,
	@BillToAddressID int,
	@ShipToAddressID int,
	@ShipMethodID int,
	@CreditCardID int,
	@CreditCardApprovalCode varchar(15),
	@CurrencyRateID int,
	@SubTotal money,
	@TaxAmt money,
	@Freight money,
	@TotalDue money output,
	@Comment nvarchar(128),
	@rowguid uniqueidentifier,
	@ModifiedDate datetime
AS

SET NOCOUNT OFF

UPDATE [Sales].[SalesOrderHeader] SET
	[RevisionNumber] = @RevisionNumber,
	[OrderDate] = @OrderDate,
	[DueDate] = @DueDate,
	[ShipDate] = @ShipDate,
	[Status] = @Status,
	[OnlineOrderFlag] = @OnlineOrderFlag,
	[PurchaseOrderNumber] = @PurchaseOrderNumber,
	[AccountNumber] = @AccountNumber,
	[CustomerID] = @CustomerID,
	[ContactID] = @ContactID,
	[SalesPersonID] = @SalesPersonID,
	[TerritoryID] = @TerritoryID,
	[BillToAddressID] = @BillToAddressID,
	[ShipToAddressID] = @ShipToAddressID,
	[ShipMethodID] = @ShipMethodID,
	[CreditCardID] = @CreditCardID,
	[CreditCardApprovalCode] = @CreditCardApprovalCode,
	[CurrencyRateID] = @CurrencyRateID,
	[SubTotal] = @SubTotal,
	[TaxAmt] = @TaxAmt,
	[Freight] = @Freight,
	[Comment] = @Comment,
	[rowguid] = @rowguid,
	[ModifiedDate] = @ModifiedDate
WHERE
	[SalesOrderID] = @SalesOrderID

-- Return computed columns
SELECT
	@SalesOrderNumber = [SalesOrderNumber],
	@TotalDue = [TotalDue]
FROM
	[Sales].[SalesOrderHeader]
WHERE
	[SalesOrderID] = @SalesOrderID

go

-- Insert Stored Procedure
-- Drop Existing Procedure
IF OBJECT_ID(N'[Sales].[uspSalesOrderHeader_Insert]') IS NOT NULL
	DROP PROCEDURE [Sales].[uspSalesOrderHeader_Insert]
go

CREATE PROCEDURE [Sales].[uspSalesOrderHeader_Insert]
	@SalesOrderID int output,
	@RevisionNumber tinyint,
	@OrderDate datetime,
	@DueDate datetime,
	@ShipDate datetime,
	@Status tinyint,
	@OnlineOrderFlag bit,
	@SalesOrderNumber nvarchar(25) output,
	@PurchaseOrderNumber nvarchar(25),
	@AccountNumber nvarchar(15),
	@CustomerID int,
	@ContactID int,
	@SalesPersonID int,
	@TerritoryID int,
	@BillToAddressID int,
	@ShipToAddressID int,
	@ShipMethodID int,
	@CreditCardID int,
	@CreditCardApprovalCode varchar(15),
	@CurrencyRateID int,
	@SubTotal money,
	@TaxAmt money,
	@Freight money,
	@TotalDue money output,
	@Comment nvarchar(128),
	@rowguid uniqueidentifier,
	@ModifiedDate datetime
AS

SET NOCOUNT OFF

INSERT INTO [Sales].[SalesOrderHeader] (
	[RevisionNumber],
	[OrderDate],
	[DueDate],
	[ShipDate],
	[Status],
	[OnlineOrderFlag],
	[PurchaseOrderNumber],
	[AccountNumber],
	[CustomerID],
	[ContactID],
	[SalesPersonID],
	[TerritoryID],
	[BillToAddressID],
	[ShipToAddressID],
	[ShipMethodID],
	[CreditCardID],
	[CreditCardApprovalCode],
	[CurrencyRateID],
	[SubTotal],
	[TaxAmt],
	[Freight],
	[Comment],
	[rowguid],
	[ModifiedDate]
) VALUES (
	@RevisionNumber,
	@OrderDate,
	@DueDate,
	@ShipDate,
	@Status,
	@OnlineOrderFlag,
	@PurchaseOrderNumber,
	@AccountNumber,
	@CustomerID,
	@ContactID,
	@SalesPersonID,
	@TerritoryID,
	@BillToAddressID,
	@ShipToAddressID,
	@ShipMethodID,
	@CreditCardID,
	@CreditCardApprovalCode,
	@CurrencyRateID,
	@SubTotal,
	@TaxAmt,
	@Freight,
	@Comment,
	@rowguid,
	@ModifiedDate
)

SET @SalesOrderID = SCOPE_IDENTITY()

-- Return computed columns
SELECT
	@SalesOrderNumber = [SalesOrderNumber],
	@TotalDue = [TotalDue]
FROM
	[Sales].[SalesOrderHeader]
WHERE
	[SalesOrderID] = @SalesOrderID
go

--Delete Stored Procedure
-- Drop Existing Procedure
IF OBJECT_ID(N'[Sales].[uspSalesOrderHeader_Delete]') IS NOT NULL
	DROP PROCEDURE [Sales].[uspSalesOrderHeader_Delete]
go

CREATE PROCEDURE [Sales].[uspSalesOrderHeader_Delete]
	@SalesOrderID int
AS

SET NOCOUNT OFF

DELETE FROM [Sales].[SalesOrderHeader]
WHERE
	[SalesOrderID] = @SalesOrderID

Using the CreateAllCrudSpSqlServer.smt template

This template calls the CrudSpSqlServer.smt template for each table in the selected database. It will also exclude any tables in the dbo schema.

The following template directives are used:
<%@ CodeTemplate Language="C#" TargetLanguage="" Description="Create all CRUD stored procedures." %>
<%@ Register Name="CrudSpSqlserver" Template="CrudSpSqlserver.smt" %> 
<%@ Property Name="SourceDatabase" Type="EquatorIT.SchemaExplorer.DatabaseSchema" Default="" Optional="False" Category="" Description="" Editor="" EditorBase="" Serializer="" %>
<%@ Property Name="OutputPath" Type="string" Default="" Optional="False" Category="" Description="Output Path for stored procedures" Editor="System.Windows.Forms.Design.FolderNameEditor"%>
<%@ Property Name="RunScript" Type="bool" Default="False" Optional="False" Category="" Description="Run generated script against database" %>

The second directive registers the CrudSpSqlserver template so that it can be called by this template.
The third directive specifies a property of type EquatorIT.SchemaExplorer.DatabaseSchema which will provide the database schema to be read.
The fourth directive specifies the output path for the generated stored procedures.
The fifth directive specifies whether or not the generated stored procedure scripts should be run against the selected database

The following is the script section of the template:
<script runat="template">
public override void Render(System.IO.TextWriter writer)
{	
  try
  {		
    writer.WriteLine("Generation for {0} started at {1}", SourceDatabase.Name, DateTime.Now);
		
    foreach(TableSchema table in SourceDatabase.Tables)
    {
      if (table.Owner.Equals("dbo", StringComparison.CurrentCultureIgnoreCase))
        continue;

      writer.WriteLine();
      writer.WriteLine("Generating stored procedures for {0}", table.FullName);
			
      CrudSpSqlserver crudSpSqlserver = new CrudSpSqlserver();
      crudSpSqlserver.SourceTable = table;						
      string outputPath = Path.Combine(@OutputPath, table.Name + ".sql");			
		
      using (StringWriter sw = new StringWriter())
      {
        crudSpSqlserver.Response.AddTextWriter(sw);								
        crudSpSqlserver.RenderToFile(outputPath, true);	
				
        writer.WriteLine("Generated stored procedures written to file {0}", outputPath);
				
        if (RunScript)
        {
          ScriptResult scriptResult = SourceDatabase.SchemaProvider.ExecuteScript(SourceDatabase, sw.GetStringBuilder().ToString());	

          if (scriptResult.ScriptErrors.ErrorCount == 0)
          {
            writer.WriteLine("Script run succeeded.");
          }
          else
          {		
            writer.WriteLine("Script failed with the following errors:");
            foreach (ScriptError scriptError in scriptResult.ScriptErrors)
            {
              writer.WriteLine("Line number: {0} {1}", scriptError.LineNumber, scriptError.Message);
            }
          }
        }	
				
        crudSpSqlserver.Response.RemoveTextWriter(sw);															
      }				
    }
		
    writer.WriteLine();
    writer.WriteLine("All stored procedures for {0} generated successfully.", SourceDatabase.Name);
  }
  catch (Exception e)
  {
    writer.WriteLine();
    writer.WriteLine("Unable to generate CRUD stored procedures for {0} database.",  SourceDatabase.Name);
  }			
}	
</script>

As you can see it is simple to call the stored procedure template. I create an instance of it as follows:
CrudSpSqlserver crudSpSqlserver = new CrudSpSqlserver();
In this scenario I want to use the template to create a stored procedure script on disk as follows:
 crudSpSqlserver.RenderToFile(outputPath, true);	

Conclusion

As you can see code generation has the potential to automate many mundane and repetitive programmer tasks.