3

How can I check for db null values in the attached code? Please understand I am a new C# convert...

What this code does is takes a IDataReader object and converts and maps it to a strongly-typed list of objects. But what I am finding is it completely errors out when there are null columns returned in the reader.

Converter

internal class Converter<T> where T : new()
{
    // Declare our _converter delegate
    readonly Func<IDataReader, T> _converter;
    // Declare our internal dataReader
    readonly IDataReader dataReader;

    // Build our mapping based on the properties in the class/type we've passed in to the class
    private Func<IDataReader, T> GetMapFunc()
    {
        // declare our field count
        int _fc = dataReader.FieldCount;
        // declare our expression list
        List<Expression> exps = new List<Expression>();
        // build our parameters for the expression tree
        ParameterExpression paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");
        ParameterExpression targetExp = Expression.Variable(typeof(T));
        // Add our expression tree assignment to the exp list
        exps.Add(Expression.Assign(targetExp, Expression.New(targetExp.Type)));
        //does int based lookup
        PropertyInfo indexerInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(int) });
        // grab a collection of column names from our data reader
        var columnNames = Enumerable.Range(0, _fc).Select(i => new { i, name = dataReader.GetName(i) }).AsParallel();
        // loop through all our columns and map them properly
        foreach (var column in columnNames)
        {
            // grab our column property
            PropertyInfo property = targetExp.Type.GetProperty(column.name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
            // check if it's null or not
            if (property != null)
            {
                // build our expression tree to map the column to the T
                ConstantExpression columnNameExp = Expression.Constant(column.i);
                IndexExpression propertyExp = Expression.MakeIndex(paramExp, indexerInfo, new[] { columnNameExp });
                UnaryExpression convertExp = Expression.Convert(propertyExp, property.PropertyType);
                BinaryExpression bindExp = Expression.Assign(Expression.Property(targetExp, property), convertExp);
                // add it to our expression list
                exps.Add(bindExp);
            }
        }
        // add the originating map to our expression list
        exps.Add(targetExp);
        // return a compiled cached map
        return Expression.Lambda<Func<IDataReader, T>>(Expression.Block(new[] { targetExp }, exps), paramExp).Compile();
    }

    // initialize
    internal Converter(IDataReader dataReader)
    {
        // initialize the internal datareader
        this.dataReader = dataReader;
        // build our map
        _converter = GetMapFunc();
    }

    // create and map each column to it's respective object
    internal T CreateItemFromRow()
    {
        return _converter(dataReader);
    }
}

Mapper

    private static IList<T> Map<T>(DbDataReader dr) where T : new()
    {
        try
        {
            // initialize our returnable list
            List<T> list = new List<T>();
            // fire up the lamda mapping
            var converter = new Converter<T>(dr);
            while (dr.Read())
            {
                // read in each row, and properly map it to our T object
                var obj = converter.CreateItemFromRow();
                // add it to our list
                list.Add(obj);
            }
            // reutrn it
            return list;
        }
        catch (Exception ex)
        {
            // make sure this method returns a default List
            return default(List<T>);
        }
    }

I just don't quite understand where the column to typed object happens in here, so I'd try to do it myself... but I just don;t know where it is.

I know this probably won't help much, but the error I am getting is:

Unable to cast object of type 'System.DBNull' to type 'System.String'.

and it happens on the

internal T CreateItemFromRow()
    {
        return _converter(dataReader); //<-- Here
    }

Note

This does not happen if I wrap the columns in the query itself with an ISNULL(column, ''), but I am sure you can understand that this is surely not a solution

Kevin
  • 2,684
  • 6
  • 35
  • 64
  • 1
    Sure. Easy to circumvent for reference types. But how does one go about for value types? How should DbNulls be treated then? May be you want a default value of type back, but how do you be very sure that that's how generically it should treated every single time? What if in case you dont want the default value but something else? In such cases, can we pass certain rules, should we need to add a provision for that as an overload or something? – nawfal Dec 10 '13 at 15:23
  • 1
    Well... I think if I understand it better, I could probably figure out the best way to handle them, but I think for now, `default value of type` would be the best way to go with this... at least that's my thoughts on it for now :) Like I said, once I can figure that out, I could always build an extension for this to handle the potential for specified `default` values... but I don't know where to look or begin LOL – Kevin Dec 10 '13 at 15:26
  • 1
    fair point! give me some time, I will add an answer within a day or so. Just too busy with work for the moment :) The problem is here `convertExp = Expression.Convert(propertyExp, property.PropertyType)`. You can't expect to convert DbNull to framework types. You need the `GetValueOrDefault` function before that to check for the value. – nawfal Dec 10 '13 at 15:37

3 Answers3

3

The problem lies in the line convertExp = Expression.Convert(propertyExp, property.PropertyType). You can't expect to convert DbNull value to its equivalent in framework type. This is especially nasty when your type is a value type. One option is to check if the read value from db is DbNull.Value and in case yes, you need to find a compatible value yourself. In some cases people are ok with default values of those types in C#. If you have to do this

property = value == DBNull.Value ? default(T): value;

a generic implementation would look like (as far as the foreach in your converter class goes):

foreach (var column in columns)
{
    var property = targetExp.Type.GetProperty(
        column.name,
        BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
    if (property == null)
        continue;

    var columnIndexExp = Expression.Constant(column.i);
    var propertyExp = Expression.MakeIndex(
        paramExp, indexerInfo, new[] { columnIndexExp });
    var convertExp = Expression.Condition(
        Expression.Equal(
            propertyExp, 
            Expression.Constant(DBNull.Value)), 
        Expression.Default(property.PropertyType), 
        Expression.Convert(propertyExp, property.PropertyType));
    var bindExp = Expression.Assign(
        Expression.Property(targetExp, property), convertExp);
    exps.Add(bindExp);
}

Now this does an equivalent of

property = reader[index] == DBNull.Value ? default(T): reader[index];

You could avoid the double lookup of the reader by assigning it to a variable and using its value in the conditional check. So this should be marginally better, but a lil' more complex:

foreach (var column in columns)
{
    var property = targetExp.Type.GetProperty(
        column.name,
        BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
    if (property == null)
        continue;

    var columnIndexExp = Expression.Constant(column.i);
    var cellExp = Expression.MakeIndex(
        paramExp, indexerInfo, new[] { columnIndexExp });
    var cellValueExp = Expression.Variable(typeof(object), "o7thPropValue");
    var convertExp = Expression.Condition(
        Expression.Equal(
            cellValueExp, 
            Expression.Constant(DBNull.Value)), 
        Expression.Default(property.PropertyType), 
        Expression.Convert(cellValueExp, property.PropertyType));
    var cellValueReadExp = Expression.Block(new[] { cellValueExp },
        Expression.Assign(cellValueExp, cellExp), convertExp);
    var bindExp = Expression.Assign(
        Expression.Property(targetExp, property), cellValueReadExp);
    exps.Add(bindExp);
}

This does the conditional check this way:

value = reader[index];
property = value == DBNull.Value ? default(T): value;
nawfal
  • 70,104
  • 56
  • 326
  • 368
  • 1
    Caveat: The `Convert` expression merely puts a cast operator to cast the boxed type to the type we specify (ie, our property type). It will explode if the cast is not to its original type. For instance if underlying db type is `byte` and property type is `int`, the cast fails. In such cases, more powerful conversion techniques should be used, like those methods in `System.Convert` class (which will be slightly slower of course). This is an issue because of the impedance mismatch between db types and class types. But if programmers can ensure that, this should work fine. – nawfal Dec 11 '13 at 18:03
  • 1
    Other than that, I have not tested this extensively, may be I missed something. I will update it once some bug is pointed out. – nawfal Dec 11 '13 at 18:04
  • 1
    I have not tested extensively yet either, but my initial test showed that it indeed worked the way I expected. I am not sure how concerned I am about the casting you mention above... I expect that any other developer using this will type their properties properly :) – Kevin Dec 11 '13 at 19:09
  • I spoke too soon. Is what you are stating above about the typing, do you mean that the properties that I am mapping the columns to will need to be typed to the SqlDataType? I am trying to run a query now, with 4 columns... 1 VarChar(10), 2 Floats, 1 VarChar(255), and I am getting `Specified cast is not valid errors` across each column->property map – Kevin Dec 12 '13 at 14:47
  • 1
    @o7thWebDesign kind of. I think you got me. In your case the problem most probably will be with the Float type. Are you mapping it to Int32 in code? – nawfal Dec 12 '13 at 14:54
  • nah... I got it too... I was trying to map it to the `float` c# type, however, searching around MSDN, I found that it is supposed to be mapped to `double`. Once I changed it over to that, it worked... I think to avoid confusion, I am going to have to put some kind of SqlType to CLR type conversion process in this as well... and force the users to type using the new way instead of the clr way... like a "Type Inheritance" or something... anyways... thanks for this! – Kevin Dec 12 '13 at 14:55
  • @o7thWebDesign these things really annoy. But I dont think one should go that far. One thing you can do is check the data type like `reader.GetFieldType` and then cast to that type using `Expression.Convert` and do another cast to the type of your property type again using convert expression. I will update the answer, may be tomorrow. – nawfal Dec 12 '13 at 15:00
  • I appreciate it, but not necessary. I'll have it in the instructions that the end-user should use the built-in type map I'm about to put in, instead of the CLR :) – Kevin Dec 12 '13 at 15:01
  • @o7thWebDesign that will be too much digression from convention. We're better off dealing with normal things. It's much easier and conventional to do what I have in mind than yours which is reinventing a whole lot of things :) – nawfal Dec 12 '13 at 15:04
2

This is one of the most annoying problems in dealing with datasets in general.

The way I normally get around it is to convert the DBNull value to something more useful, like an actual null or even a blank string in some cases. This can be done in a number of ways, but just recently I've taken to using extension methods.

public static T? GetValueOrNull<T>(this object value) where T : struct
        {
            return value == null || value == DBNull.Value ? (T?) null : (T) Convert.ChangeType(value, typeof (T));
        }

A handy extension method for nullable types, so for example:

int? myInt = DataSet.Tables[0].Rows[0]["DBNullInt"].GetValueOrNull<int>();

Or a more generic one to just convert a DBNull in to a null:

public static object GetValueOrNull(this object value)
        {
            return value == DBNull.Value ? null : value;
        }

string myString DataSet.Tables[0].Rows[0]["DBNullString"].GetValueOrNull();

You'll then get a null string, rather than trying to put a DBNull in to a string.

Hopefully that may help you a little.

huMpty duMpty
  • 14,346
  • 14
  • 60
  • 99
Pete Ebdon
  • 236
  • 2
  • 6
  • Thanks for the edit. I think I got it right after you and it wouldn't let me update it. :) – Pete Ebdon Dec 06 '13 at 15:39
  • 1
    that is definately a good idea, and under normal mapping circumstances I would definately do it like that, but in this case where it's genericly creating the map, it makes it a bit more difficult... I guess, really I just need to be pointed out to where the field data -> strong type occurs, I can probably figure it out... I just don't know where it happens due to lack of C# understanding LOL – Kevin Dec 06 '13 at 15:41
  • 1
    At a quick look, without creating it and debugging it myself (which I'd do if I wasn't in the middle of something) your dataReader is reading and your converter is doing the converting. What part of the code throws the conversion error? – Pete Ebdon Dec 06 '13 at 16:04
  • @PeteEbdon sorry I didn't get back to you, I put it in the question. – Kevin Dec 11 '13 at 17:20
0

As I come across this problem recently both

Expression.TypeIs(propertyExp,typeof(DBNull));

and

Expression.Equal(propertyExp,Expression.Constant(DBNull.Value));

didn't work for me as they did increase memory allocation (which is my primary concern in this case)

here is the benchmark for both mapper approach compare to Dapper on 10K rows query.

TypeIs enter image description here

and Equal enter image description here

so to fix this problem it came out that an IDataRecord is able to call "IsDBNull" to check whether the column in current reader is DBNull or not

enter image description here

and can be write as expression like

var isReaderDbNull = Expression.Call(paramExp, "IsDBNull", null, readerIndex);

finally, I end up with this solution

enter image description here

and now the performance is acceptable again.

Deszolate_C
  • 61
  • 1
  • 8