One thing I noticed recently when working with Subsonic 3.0 and the T4 templates was that there was no way to detect if a stored procedure has nullable parameters. After a bit of research I found this msdn article that explains:
“SQL Server only maintains default values for CLR objects in this catalog view; therefore, this column has a value of 0 for Transact-SQL objects. To view the default value of a parameter in a Transact-SQL object, query the definition column of the sys.sql_modules catalog view, or use the OBJECT_DEFINITION system function.”
Well that kind of stinks. I want my cake and I want to eat it too.
I want my generated code to
a) Declare the parameters to stored procedures as nullable types
b) Ensure the code passes these parameters in as nullable types.
In order to accomplish this I changed the following in the T4 Templates.
1. Inside SQLServer.ttinclude
a. Add methods
public string GetStoredProcedureText(string procedureName)
{
string sql;
sql = @"EXEC sp_helptext '" + procedureName + "'";
SqlDataReader sprocReader;
StringBuilder sprocBuilder = new StringBuilder();
using (SqlConnection cn = new SqlConnection(ConnectionString))
{
cn.Open();
SqlCommand cmd = new SqlCommand(sql, cn);
sprocReader = cmd.ExecuteReader();
while (sprocReader.Read())
{
sprocBuilder.Append(sprocReader.GetString(0));
}
return sprocBuilder.ToString();
}
}
public List<string> GetNullableParams(string procedureText)
{
string arguments = string.Empty;
string[] argumentArray = new string[0];
List<string> parameterNullibility = new List<string>();
procedureText = Regex.Replace(procedureText, @"\r\n", " ");
procedureText = Regex.Replace(procedureText, @"\s+", " ");
procedureText = Regex.Replace(procedureText, @"\s*,\s*", ",");
procedureText = Regex.Replace(procedureText, @"/\*.*?\*/", "");
string regex = @"CREATE\s+((PROC)|(PROCEDURE))\s+((\[.+?\]\.?){*}|(\S*))(?<Params>.*?)\s{1}as\s{1}";
RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture;
MatchCollection matches = Regex.Matches(procedureText, regex, options);
if (matches.Count > 0 && matches[0].Groups["Params"] != null)
{
Match match = matches[0];
arguments = match.Groups["Params"].ToString();
argumentArray = Regex.Split(arguments, ",");
foreach (string sArgument in argumentArray)
{
MatchCollection paramMatches = Regex.Matches(sArgument, @"(?<ParamName>@\S*)", options);
MatchCollection nullMatches = Regex.Matches(sArgument, @"(?<NullString>=\s*)", options);
if (paramMatches.Count > 0 && paramMatches[0].Groups["ParamName"] != null)
{
bool isNullDefault = (nullMatches.Count > 0 && nullMatches[0].Groups["NullString"] != null);
parameterNullibility.Add(paramMatches[0].Groups["ParamName"].ToString());
}
}
}
return parameterNullibility;
}
b. Change GetSPPArams
List GetSPParams(string spName)
{
var result = new List();
string[] restrictions = new string[3] { null, null, spName };
string procedureText;0
procedureText = GetStoredProcedureText(spName);
List nullableParams = this.GetNullableParams(procedureText);
using (SqlConnection conn = new SqlConnection(ConnectionString))
{
conn.Open();
var sprocs = conn.GetSchema("ProcedureParameters", restrictions);
using (SqlCommand cmd = new SqlCommand(spName, conn))
{
cmd.CommandType = CommandType.StoredProcedure;
SqlCommandBuilder.DeriveParameters(cmd);
}
conn.Close();
foreach (DataRow row in sprocs.Select("", "ORDINAL_POSITION"))
{
SPParam p = new SPParam();
p.SysType = GetSysType(row["DATA_TYPE"].ToString());
p.DbType = GetDbType(row["DATA_TYPE"].ToString()).ToString();
p.Name = row["PARAMETER_NAME"].ToString().Replace("@", "");
p.ParameterMode = GetParamDirection(row["PARAMETER_MODE"].ToString()).ToString();
p.CleanName = CleanUp(p.Name);
if (nullableParams.Contains(row["PARAMETER_NAME"].ToString()))
p.Nullable = true;
result.Add(p);
}
}
return result;
}
2. Inside Setting.ttinclude
a. Change SSParam class as follows
public class SPParam{
public string Name;
public string CleanName;
public string SysType;
public string DbType;
public string ParameterMode;
public bool Nullable;
}
b. Change ArgList property as follows.
public string ArgList{
get{
StringBuilder sb=new StringBuilder();
int loopCount=1;
foreach(var par in Parameters){
if (par.Nullable)
{
if ( par.SysType.ToString() != "string" && par.SysType.ToString() != "byte[]")
{
sb.AppendFormat("{0}{1} {2}", par.SysType, "?", par.CleanName);
}
else
{
sb.AppendFormat("{0} {1}", par.SysType, par.CleanName);
}
}
else
{
sb.AppendFormat("{0} {1}", par.SysType, par.CleanName);
}
if(loopCount<Parameters.Count)
sb.Append(",");
loopCount++;
}
return sb.ToString();
}
}
}
3. Inside StoredProcedures.tt change the code to account for 1 and 2 above.
<#@ template language=”C#v3.5″ debug=”False” hostspecific=”True” #>
<#@ output extension=”.cs” #>
<#@ include file=”SQLServer.ttinclude” #>
<#
var sps = GetSPs();
if(sps.Count>0){
#>
using System;
using SubSonic;
using SubSonic.Schema;
using SubSonic.DataProviders;
using System.Data;
namespace <#=Namespace#>{
public partial class <#=DatabaseName#>DB{
<# foreach(var sp in sps){#>
public StoredProcedure <#=sp.CleanName#>(<#=sp.ArgList#>){
StoredProcedure sp=new StoredProcedure(“<#=sp.Name#>”,this.Provider);
<# foreach(var par in sp.Parameters){
if ( par.Nullable == true )
{
if (par.SysType == “string” || par.SysType == “byte[]”)
{
#>
sp.Command.AddParameter(“<#=par.Name#>”,<#=par.CleanName#>,DbType.<#=par.DbType#>,<#=par.ParameterMode#>);
<# }
else
{
#>
sp.Command.AddParameter(“<#=par.Name#>”,(<#=par.SysType#>?)<#=par.CleanName#>,DbType.<#=par.DbType#>,<#=par.ParameterMode#>);
<#
}
}
else
{
#>
sp.Command.AddParameter(“<#=par.Name#>”,<#=par.CleanName#>,DbType.<#=par.DbType#>,<#=par.ParameterMode#>);
<#
}
}#>
return sp;
}
<# }#>
}
}
<# }#>

10 thoughts on “SubSonic T4 Templates / Stored Procedures / Nullable Parameters”
Thanks for the solution!
However, I noticed (after dealing with a
System.ArgumentException: parsing “CREATE\s+((PROC)|(PROCEDURE))\s+((\[.+?\]\.?){*}|(\S*))(?.*?)\s{1}as\s{1}” – Unrecognized grouping construct.
error) that the group names in the RegEx are interpreted as HTML tags when the post is rendered as HTML, so there’s some missing info in the post.
Sure did. I pasted the updated code. Thanks for pointing that out.
Hi,
I’m struggling to get this to work. Where did you get “GetParamDirection”?
I will send them to you directly.
do you mind sending these my way as well? I’m having the same problem with GetParamDirection.
Thanks!
Please disregard my last post. After some debugging I was able to figure this out.
One last note for everyone else, make sure that your database connection context has permissions to “view definition” of all stored procedures in your database. Otherwise, you will receive the error: “There is no text for object ”.
Glad to you got this working. Great point about “view definition” permissions. Typically I have my T4 connection string have credentials of a power user.
If you want, I can still send the T4s to you. Hope this helps you out.
Brian
A quick and dirty way to accomplish the same thing would be to edit “ArgList” in Settings.ttinclude.
In the foreach loop change this line …
sb.AppendFormat(“{0} {1}”, par.SysType,par.CleanName);
to (notice the additional “?”)
sb.AppendFormat(“{0}? {1}”, par.SysType,par.CleanName);
That’s true but now the method parameters make it seem like everything is nullable. What if the underlying stored procedure has a parameter that is not nullable? The method signature would make it appear as if you could pass null.
Thanks for this – but one correction: in GetNullableParams, it needs to test for isNullDefault:
if(isNullDefault){
parameterNullibility.Add(paramMatches[0].Groups[“ParamName”].ToString());
}