Better Entities with Nullable Types, Page 2
Implementing a SafeRead(Of T) Method
Generic classes and methods are now used all over the place in .NET. Generics are useful. Any time you can find a general algorithm that will work on a variety of types, use Generics.
In my daily work, I routinely use macros, snippets, CodeRush, and CodeDOM code generators. By establishing a pattern of completing a task, it makes it easier to automate that task, or at least do it very quickly. For this reason, the SafeRead(Of T) method was developed (by me).
SafeRead is implemented to accept a type, some kind of ADO.NET container such as a DataRow, a field name, and a default value. Then, SafeRead attempts to read the field as long as it isn't null. If the field contains a null, the default value is used. Instead of code that looks like this:
Dim v As Object = reader("EmployeeID")
Dim employeeID As Integer
If (v Is Nothing Or v Is System.DBNull.Value) Then
employeeID = Convert.ToInt32(v)
Else
employeeID = -1 ' default value
End If
repeated over and over, the checking code is converged into a single method and the workload is pleasantly reduced. Listing 4 contains my implementation of SafeRead(Of T).
Listing 4: A generic method named SafeRead that takes the grunt work out of reading field values and checking for null.
Public Shared Function SafeRead(Of T)(ByVal fieldname As String, _
ByVal reader As SqlDataReader, ByVal defaultValue As T) As T
Dim v As Object = reader(fieldname)
If (v Is Nothing Or v Is System.DBNull.Value) Then
Return defaultValue
Else
Return Convert.ChangeType(v, GetType(T))
End If
End Function
First, you read the field. In this example, from a SqlDataReader. (You can extend SafeRead to accept an IDataReader, making SafeRead provider agnostic.) Then, you check for Nothing or DBNull. If the field is any variation of nothingness, the default value you provided is returned. If the data is good, you use Convert.ChangeType and the parameterized type T to convert to the correct data type.
It is worth noting here that VB permits you to get loosey goosey with data types, and you probably can get away with assigning the Object type from an indexed SqlDataReader to a specific type. However, it is worth noting that being specific is a more portable and reliable way to write code in general. To use SafeRead, call it with parameters that look like this:
Dim employeeID As Integer = _
SafeRead(Of Integer)("EmployeeID", reader, -1)
Creating a Field Descriptor Attribute
Suppose that you want to eliminate some more overhead for building entity types. For example, you could use metadata to provide the field name, field type, and default value. Then, each entity's field can carry with it the information it needs for SafeRead. Listing 5 contains an implementation of a custom attribute named FieldDescriptorAttribute.
Listing 5: A custom attribute that can be used to assist with reading the entity fields.
<AttributeUsage(AttributeTargets.Property)> _
Public Class FieldDescriptorAttribute
Inherits Attribute
''' <summary>
''' Initializes a new instance of the FieldDescriptorAttribute
''' class.
''' </summary>
''' <param name="fieldName"></param>
''' <param name="fieldType"></param>
Public Sub New(ByVal fieldName As String, _
ByVal fieldType As Type, ByVal defaultValue As Object)
FFieldName = fieldName
FFieldType = fieldType
If (FFieldType Is GetType(DateTime)) Then
FDefaultValue = DateTime.MinValue
Else
FDefaultValue = defaultValue
End If
End Sub
Private FFieldName As String
Public Property FieldName() As String
Get
Return FFieldName
End Get
Set(ByVal Value As String)
FFieldName = Value
End Set
End Property
Private FFieldType As Type
Public Property FieldType() As Type
Get
Return FFieldType
End Get
Set(ByVal Value As Type)
FFieldType = Value
End Set
End Property
Private FDefaultValue As Object
Public Property DefaultValue() As Object
Get
Return FDefaultValue
End Get
Set(ByVal Value As Object)
FDefaultValue = Value
End Set
End Property
End Class
The custom attribute class FieldDescriptorAttribute is simply a container for arguments to SafeRead. The only noteworthy aspect is that I check for DateTime and set the default value in the custom attribute because I couldn't remember (or figure out) how to initialize an attribute with a literal Date. (If any of you smart readers know how to do this, drop me a line at pkimmel@softconcepts.com.) Having defined the FieldDescriptorAttribute, you can define a custom entity class (based on any table, but Employees was used) and adorn the property/entity fields with the attribute (see Listing 6).
Listing 6: An employee class that uses the FieldDescriptorAttribute, which can be used by SafeRead with Reflection.
Public Class Employee
Private FEmployeeID As Nullable(Of Integer)
<FieldDescriptor("EmployeeID", GetType(Integer), -1)> _
Public Property EmployeeID() As Nullable(Of Integer)
Get
Return FEmployeeID
End Get
Set(ByVal Value As Nullable(Of Integer))
If (Value.HasValue = False) Then
Console.WriteLine("Employee ID is null")
End If
FEmployeeID = Value
End Set
End Property
Private FLastName As String
<FieldDescriptor("LastName", GetType(String), "")> _
Public Property lastName() As String
Get
Return FLastName
End Get
Set(ByVal Value As String)
FLastName = Value
End Set
End Property
Private FFirstName As String
<FieldDescriptor("FirstName", GetType(String), "")> _
Public Property FirstName() As String
Get
Return FFirstName
End Get
Set(ByVal Value As String)
FFirstName = Value
End Set
End Property
Private FBirthDate As Nullable(Of DateTime)
<FieldDescriptor("BirthDate", GetType(DateTime), Nothing)> _
Public Property BirthDate() As Nullable(Of DateTime)
Get
Return FBirthDate
End Get
Set(ByVal Value As Nullable(Of DateTime))
FBirthDate = Value
End Set
End Property
Private FPhoto As Byte()
<FieldDescriptor("Photo", GetType(Byte()), New Byte() {0})> _
Public Property Photo() As Byte()
Get
Return FPhoto
End Get
Set(ByVal Value As Byte())
FPhoto = Value
End Set
End Property
Public Overrides Function ToString() As String
Return String.Format("Employee: {0} {1}, {2} is in {3}", _
FEmployeeID, FLastName, FFirstName, FRegion)
End Function
Private FRegion As String
<FieldDescriptor("Region", GetType(String), "(unk)")> _
Public Property Region() As String
Get
Return FRegion
End Get
Set(ByVal Value As String)
FRegion = Value
End Set
End Property
End Class
