Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#
Article

C# 2.0 Nullable Data Readers

Rate me:
Please Sign up or sign in to vote.
4.90/5 (43 votes)
20 Jan 20067 min read 173.4K   1K   94   21
An article on nullable data readers for .NET 2.0 nullable types.

Introduction

.NET 2.0 introduced nullable types into the CLR which, for the first time, provided the ability for value types to be assigned a null value. However, ADO.NET 2.0 did not introduce any new features specifically for dealing with nullable types. Therefore, one of the primary goals for the classes presented in this article is to provide a simple API for working with nullable types within the persistence layer.

There are three primary goals that the data readers will address.

  • Goal #1: Provide a simple, strongly-typed API for dealing with non-nullable types for both an IDataReader and a DataRow.
  • Goal #2: Provide a simple, strongly-typed API for dealing with nullable types for both an IDataReader and a DataRow.
  • Goal #3: Provide a unified interface so that the same code can be used polymorphically to consumer either an IDataReader or a DataRow.

Goal #1

Goal #1 is to provide a simple, unified, strongly-typed API for dealing with non-nullable types for both an IDataReader and a DataRow. The two features of the API are:

  1. to be able to reference everything with a readable column name (rather than an ordinal), and
  2. have strongly-typed methods for everything to avoid casting/conversion code.

The IDataReader interface provides several convenient strongly-typed GetXXX() methods to access the data. The problem is that these methods require a cryptic ordinal rather than a readable column name. This results in data access code that looks like this:

C#
person.Age = reader.GetInt32(reader.GetOrdinal("Age"));

It would be much more convenient to write the method like this:

C#
person.Age = reader.GetInt32("Age");

When working with a DataRow, the value returned is System.Object which means that you have to either pay the unboxing penalty by casting like this:

C#
person.Age = (int)row["Age"];

or you have to using the Convert class like this:

C#
person.Age = Convert.ToInt32(row["Age"]);

It would be more convenient if you could access data from the DataRow via strongly-typed methods similar to the IDataReader without having to code the monotonous casting/conversion code.

Goal #2

Goal #2 is to provide a simple, strongly-typed API for dealing with nullable types.

The IDataReader provides no methods for dealing with nullable types. Therefore, to correctly populate nullable types, the data access code would have to be littered with procedural, error-prone code like this:

C#
if (reader.IsDBNull(reader.GetOrdinal("FiredDate")))
    person.FiredDate = null;
else
    person.FiredDate = 
      reader.GetDateTime(reader.GetOrdinal("FiredDate"));

This is clearly not ideal – it would be best to have the same strongly-typed GetXXX() methods for nullable types.

Goal #3

Goal #3 is to provide a unified interface so that the same code can be used polymorphically to consume either an IDataReader or a DataRow.

The IDataReader and DataRow have very different APIs. In some instances, an application might require an object to be retrieved via an IDataReader for optimal performance. In other cases, the data might be retrieved as part of a DataSet (if it is part of a larger query). It would be ideal to be able to program against the same interface polymorphically in either case.

INullableReader

The INullableReader interface defines a contract that a class must implement in order to read both nullable and non-nullable data. This will not only provide a unified interface between an IDataReader and a DataRow but also it will allow the classes to be used polymorphically. The interface definition ensures that all of the GetXXX() methods from the IDataReader have corresponding GetXXX() methods that take a string (for the column name) instead of an ordinal. Additionally, each of these GetXXX() methods have GetNullableXXX() counterparts.

Interface definition:

C#
public interface INullableReader
{
    bool GetBoolean(string name);
    Nullable<bool> GetNullableBoolean(string name);
    byte GetByte(string name);
    Nullable<byte> GetNullableByte(string name);
    char GetChar(string name);
    Nullable<char> GetNullableChar(string name);
    DateTime GetDateTime(string name);
    Nullable<DateTime> GetNullableDateTime(string name);
    decimal GetDecimal(string name);
    Nullable<Decimal> GetNullableDecimal(string name);
    double GetDouble(string name);
    Nullable<double> GetNullableDouble(string name);
    float GetFloat(string name);
    Nullable<float> GetNullableFloat(string name);
    Guid GetGuid(string name);
    Nullable<Guid> GetNullableGuid(string name);
    short GetInt16(string name);
    Nullable<short> GetNullableInt16(string name);
    int GetInt32(string name);
    Nullable<int> GetNullableInt32(string name);
    long GetInt64(string name);
    Nullable<long> GetNullableInt64(string name);
    string GetString(string name);
    string GetNullableString(string name);
    object GetValue(string name);
    bool IsDBNull(string name);
}

Although this interface provides GetValue() and IsDBNull() methods, these are more for completeness, and will typically not be used in code.

NullableDataReader

The NullableDataReader implements the INullableReader interface and provides a wrapper around an IDataReader object. Therefore, this works with SqlDataReader, OracleDataReader, etc. There is even a new class in ADO.NET 2.0, called DataTableReader, which can be wrapped as well.

To instantiate a NullableDataReader, simply pass the IDataReader to the constructor. Example with the Enterprise Library Data Access block:

C#
dr = new NullableDataReader(db.ExecuteReader(cmd));

Example with raw ADO.NET:

C#
dr = new NullableDataReader(cmd.ExecuteReader());

To read values, simply refer to the column names:

C#
person.Age = dr.GetInt32("Age");
person.FiredDate = dr.GetNullableDateTime("FiredDate");

The NullableDataReader also implements IDataReader. Therefore, the NullableDataReader can be used like any other data reader. For example:

C#
try
{
    while (dr.Read())
    {
      Person person = new Person();
      person.Age = dr.GetInt32("Age");
      person.FiredDate = 
        dr.GetNullableDateTime("FiredDate");
      personList.Add(person);
    }
}
finally
{
    dr.Dispose();
}

The above code looks no different than that of any other data reader except:

  1. the GetInt32() methods take a column name instead of an ordinal, and
  2. a GetNullableDateTime() method is available which is not present on a normal data reader.

NullableDataRowReader

The NullableDataReader also implements the INullableReader interface and provides a wrapper around a DataRow object. Because it provides all of the strongly-typed methods, the access code need not contain casts and conversions.

Instantiate a NullableDataRow, by passing a DataRow to the constructor, or by assigning the DataRow to the Row property.

If reading a single row, then passing a DataRow to the constructor is the simplest:

C#
NullableDataRowReader dr = new NullableDataRowReader(row);
person.Age = dr.GetInt32("Age");
person.FiredDate = dr.GetNullableDateTime("FiredDate");

Notice, the access methods look identical to that of the DataReader.

If reading multiple rows (e.g., while iterating through a loop), then assigning the DataRow to the Row property is the simplest:

C#
NullableDataRowReader dr = new NullableDataRowReader();
foreach (DataRow row in dataTable.Rows)
{
    dr.Row = row;
    Person person = new Person();
    person.Age = dr.GetInt32("Age");
    person.FiredDate = dr.GetNullableDateTime("FiredDate");
    personList.Add(person);
}

In the above example, we just iterated all the rows of a DataTable for the sake of a simplistic example. In fact, in that example, you could simply use a NullableDataReader in conjunction with the new DataTableReader, like this:

C#
NullableDataReader dr = new 
   NullableDataReader(dataTable.CreateDataReader());

However, when working with DataTables, we often want to filter and also utilize GetChildRows(), which makes the NullableDataRowReader extremely convenient.

Polymorphic NullableReader

In some cases, we may need to populate a business object with a DataReader (for optimal performance), and other times populate the same object with a DataRow (e.g., if retrieved in a multi-resultset DataSet). Rather than having to write two separate methods (one for the NullableDataReader and another for the NullableDataRowReader), one can program against the INullableReader interface polymorphically and write just a single method.

C#
public Address BuildItem(INullableReader dr)
{
    Address address = new Address();

    address.ID = dr.GetInt32(Params.AddressID);
    address.StreetAddress1 = dr.GetString(Params.StreetAddress1);
    address.StreetAddress2 = dr.GetString(Params.StreetAddress2);
    address.City = dr.GetString(Params.City);
    address.State = dr.GetString(Params.State);
    address.ZipCode = dr.GetString(Params.ZipCode);

    return address;
}

The BuildItem() method can be called in two different ways. First, with a NullableDataReader:

C#
NullableDataReader dr = new 
       NullableDataReader(db.ExecuteReader(cmd));
person.Address = addressMapper.BuildItem(dr);

And secondly, with a NullableDataRowReader:

C#
NullableDataRowReader dr = new 
      NullableDataRowReader(addressTable.Rows[0]);
person.Address = addressMapper.BuildItem(dr);

Implementation Details

Internally, the NullableDataReader and the NullableDataRowReader use many of the new C# 2.0 language features to produce concise, high-performance code. Specifically, they utilize:

  1. generics,
  2. delegate inference, and of course
  3. nullable types.

Of course, to consume the data readers, the developer is not required to be aware of any of these implementation details.

To illustrate the internal implementation, we will examine the GetInt32() and GetNullableInt32() methods of the NullableDataReader class. Since the NullableDataReader wraps an IDataReader via its constructor as a private member, the GetInt32() method simply delegates this method call to the wrapped reader:

C#
public int GetInt32(int i)
{
    return reader.GetInt32(i);
}

To provide an overloaded method that takes a column name instead of an ordinal, the standard approach is used while shielding the implementation from the consumer:

C#
public int GetInt32(string name)
{
    return reader.GetInt32(reader.GetOrdinal(name));
}

Up to this point, we haven't done anything terribly interesting (although the new overload has provided considerable convenience). To provide two overloads to the GetNullableInt32() method, we could do this:

C#
public Nullable<int> GetNullableInt32(string name)
{
    return this.GetNullableInt32(reader.GetOrdinal(name));
}

public Nullable<int> GetNullableInt32(int index)
{
    Nullable<int> nullable;
    if (reader.IsDBNull(index))
    {
        nullable = null;
    }
    else
    {
        nullable = GetInt32(index);
    }
    return nullable;
}

However, the problem here is that, while not complicated, the nullable assignment with the if statement in the second overload will essentially have to be duplicated in each GetNullableXXX() method for each of the different data types – the only differences being:

  1. the type of nullable, and
  2. the method called (e.g., the GetInt32() method in the else block above).

To address this issue and produce code that is more concise and elegant, we can utilize a generic method that includes passing a delegate which we now have available as part of the C# 2.0 anonymous methods functionality. Therefore, we can simply create one method that performs the assignment:

C#
private Nullable<T> GetNullable<T>(int ordinal, 
            Conversion<T> convert) where T : struct
{
    Nullable<T> nullable;
    if (reader.IsDBNull(ordinal))
    {
        nullable = null;
    }
    else
    {
        nullable = convert(ordinal);
    }
    return nullable;
}

First, notice that we are using a generic type and specified the constraint that T must be a value type (i.e., struct). Secondly, notice that the second argument of the method is actually a custom private delegate that was defined for our purposes:

C#
private delegate T Conversion<T>(int ordinal);

This makes it possible for all of the GetNullableXXX() methods to simply require a single line of code (rather than its own if statement):

C#
public Nullable<INT> GetNullableInt32(int index)
{
    return GetNullable<INT>(index, GetInt32);
}

Notice that the second argument called is actually utilizing C# 2.0 delegate inference, and specifying that the normal GetInt32() method should be invoked in order to make the assignment in the case where the value is not DBNull.

Having the ability to utilize the same method for every GetNullableXXX() method results in code that is more concise, less error-prone, and more maintainable.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
Steve Michelotti, MCSD, MCT is Principal Developer at e.magination in Baltimore - www.emagination.com.

Comments and Discussions

 
GeneralNullable writer Pin
Edwin Engelen29-May-07 20:14
Edwin Engelen29-May-07 20:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.