First let me apologize a bit for the length of this post, it's mostly code though so I hope you all bear with me!
I have a scenario in dealing with a legacy database, where I needed to write an IUserType using NHibernate 3.2 to take a 2 character "status" field and return a Boolean value from it. The status field can hold 3 possible values:
* 'DI' // 'Disabled', return false
* ' ' // blank or NULL, return true
* NULL
Here is what I have simplified.
Table Definition:
CREATE TABLE [dbo].[Client](
[clnID] [int] IDENTITY(1,1) NOT NULL,
[clnStatus] [char](2) NULL,
[clnComment] [varchar](250) NULL,
[clnDescription] [varchar](150) NULL,
[Version] [int] NOT NULL
)
Fluent Mapping:
public class ClientMapping : CoreEntityMapping<Client>
{
public ClientMapping()
{
SchemaAction.All().Table("Client");
LazyLoad();
Id(x => x.Id, "clnId").GeneratedBy.Identity();
Version(x => x.Version).Column("Version").Generated.Never().UnsavedValue("0").Not.Nullable();
OptimisticLock.Version();
Map(x => x.Comment, "clnComment").Length(250).Nullable();
Map(x => x.Description, "clnDescription").Length(250).Nullable();
Map(x => x.IsActive, "clnStatus").Nullable().CustomType<StatusToBoolType>();
}
}
My IUserType Implementation:
public class StatusToBoolType : IUserType
{
public bool IsMutable { get { return false; } }
public Type ReturnedType { get { return typeof(bool); } }
public SqlType[] SqlTypes { get { return new[] { NHibernateUtil.String.SqlType }; } }
public object DeepCopy(object value)
{
return value;
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return cached;
}
public object Disassemble(object value)
{
return value;
}
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
return x.Equals(y);
}
public int GetHashCode(object x)
{
return x == null ? typeof(bool).GetHashCode() + 473 : x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (obj == null) return true;
var status = (string)obj;
if (status == " ") return true;
if (status == "DI") return false;
throw new Exception(string.Format("Expected data to be either empty or 'DI' but was '{0}'.", status));
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var parameter = ((IDataParameter) cmd.Parameters[index]);
var active = value == null || (bool) value;
if (active)
parameter.Value = " ";
else
parameter.Value = "DI";
}
}
However this doesn't work. This unit test fails with an inaccurate count.
[TestMethod]
public void GetAllActiveClientsTest()
{
//ACT
var count = Session.QueryOver<Client>()
.Where(x => x.IsActive)
.SelectList(l => l.SelectCount(x => x.Id))
.FutureValue<int>().Value;
//ASSERT
Assert.AreNotEqual(0, count);
Assert.AreEqual(1721, count);
}
The reason it fails is because it generates the following SQL:
SELECT count(this_.clnID) as y0_ FROM Client this_ WHERE this_.clnstatus = @p0;
/* @p0 = ' ' [Type: String (0)] */
But I need it to generate this instead:
SELECT count(this_.clnID) as y0_ FROM Client this_ WHERE (this_.clnstatus = @p0 <b> OR this_.clnstatus IS NULL);</b>
After some debugging I saw that the NullSafeSet() method in my StatusToBoolType class is invoked before the query is generated, so I was able to get around this by writing some hackish code in that method to manipulate the SQL in the cmd.CommandText property.
...
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var parameter = ((IDataParameter) cmd.Parameters[index]);
var active = value == null || (bool) value;
if (active)
{
parameter.Value = " ";
if (cmd.CommandText.ToUpper().StartsWith("SELECT") == false) return;
var paramindex = cmd.CommandText.IndexOf(parameter.ParameterName);
if (paramindex > 0)
{
// Purpose: change [columnName] = @p0 ==> ([columnName] = @p0 OR [columnName] IS NULL)
paramindex += parameter.ParameterName.Length;
var before = cmd.CommandText.Substring(0, paramindex);
var after = cmd.CommandText.Substring(paramindex);
//look at the text before the '= @p0' and find the column name...
var columnSection = before.Split(new[] {"= " + parameter.ParameterName}, StringSplitOptions.RemoveEmptyEntries).Reverse().First();
var column = columnSection.Substring(columnSection.Trim().LastIndexOf(' ')).Replace("(", "");
var myCommand = string.Format("({0} = {1} OR {0} IS NULL)", column.Trim(), parameter.ParameterName);
paramindex -= (parameter.ParameterName.Length + column.Length + 1);
var orig = before.Substring(0, paramindex);
cmd.CommandText = orig + myCommand + after;
}
}
else
parameter.Value = "DI";
}
But this is NHibernate!!! Hacking the sql statement like this can't possibly be the correct way to handle this? Right?
Because it is a shared legacy database, I can't change the table schema to NOT NULL otherwise I would have just done that, and avoided this scenario.
So finally after all this prelude my question is simply this, where can I tell NHibernate to generate a custom SQL criteria statement for this IUserType?
Thank you all in advance!