Generics and Nullable Types

It is often useful in OR mapping to analyze a value type that might be null prior to setting it in a business entity. For instance, SqlDateTime type (in System.Data.SqlTypes namespace) is a value type that is nullable. You can call IsNull to check and Value to retrieve the underlying DateTime type:

SqlDateTime d = new SqlDateTime();
if (d.IsNull)
{
    Trace.WriteLine("SqlDateTime is null");
}

d = DateTime.Now;
Trace.WriteLine(string.Format("SqlDateTime is {0}", d.Value.ToString()));

This is useful when fetching data from SQLServer. DataSets allow nullable values but strongly-typed classes do not. So if I’m instantiating a business entity, I often call TryParse first to protect against null:

//instantiate SqlCommand cm here
SomeBusinessEntity obj = new SomeBusinessEntity();
DateTime dt = new DateTime();
SqlDataReader dr = cm.ExecuteReader();
if (dr != null) 
{
    while (dr.Read()) 
    {
        obj.ID = (DBNull.Value != dr["ID"]) ? Convert.ToInt32(dr["ID"]) : 0;
        if (DateTime.TryParse(dr["ApprovalDate"].ToString(), out dt))
        obj.ApprovalDate = dt;
    }
}

Wouldn’t it be great to have a DateTime struct in C# that allows for null? A bunch of folks have gnashed their teeth over this issue since the release of .NET. And now .NET Framework 3.0 supports nullable value types. But I’m still in 2.0 land and will likely remain there for some time so here’s my solution. The goal is to support both the above OR mapping logic via generics and to emulate the nullable feature of the SqlDateTime type. In the end we want to do something like this:

Nullable ndt = new Nullable();
if (Nullable.TryParse(DateTime.Now.ToString(), out ndt))
{
    obj.ApprovalDate = ndt;
}

We start with a basic generics-enabled struct with exposed properties Value and IsNull:

public struct Nullable 
{
    private static bool _isnull;
    private T _value;

    static Nullable() 
    {
        _isnull = true;
    }

    public bool IsNull 
    {
        get { return _isnull; }
    }

    public T Value 
    {
        get { return _value; }
        set 
        {
            _value = value;
            _isnull = false;
        }
    }
}

The struct has a private _isnull member which is set to true in the static constructor. Only when the value is set does that flag flip to false. So by default it will be null even if the type is not really null; for instance if the underlying DateTime is initialized at 1/1/0001 or the underlying Int32 is initialized at 0. Now we support the TryParse functionality by adding a delegate function that mimics the signature. Note that I’m not supporting the overloaded method for IFormatProvider but rather keeping it simple:

private delegate bool TryParseDelegate(string s, out T result);

Then add a private static function to do the work:

private static bool ParseNullable(string s, out Nullable result, TryParseDelegate Parse) where T : struct 
{
    if (string.IsNullOrEmpty(s)) 
    {
        result = default(Nullable);
        return false;
    }
    else 
    {
        T t;
        bool success = Parse(s, out t);
        Nullable n = new Nullable();
        n.Value = t;
        result = n;
        return success;
    }
}

At this point it’s just a matter of adding public methods for each type that you need to support. I’ve added two methods, one for DateTime and one for Int32. Here is the complete Nullable struct:

public struct Nullable 
{
    private static bool _isnull; 
    private T _value;
    private delegate bool TryParseDelegate(string s, out T result);

    static Nullable() 
    {
        _isnull = true;
    }

    public bool IsNull 
    { 
        get 
        { 
            return _isnull; 
        }   
    }

    public T Value 
    {
        get 
        {           
           return _value;       
        }
        set 
        {
            _value = value;
            _isnull = false;
        }
    }

    public static bool TryParse(string s, out Nullable result) 
    {
        return ParseNullable(s, out result, Int32.TryParse);
    }

    public static bool TryParse(string s, out Nullable result) 
    {
        return ParseNullable(s, out result, DateTime.TryParse);
    }

    private static bool ParseNullable(string s, out Nullable result, TryParseDelegate Parse) where T : struct 
    {
        if (string.IsNullOrEmpty(s)) 
        {
            result = default(Nullable);
            return false;
        }
        else 
        {
            T t;
            bool success = Parse(s, out t);            
            Nullable n = new Nullable();            
            n.Value = t;
            result = n;
            return success;
        }
    }
}

To exercise the struct:

class Program 
{
    static void Main(string[] args) 
    {
        // exercise Nullable with type DateTime
        Nullable ndt = new Nullable();
        Console.WriteLine(string.Format("Is null? {0}", ndt.IsNull.ToString()));
        Console.WriteLine(string.Format("Value is {0}", ndt.Value.ToString()));
        
        ndt.Value = DateTime.Now;

        Console.WriteLine(string.Format("Is null? {0}", ndt.IsNull.ToString()));
        Console.WriteLine(string.Format("Value is {0}", ndt.Value.ToString()));
        Console.WriteLine();

        // exercise Nullable with type Int32
        Nullable nint = new Nullable();
        Console.WriteLine(string.Format("Is null? {0}", nint.IsNull.ToString()));
        Console.WriteLine(string.Format("Value is {0}", nint.Value.ToString()));
        nint.Value = 42;

        Console.WriteLine(string.Format("Is null? {0}", nint.IsNull.ToString()));
        Console.WriteLine(string.Format("Value is {0}", nint.Value.ToString()));

        // exercise TryParse functionality
        if (Nullable.TryParse(DateTime.Now.ToString(), out ndt)) 
        {
            Console.WriteLine(string.Format("Successful TryParse with result {0}", ndt.Value.ToString()));
        }

        if (Nullable.TryParse("99", out nint)) 
        {
            Console.WriteLine(string.Format("Successful TryParse with result {0}", nint.Value.ToString()));
        }

        Console.ReadKey();
    }
}