Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

'Null object cannot be converted to a value type.' when using nullable type on interface #933

Closed
JaseRandall opened this issue Feb 1, 2018 · 4 comments

Comments

@JaseRandall
Copy link
Contributor

JaseRandall commented Feb 1, 2018

Using Dapper.Contrib (for the change tracking) and SQLite.

I have an interface and class... here's a cut down version:

public interface IExample
{
   public int Id { get; set; }
   DateTime? Dob { get; set; }
}

public class Example : IExample
{
   public int Id { get; set; }
   public DateTime? Dob { get; set; }
}

This works:

public IEnumerable<IExample> GetAll()
{
    return Connection.GetAll<Example>();
}

This does not work:

public IEnumerable<IExample> GetAll()
{
    return Connection.GetAll<IExample>();
}

It throws 'Null object cannot be converted to a value type.' which obviously isn't right since I'm using a nullable type. I've removed all nulls from the database and it made no difference. If I change DateTime? to DateTime then it works. I could work around it by assinging DateTime.MinValue however I'd prefer not to.

Is this a bug in Dapper.Contrib or something I'm doing wrong?

@NickCraver
Copy link
Member

When you're trying to deserialize into IExample, what is the type of the object in the list? When you call .GetType() on an object, what would you expect the result to be?

I ask the above to illustrate why deserializing to an interface (or an abstract class) wouldn't really work...because you can't instantiate it. Does that make sense?

@JaseRandall
Copy link
Contributor Author

JaseRandall commented Feb 2, 2018

I've had more time today to look into this and have worked out why it occurs when I use the interface.

What you've said does make sense however the issue isn't the interface. Dapper.Contrib has code to create an object from an interface:

if (!type.IsInterface()) return connection.Query<T>(sql, null, transaction, commandTimeout: commandTimeout);

var result = connection.Query(sql);
var list = new List<T>();
 foreach (IDictionary<string, object> res in result)
 {
    var obj = ProxyGenerator.GetInterfaceProxy<T>();
    foreach (var property in TypePropertiesCache(type))
    {
        var val = res[property.Name];
        property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null);
     }
     ((IProxy)obj).IsDirty = false;   //reset change tracking and return
     list.Add(obj);
}

I've never used reflection, I am self taught and I don't program professionally however, I think the issue is caused by:
property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null);
which can't set the value of a nullable type.

This link explains why: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/nullable-types/how-to-identify-a-nullable-type

You can also use the classes and methods of the System.Reflection namespace to generate Type objects that represent Nullable types. However, if you try to obtain type information from Nullable variables at runtime by using the GetType method or the is operator, the result is a Type object that represents the underlying type, not the Nullable type itself.

Calling GetType on a Nullable type causes a boxing operation to be performed when the type is implicitly converted to Object. Therefore GetType always returns a Type object that represents the underlying type, not the Nullable type.

Example

Use the following code to determine whether a Type object represents a Nullable type. Remember that this code always returns false if the Type object was returned from a call to GetType, as explained earlier in this topic.

if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) {…}

I've also looked at http://codewut.de/content/how-set-nullable-types-reflection-properties-c-net which suggests a solution. I adapted that for Dapper.Contrib and the line above that failed could be changed to:

if (property.PropertyType.IsGenericType() && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) 
{
     var genericType = property.PropertyType.GetGenericArguments()[0];
     property.SetValue(obj, Convert.ChangeType(val, genericType), null);
}
else
{
     property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null);
}

I haven't tested this yet so I'm not sure if it will work or if it's the best solution.

@JaseRandall
Copy link
Contributor Author

JaseRandall commented Feb 3, 2018

Just further to the last:
I've managed to download the Dapper source code and test the solution.
I found a couple of issues... firstly I had DOB in the database rather than Dob which was giving the null.
I fixed that then had this error:

System.InvalidCastException: 'Invalid cast from 'System.String' to 'System.Nullable`1[[System.DateTime, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]'.'

That's understandable; The date value is stored as a string in SQLite and property.SetValue() can't cast that to nullable.

I changed the original code from Dapper.Contrib:

            foreach (IDictionary<string, object> res in result)
            {
                var obj = ProxyGenerator.GetInterfaceProxy<T>();
                foreach (var property in TypePropertiesCache(type))
                {
                    var val = res[property.Name];
                    property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null);
                }
                ((IProxy)obj).IsDirty = false;   //reset change tracking and return
                list.Add(obj);
            }

to

            foreach (IDictionary<string, object> res in result)
            {
                var obj = ProxyGenerator.GetInterfaceProxy<T>();
                foreach (var property in TypePropertiesCache(type))
                {
                    var val = res[property.Name];
                    if (property.PropertyType.IsGenericType() && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                    {
                        if (val == null) continue;
                        var genericType = property.PropertyType.GetGenericArguments()[0];
                        property.SetValue(obj, Convert.ChangeType(val, genericType), null);
                    }
                    else
                    {
                        property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null);
                    }
                }

                ((IProxy) obj).IsDirty = false; //reset change tracking and return
                list.Add(obj);
            }

and after that change, Dapper.Contrib was able to handle the DateTime? type including whether the value from the database was null or an actual value.

Now I just have to figure out how to do a pull request to submit the change.

NickCraver pushed a commit that referenced this issue Mar 9, 2018
… an interface… #933 (#936)

* Change to GetAll to allow the usage of a nullable type in T when T is an interface.

* Change to GetAll to allow the usage of a nullable type in T when T is an interface: further change to check if val == null and if so then don't try to assign it as a property value.

* Change to Get to allow the usage of a nullable type in T when T is an interface. (effectively the same change as the previous change to GetAll.)

* Change to GetAsync and GetAllAsync to allow the usage of a nullable type in T when T is  an interface.

* Added in UserWithNullableDob and IUserWithNullableDob.

Added in tests for GetAndGetAllWithNullableValues and  GetAsyncAndGetAllAsyncWithNullableValues.

* Added in .ConfigureAwait(false) in the GetAsync Test.
Added comment to identify which issue the test relates to.

* Added NullableDates tables to the test databases.
Adjusted variable names in GetAndGetAllWithNullableValues and GetAsyncAndGetAllAsyncWithNullableValues.

* Changed IUserWithNullableDob to INullableDate.
@JaseRandall
Copy link
Contributor Author

Issue resolved by Pull Request #936.
Closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants