MrSoundless
MrSoundless

Reputation: 1384

InvalidCastException while using implicit keyword

I have a 'model' which I'm trying to map query results to, but it keeps failing for some reason. Here is some more info:

The exception happens here (everything but the select part of the query is different in real life):

var query = @"
        SELECT 
            Id,
            PublicationDate,  
            Title,  
            IntroText,  
            BodyText,  
            IsReadByTarget,
            IsRequired
        FROM Notifications
        WHERE 
            CategoryId = @categoryId
        ";
var parameters = new Dictionary<string, object> {
    { "@categoryId", AppSettings.NotificationCategoryId },
};

var notifications = SqlHelper.GetList<Notification>(_connectionString, query, parameters);

SqlHelper is a small helper class that does all the mapping. Notification is the model I'm mapping to. This is what it looks like:

public class Notification
{
    public string Id { get; set; }

    public Time PublicationDate { get; set; }

    public string Title { get; set; }
    public string IntroText { get; set; }
    public string BodyText { get; set; }

    public string ActiveText
    {
        get
        {
            return string.IsNullOrEmpty(IntroText) ? BodyText : IntroText;
        }
    }

    public Notifiable Target { get; set; }
    public bool IsReadByTarget { get; set; }
    public bool IsRequired { get; set; }
}

Time is also a custom class. It basically holds a date + time (just like datetime but much smaller). It's only used for communication not for calculations or whatever:

public class Time
{
    public int Year { get; set; }
    public int Month { get; set; }
    public int Day { get; set; }
    public int Hour { get; set; }
    public int Minute { get; set; }
    public int Second { get; set; }

    public Time()
        : this(DateTime.Now)
    {
    }
    public Time(DateTime time)
    {
        Year = time.Year;
        Month = time.Month;
        Day = time.Day;
        Hour = time.Hour;
        Minute = time.Minute;
        Second = time.Second;
    }

    public static implicit operator DateTime(Time time)
    {
        return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second);
    }
    public static implicit operator Time(DateTime dateTime)
    {
        return new Time(dateTime);
    }
}

So this is also where the magic starts. As you can see, it should silently convert from DateTime to Time and from Time to DateTime. This works fine in normal cases. So doing something like...

Time myTime = DateTime.Now;

...works fine.

But in my case, I get:

Invalid cast from 'System.DateTime' to 'MyNamespace.Time'.

public static List<T> GetList<T>(string connectionString, string query, Dictionary<string, object> parameters) where T : class, new()
{
    var data = new List<T>();

    using (var conn = new SqlConnection(connectionString))
    {
        conn.Open();
        using (var command = conn.CreateCommand())
        {
            command.CommandText = query;

            if (parameters != null)
            {
                foreach (var parameter in parameters)
                {
                    command.Parameters.AddWithValue(parameter.Key, parameter.Value);
                }
            }

            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    var item = Read<T>(reader);
                    data.Add(item);
                }
            }
        }
    }

    return data;
}

public static T Read<T>(SqlDataReader reader) where T : new()
{
    var item = new T();
    var properties = typeof(T).GetProperties();
    foreach (var propertyInfo in properties)
    {
        if (!reader.HasColumn(propertyInfo.Name)) continue;
        var ordinal = reader.GetOrdinal(propertyInfo.Name);

        if (reader.IsDBNull(ordinal)) continue;
        propertyInfo.SetValue(item, Convert.ChangeType(reader[ordinal], propertyInfo.PropertyType), null);
    }

    return item;
}

So basically, it fails when mapping a DateTime column to a Time object while mapping it to a DateTime object works fine. Any help in why this happens, and a reasonable fix, is appreciated.

I know I can create a new model which uses DateTime instead of Time and map to that and then map that model to the model with Time but that's not a reasonable fix.

Upvotes: 0

Views: 233

Answers (1)

Jon Skeet
Jon Skeet

Reputation: 1503439

I would suggest creating a custom dictionary of conversions:

private static readonly Dictionary<Tuple<Type, Type>, Func<object, object>>
    Mappings = new Dictionary<Tuple<Type, Type>, Func<object, object>>
{
    { Tuple.Create(typeof(DateTime), typeof(Time)), x => (Time)(DateTime) x },
    // Any other conversions...
};

Then:

object originalValue = reader[ordinal];
Func<object, object> converter;
if (!Mappings.TryGetValue(Tuple.Create(originalValue.GetType(), 
                                       propertyInfo.PropertyType),
                          out converter)
{
    // Fall back to Convert.ChangeType
    converter = x => Convert.ChangeType(x, propertyInfo.PropertyType);
}
object targetValue = converter(originalValue);
propertyInfo.SetValue(item, targetValue, null);

Upvotes: 4

Related Questions