6

I have the following method:

public static T ExecuteScalar<T>(
    string query, 
    SqlConnection connection, 
    params SqlParameter[] parameters) where T : new()
{
    // Create SqlCommand
    SqlCommand command = CreateCommand(query, connection, parameters);

    // Execute command using ExecuteScalar
    object result = command.ExecuteScalar();

    // Return value as expected type
    if (result == null || result is DBNull) return default(T);
    return (T)result;
}

I want to have the MIN_ACTIVE_ROWVERSION of the database as an ulong. The strange thing is.. the First method call below generates an error but the second method call works fine.

Method call 1 generates an error:

ulong minActiveRowversion = 
    SqlUtils.ExecuteScalar<ulong>(
        "SELECT CAST(MIN_ACTIVE_ROWVERSION() AS BIGINT)"
        , _connectionString);

Error:

System.InvalidCastException: Specified cast is not valid.

Method call 2 works fine:

ulong minActiveRowversion = 
    (ulong)SqlUtils.ExecuteScalar<long>(
        "SELECT CAST(MIN_ACTIVE_ROWVERSION() AS BIGINT)"
        , _connectionString);

I don't understand how that is possible because the result of the command.ExecuteScalar() method is this:

object result       | 1955612
result.GetType()    | {Name = "Int64" FullName = "System.Int64"}
  1. Can someone tell me why the first scenario is not possible and the second scenario works?
  2. Can someone tell me how I can solve it so I can use scenario 1.
hwcverwe
  • 5,287
  • 7
  • 35
  • 63
  • 1
    You can only unbox value types to their actual type (the cast operator wouldn't know how to do the version otherwise). So do that, and *then* do the cast. – harold Jun 27 '12 at 12:44
  • an Int64 is a _signed_ 64-bit integer, as is a *long*. However a ulong is unsigned and so the unboxing in the first instance is invalid. You can do Convert.ToUInt64 on the result of the 2nd scenario to obtain an unsigned long though. http://stackoverflow.com/questions/5260314/c-sharp-isnt-a-int64-equal-to-a-long – Jason Larke Jun 27 '12 at 12:45

2 Answers2

5

Why

You can only unbox a value type to it's original type. In your case, the cast first needs to go to long from object and then to ulong.

See this question for more detail:

Why can't I unbox an int as a decimal?

It also links a blog post by Eric Lippert.

How

One way, as you know, is to cast to the original type before casting to T - unless, of course, the original type is T.

As mentioned in the comments, another way is to use conversion routines (Convert.ToUInt64) and not explicit casting.

This could potentially be achieved using a Func<object, T>:

public static T ExecuteScalar<T>(
    Func<object, T> conversionFunctor,
    string query, 
    SqlConnection connection, 
    params SqlParameter[] parameters) where T : new()
{
    // Create SqlCommand
    SqlCommand command = CreateCommand(query, connection, parameters);

    // Execute command using ExecuteScalar
    object result = command.ExecuteScalar();

    // Return value as expected type
    if (result == null || result is DBNull) 
        return default(T);

    return conversionFunctor(result);
}

Making your call:

ulong minActiveRowversion = 
    SqlUtils.ExecuteScalar<ulong>(
        Convert.ToUInt64,
        "SELECT CAST(MIN_ACTIVE_ROWVERSION() AS BIGINT)"
        , _connectionString);
Community
  • 1
  • 1
Adam Houldsworth
  • 63,413
  • 11
  • 150
  • 187
4

Adam's answer correctly identifies the problem; here is a solution: you can use LINQ to unbox any type, as long as it can be cast to T with a built-in or a custom conversion.

static T UnboxUnchecked<T>(object obj) {
    var pe = Expression.Parameter(typeof(object));
    return Expression.Lambda<Func<object,T>>(
        Expression.Convert(
            Expression.Convert(pe, obj.GetType())
        ,   typeof (T)
        )
    ,   pe
    ).Compile()(obj);
}

This method produces a LINQ expression that first unboxes the object to its actual type, and then applies the conversion. Replace the last line of your method

return (T)result;

with

return UnboxUnchecked<T>(result);

to make it work.

Here is a link to an article that explains how to make conversions of this kind more efficient by caching the compiled lambdas.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • +1 for the nice answer. I hesitated a moment what answer I had to accept. I accepted @AdamHouldsworth answer because I was more curious about why. Thanks! – hwcverwe Jul 02 '12 at 07:19
  • What would be the code to check if obj is null? Would it be better to throw a NullReferenceException, or do something like return default(T)? – Mas Oct 01 '12 at 08:33
  • @Mas What to do when you see a `null` depends on your requirements. The code to check for `null` inside your lambda would not fit in a comment. If you need an answer, post a separate question, link to this answer, and someone will explain how to insert a check into a lambda expression. There will be quite a bit of additional code, though. – Sergey Kalinichenko Oct 01 '12 at 10:50