| More
All posts tagged 'c# code programming conversion casting'
 


Using Extension Methods to Improve & Simplify Type Conversion in .Net

As the .NET platform has evolved, languages like C# and VB.NET have introduced more and more features to improve the compile-time type safety of the code we write. Features like generics, custom conversion operators, and now co- and contra-variance make it easier than ever before to write code that the compiler can examine for type errors. With each new release, more and more of these features also find their way into the .NET framework classes.

Even in .Net 4, every now and then you run across a situation where type conversion rears it's ugly head and we have to revert to writing code that requires runtime casts. One such situation is when you write code that uses the ADO.NET classes and interfaces - particularly DataTable and IDataReader. Matters can get even worse when we don't know (or don't correctly anticipate) the type of value returned in a given call. If we don't tread carefully here, we can easily encounter runtime errors. Look at the following innocuous bit of code:

var connection = new OracleConnection("Data Source=DEV;");
IDbCommand cmd = new OracleCommand("SELECT 1.85 AS AGE FROM DUAL", connection);
connection.Open();

double age = (double) cmd.ExecuteScalar();

While this looks like it should work, if we actually run it we get:

System.InvalidCastException: Specified cast is not valid.

Clearly, that's not what we want. What's happening here is that Oracle returns a decimal here rather than a double. ExecuteScalar() returns object, and it's illegal to try to unbox a value of one type into a variable of a different type, even if a conversion exists between the two types. While this is a common point of confusion in C#, there is a straight-forward solution - perform the cast in two steps:

double age = (double)(decimal) cmd.ExecuteScalar();

An alternative approach is to use the Convert class:

double age = Convert.ToDouble( cmd.ExecuteScalar() );

Convert will do all of the necessary runtime checks to figure out what type we're dealing with, and will return an appropriately-typed result. Convert.ToDouble() will even work if the value passed in is a string:

IDbCommand cmd = new OracleCommand("SELECT '1.85' AS AGE FROM DUAL", connection);
double age = Convert.ToDouble( cmd.ExecuteScalar() );

Convert is nice, but it does have it's limitations. Among them are:

  1. It can't convert to or from nullable types (int?, double?, DateTime?)
  2. It can't convert DBNull values, which are often encountered in ADO.NET
  3. It doesn't handle conversions to or from enum types
  4. It offers no ability to supply a default value when dealing with nulls
  5. It isn't much less verbose than the straight-forward casting syntax

We can do better than this. In fact, we're going to create a set of extension methods that will make it simpler, easier, and cleaner to perform runtime conversion of types, and we're going to address the limitations above along the way.

Show me the code! All right, down to business. The following listing shows the complete source for our conversion utility class. The class is marked as static to allow it to define a number of extension methods.

public static class ConvertExtesions
{
// converts the source object variable value to the desired result type
public static TR ChangeType<TR>( this object value ) { return (TR)ChangeType(value, typeof(TR)); }

// converts the source value, and if null returns the specified default value public static TR ChangeType<TR>( this object value, TR whenNull ) { return (value == null || value is DBNull)
? whenNull
: (TR)ChangeType(value, typeof(TR)); } public static object ChangeType( this object value, Type convertToType ) { if ( convertToType == null )
{
throw new ArgumentNullException("convertToType");
}

// return null if the value is null or DBNull
if( value == null || value is DBNull )
{
return null;
} // non-nullable types, which are not supported by Convert.ChangeType(), // unwrap the types to determine the underlying time if (convertToType.IsGenericType &&
convertToType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
{ convertToType = Nullable.GetUnderlyingType(convertToType);
} // deal with conversion to enum types when input is a string if (convertToType.IsEnum && value is string) { return Enum.Parse(convertToType, value as string); }

// deal with conversion to enum types when input is a integral primitive
if (value != null && convertToType.IsEnum && value.GetType().IsPrimitive && !(value is bool) && !(value is char) && !(value is float) && !(value is double) ) { return Enum.ToObject(convertToType, value); }
// use Convert.ChangeType() to do all other conversions
return Convert.ChangeType(value, convertToType, CultureInfo.InvariantCulture); } }

We're going to break down and examine the code in a moment. But first, let's see how this makes our earlier example better. The same code from before can now be written as:

double age = cmd.ExecuteScalar().ChangeType<double>();

We can even substitute null values with defaults gracefully:

double age = cmd.ExecuteScalar().ChangeType( 0.0d ); // return 0.0 when null

Nullable types are no obstacle either:

double? age = cmd.ExecuteScalar().ChangeType<double?>();

Don't know the type at compile time? Not a problem:

object age = cmd.ExecuteScalar().ChangeType( Type.GetType("System.Double") );

Strings and integral values can now also be converted to enums:

enum CompareResult
{
    LessThan,
    EqualTo,
    GreaterThan,
}

IDbCommand cmd = new OracleCommand("SELECT 'EqualTo' AS CR FROM DUAL", connection);


CompareResult age = cmd.ExecuteScalar().ChangeType<CompareResult>();

So, how did we make this all possible? Let's take a look at the implementation of the CompareExtensions class. The first two overloads of ChangeType<> are:

public static TR ChangeType<TR>( this object value );
public static TR ChangeType<TR>( this object value, TR whenNull );

In all overloads, TR is the type of the result that will be returned. The first overload accepts any object will attempt to convert it to the type TR. Because the type parameter only appears in the output position it cannot be inferred for us - which requires that we supply it:

int valueAsInt = "12345".ChangeType<int>();

The second overload accepts a default value parameter to return if the conversion fails. By making the type of the default value parameter TR we also allow the compiler to infer the type of the return value from the default, which means we can omit the explicit type argument list:

object value = null;
int result = value.ChangeType(-1); // TR is inferred here...

Neither the first or second overloads do much work, the second performs a bit of null checking as a shortcut - but otherwise, both simply delegate to the third overload. That's where all the real work happens.

The third overload of ChangeType() looks like:

public static object ChangeType( this object value, Type convertToType )

You probably notice that this overload is not, in fact, generic. This allows it to be used in situations where the type we are converting to is not known at compile time but only at runtime. The very first step of this method:

if( convertToType == null ) { throw new ArgumentNullException("convertToType"); }

ensures that there's always a Type to which we will try to convert the value to. Next, we check if the value is null or DBNull, in which case the converted value will always be null, so we just return that:

if( value == null || value is DBNull ) { return null; }

Otherwise, we check if the type we're converting to is a nullable type - in which case we really need to perform a conversion to the type that underlies the nullable type. We determine this by using the Nullable.GetUnderlyingType() method, and set that as the type we are actually converting to.

if (convertToType.IsGenericType && 
    convertToType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
{
    convertToType = Nullable.GetUnderlyingType(convertToType);
}

The next step is to check if we are converting from a string representation to an enum representation, and if so, use Enum.Parse() to convert the value. Otherwise, we also need to check if we are converting from a primitive type to an enum representation:

if (value != null && convertToType.IsEnum && value.GetType().IsPrimitive &&
   !(value is bool) && !(value is char) && !(value is float) && !(value is double) )
{
    return Enum.ToObject(convertToType, value);
}

Here we should note that for such a conversion to be possible, the incoming value must not be null and must be one of the integral types other than char: byte, sbyte, short, ushort, int, uint, long, ulong). If this is the case, we use Enum.ToObject() to convert and return the result.

Finally, if no other case holds, we just delegate to Convert.ChangeType() to do all other conversion processing, and return its result.

And there you have it. Let's wrap things up and look at what we've figured out:

  1. Casting can be a tricky business, particularly when you don't know (or can't always know) the types you are converting from at compile time.
  2. You encounter runtime exceptions if you try to unbox a value from an object to a type other than what is actually boxed, even if a conversion exists.
  3. The Convert class provides a helpful suite of methods for runtime conversion, including the flexible ChangeType() method.
  4. Convert has a number of limitations - it doesn't deal with nullable types, DBNull, or enums, and it doesn't allow us to specify a default value for nulls.
  5. Using extension methods and type inference, we can create our own flexible conversion utility that overcome the limitations of ChangeType, and simplifies the syntax of runtime type conversion.

Posted by: Leo Bushkin
Posted on: 7/6/2010 at 5:33 PM
Tags:
Categories: .NET
Actions: E-mail | Kick it! | DZone it! | del.icio.us
Post Information: Permalink | Comments (0) | Post RSSRSS comment feed