3

I have an expression tree function from a previous SO question. It basically allows the conversion of a data row into a specific class.

This code works fine, unless you're dealing with data types that can be bigger or smaller (eg. Int32/Int64).

The code throws an invalid cast exception when going from an Int64 to an Int32 when the value would fit in an Int32 (eg. numbers in the 3000).

Should I?

  1. Attempt to fix this in the code? (If so, any pointers?)
  2. Leave the code as it is.

    private Func<SqlDataReader, T> getExpressionDelegate<T>()
    {
        // hang on to row[string] property 
        var indexerProperty = typeof(SqlDataReader).GetProperty("Item", new[] { typeof(string) });
    
        // list of statements in our dynamic method
        var statements = new List<Expression>();
    
        // store instance for setting of properties
        ParameterExpression instanceParameter = Expression.Variable(typeof(T));
        ParameterExpression sqlDataReaderParameter = Expression.Parameter(typeof(SqlDataReader));
    
        // create and assign new T to variable: var instance = new T();
        BinaryExpression createInstance = Expression.Assign(instanceParameter, Expression.New(typeof(T)));
        statements.Add(createInstance);
    
        foreach (var property in typeof(T).GetProperties())
        {
    
            // instance.MyProperty
            MemberExpression getProperty = Expression.Property(instanceParameter, property);
    
            // row[property] -- NOTE: this assumes column names are the same as PropertyInfo names on T
            IndexExpression readValue = Expression.MakeIndex(sqlDataReaderParameter, indexerProperty, new[] { Expression.Constant(property.Name) });
    
            // instance.MyProperty = row[property]
            BinaryExpression assignProperty = Expression.Assign(getProperty, Expression.Convert(readValue, property.PropertyType));
            statements.Add(assignProperty);
        }
    
        var returnStatement = instanceParameter;
        statements.Add(returnStatement);
    
        var body = Expression.Block(instanceParameter.Type, new[] { instanceParameter }, statements.ToArray());
    
        var lambda = Expression.Lambda<Func<SqlDataReader, T>>(body, sqlDataReaderParameter);
    
        // cache me!
        return lambda.Compile();
    }
    

Update:

I have now given up and decided it is not worth it. From the comments below, I got as far as:

            if (readValue.Type != property.PropertyType)
            {
                BinaryExpression assignProperty = Expression.Assign(getProperty, Expression.Convert(Expression.Call(property.PropertyType, "Parse", null, new Expression[] { Expression.ConvertChecked(readValue, typeof(string)) }), property.PropertyType));
                statements.Add(assignProperty);
            }
            else
            {
                // instance.MyProperty = row[property]
                BinaryExpression assignProperty = Expression.Assign(getProperty, Expression.Convert(readValue, property.PropertyType));
                statements.Add(assignProperty);
            }

I don't think I was too far off, feel free to finish it and post the answer if you figure it out :)

Stuart Blackler
  • 3,732
  • 5
  • 35
  • 60
  • I dont think you should be really worrying about it. In case you want this feature for integral types, rely on `System.Convert` class. See a [similar question](http://stackoverflow.com/questions/19841120/listt-property-binding-to-dbdatareader-issue?lq=1) here for a lil more fleshed out solution. – nawfal Dec 11 '13 at 19:23

2 Answers2

3

You could try to fix it by "convert checked" before assigning i.e. using Expression.ConvertChecked on the value instead of Expression.Convert .

Couldn't try it right now but this should take care of the case you describe...

EDIT - as per comment this could be a boxing issue:

In this case you could try using Expression.TypeAs or Expression.Unbox for the conversion or use Expression.Call for calling a method to do the conversion... an example for using Call can be found at http://msdn.microsoft.com/en-us/library/bb349020.aspx

Yahia
  • 69,653
  • 9
  • 115
  • 144
  • 1
    I don't think that this is an issue with overflow etc. It sounds more like an unboxing issue to me: a boxed `Int64` can't be unboxed as an `Int32` etc; boxed values must be unboxed to exactly the same type. – LukeH Sep 05 '11 at 22:01
  • Unfortunately this didn't work. I tried `.TypeAs` as well. I have a feeling I will need to add more check's into this. It's not the end of the world, but I would like to keep this as fast as possible. – Stuart Blackler Sep 05 '11 at 22:15
  • @SBlackler - did you try `.Unbox` ? – Yahia Sep 05 '11 at 22:16
  • Is there a way of calling the native Parse method for a given type? eg. `if type 1 == type2 then normal else call native parse` – Stuart Blackler Sep 05 '11 at 22:20
  • you could try using `Expression.Call` - see http://msdn.microsoft.com/en-us/library/bb343914.aspx ... – Yahia Sep 05 '11 at 22:22
  • @Yahia, I've given up on that idea now, seem's to be more trouble than it's worth? see my updated post above for how far i had got. Thanks :) – Stuart Blackler Sep 05 '11 at 23:06
1

What you're trying to build is actually much more complicated if you want to support 100% of the primitives in .NET and SQL.

If you don't care about some of the edge cases (nullable types, enums, byte arrays, etc), two tips to get you 90% there:

Don't use the indexer on IDataRecord, it returns an object and the boxing/unboxing will kill performance. Instead, notice that IDataRecord has Get[typeName] methods on it. These exist for all .NET primitive types (note: it's GetFloat, not GetSingle, huge annoyance).

You can use IDataRecord.GetFieldType to figure out which Get method you need to call for a given column. Once you have that, you can use Expression.Convert to coerce the DB column type to the target property's type (if they're different). This will fail for some of the edge cases I listed above, for those you need custom logic.

Steve
  • 438
  • 3
  • 10
  • +1 for info on `Get[typeName]` methods, indeed they are faster, but in real world you can't use them considering it doesn't handle DbNulls, so a generic handling is quite impossible. – nawfal Dec 12 '13 at 16:08
  • You can use IDataRecord.IsDBNull to figure out if the column for the current row is null or not before you read it. – Steve Dec 14 '13 at 19:46
  • Steve, in that case its performance becomes similar to other overloads, more or less. – nawfal Dec 15 '13 at 03:20
  • Do you have benchmark results to back that up? Any time I've done tests, IsDBNull + Get is significantly more efficient when also taking into consideration GC time. – Steve Dec 16 '13 at 00:55
  • Steve I would let you know after testing with various providers. – nawfal Dec 16 '13 at 01:02