72

I'm using Dapper to map my entities to SQL Server CE. If I save a DateTime with Kind=Utc, when I read it back I get a DateTime with Kind=Unspecified, which leads to all kind of problems.

Example:

var f = new Foo { Id = 42, ModificationDate = DateTime.UtcNow };
Console.WriteLine("{0} ({1})", f.ModificationDate, f.ModificationDate.Kind);
connection.Execute("insert into Foo(Id, ModificationDate) values(@Id, @ModificationDate)", f);
var f2 = connection.Query<Foo>("select * from Foo where Id = @Id", f).Single();
Console.WriteLine("{0} ({1})", f2.ModificationDate, f2.ModificationDate.Kind);

This code gives the following output:

20/09/2012 10:04:16 (Utc)
20/09/2012 10:04:16 (Unspecified)

I know I should be using a DateTimeOffset, but unfortunately SQL CE has no support for this type.

Is there a workaround? Can I tell Dapper to assume that all dates have DateTimeKind.Utc? And more generally, what are my options to customize the mapping?


EDIT: My current workaround is to patch the dates after Dapper has materialized the result, but it kind of smells...

var results = _connection.Query<Foo>(sql, param).Select(PatchDate);

...

static Foo PatchDate(Foo f)
{
    if (f.ModificationDate.Kind == DateTimeKind.Unspecified)
        f.ModificationDate = DateTime.SpecifyKind(f.ModificationDate, DateTimeKind.Utc);
    return f;
}
Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • It's kind of a limitation of the SQL Server in my experience. Assuming it's UTC was what I did. – Peter Ritchie Sep 20 '12 at 14:00
  • 1
    @PeterRitchie, it's a limitation of SQL Server Compact Edition (the "full" SQL Server has a datetimeoffset data type). I could easily work around the issue with vanilla ADO.NET, but my question is more specific to Dapper, which apparently doesn't give me much control over how the result is materialized... – Thomas Levesque Sep 20 '12 at 14:37
  • You could probably call `SpecifyKind` in the property setter. Then it would play nice with most (if not all) ORMs. – default.kramer Sep 20 '12 at 14:55
  • 1
    @ThomasLevesque Well, it's more like a limitation of ADO.NET and `DbType.DateTime` mappings. ADO.NET gives you the `Unspecified`. There's nothing telling SQL Server the column is "UTC", so ADO.NET follows suit. It would be nice to have `DbType.UtcDateTime`; but alas... (and if we did, would CE support it? :) – Peter Ritchie Sep 20 '12 at 15:01
  • 1
    @ThomasLevesque One of the many reasons I avoid relational databases--too many limitations when it comes to OO and applications. – Peter Ritchie Sep 20 '12 at 15:03
  • I can confirm this isn't something we directly support.... frankly, though, most people use `datetime` (not `datetimeoffset`), even on full SQL server, and this isn't a problem most of the time. – Marc Gravell Sep 21 '12 at 05:56
  • @MarcGravell, you're right, I don't know what made me think my problems were caused by that, but I made more tests and actually they're not... so the question is pointless now – Thomas Levesque Sep 21 '12 at 07:53
  • A similar problem occurs using Dapper with SQLite. Items which are persisted as UTC get materialized with the DateTimeKind unspecified and the value showing local time. – Richard J Foster Feb 22 '13 at 20:30

4 Answers4

120

Adding this answer for anyone else who comes looking for a simple fix. This is possible now with the addition of SqlMapper.TypeHandler in Dapper.

Add this class to convert the value from the db to a datetime with the kind specified as UTC.

public class DateTimeHandler : SqlMapper.TypeHandler<DateTime>
{
    public override void SetValue(IDbDataParameter parameter, DateTime value)
    {
        parameter.Value = value;
    }

    public override DateTime Parse(object value)
    {
        return DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc);
    }
}

Then in my Global.asax file of my Web API I add the type handler to dapper.

SqlMapper.AddTypeHandler(new DateTimeHandler());

If you need to ensure you are always inserting dates as UTC, then on the SetValue method you can use:

parameter.Value = DateTime.SpecifyKind(value, DateTimeKind.Utc);
Matt Jenkins
  • 1,245
  • 1
  • 10
  • 7
  • 10
    This is absolutely the correct answer. The one difference I would make is put the `SqlMapper.AddTypeHandler` initialization code in something that is SQL-specific, such a static constructor of the connection factory. That way you have this handler initialized no matter where you make the connection from, rather than only via a web service that remembers to initialize it. – Technetium Jun 06 '17 at 18:39
  • Somehow I never saw this answer... Yes, it's obviously the correct way to do it. Thanks! – Thomas Levesque Apr 12 '18 at 09:26
  • 3
    Does this work? I thought Dapper didn't let you override the mapping for built in types. – jpmc26 Jan 10 '19 at 05:13
  • 1
    Seems that SetValue is indeed never called, so I'm also wondering how this could have worked? – L-Four Dec 10 '19 at 13:18
  • 12
    To make this work: first remove default DateTime handler with SqlMapper.RemoveTypeMap(typeof(DateTime)); and then register yout custom handler with SqlMapper.AddTypeHandler(new DateTimeHandler()); – L-Four Dec 10 '19 at 13:28
28

Looked into the Dapper code. Unless mine was out of date, for value types like datetime (which is mapped to DbType.DateTime), dapper just does a simple cast from the IDataReader object.

Pseudo : yield return (DateTime)IDataReader.GetValue(0);

That's the specific case for Datetime out of a bunch of generic code and lambdas.

AFAIK, SQL datetime never stores the offset / timezone so the kind will always say "Unspecified" on any datetime you store and fetch.

So, to do it cleanly, you could touch dapper internals:

which is a pain as you'd have to touch a big IL generating method (the DataRow Deserializer) and put in an if case for DateTime.

OR

just put a setter on the DateTime props where UTC is an issue (which is kinda against POCO but is relatively sane):

class Foo
{
    private DateTime _modificationDate;
    public DateTime ModificationDate
    {
        get { return _modificationDate; }
        set { _modificationDate = DateTime.SpecifyKind(value, DateTimeKind.Utc); }
    }
    //Ifs optional? since it's always going to be a UTC date, and any DB call will return unspecified anyways
}
Vivek
  • 2,103
  • 17
  • 26
  • Yes, I guess I could do it in the setter. Actually my question is a bit pointless now, because I eventually realized that it wasn't really an issue to have the datetime kind as "Unspecified", but thanks anyway. – Thomas Levesque Jun 14 '13 at 08:29
  • This is a very elegant solution. Thanks! – Chad Sep 09 '13 at 19:01
5

Just wanted to put my full solution here for seamlessly integrating DateTimeOffset / DateTimeOffset? fields/properties with a MySQL 5.7 database (which doesn't support DbType.DateTimeOffset) - based on @matt-jenkins answer above:

public static class DapperExtensions
{
    class DateTimeOffsetTypeHandler : SqlMapper.TypeHandler<DateTimeOffset>
    {
        public override void SetValue(IDbDataParameter parameter, DateTimeOffset value)
        {
            switch (parameter.DbType)
            {
                case DbType.DateTime:
                case DbType.DateTime2:
                case DbType.AnsiString: // Seems to be some MySQL type mapping here
                    parameter.Value = value.UtcDateTime;
                    break;
                case DbType.DateTimeOffset:
                    parameter.Value = value;
                    break;
                default:
                    throw new InvalidOperationException("DateTimeOffset must be assigned to a DbType.DateTime SQL field.");
            }
        }

        public override DateTimeOffset Parse(object value)
        {
            switch (value)
            {
                case DateTime time:
                    return new DateTimeOffset(DateTime.SpecifyKind(time, DateTimeKind.Utc), TimeSpan.Zero);
                case DateTimeOffset dto:
                    return dto;
                default:
                    throw new InvalidOperationException("Must be DateTime or DateTimeOffset object to be mapped.");
            }
        }
    }


    private static int DateTimeOffsetMapperInstalled = 0;

    public static void InstallDateTimeOffsetMapper()
    {
        // Assumes SqlMapper.ResetTypeHandlers() is never called.
        if (Interlocked.CompareExchange(ref DateTimeOffsetMapperInstalled, 1, 0) == 0)
        {
            // First remove the default type map between typeof(DateTimeOffset) => DbType.DateTimeOffset (not valid for MySQL)
            SqlMapper.RemoveTypeMap(typeof(DateTimeOffset));
            SqlMapper.RemoveTypeMap(typeof(DateTimeOffset?));

            // This handles nullable value types automatically e.g. DateTimeOffset?
            SqlMapper.AddTypeHandler(typeof(DateTimeOffset), new DateTimeOffsetTypeHandler());
        }
    }
}
jamespconnor
  • 1,382
  • 14
  • 29
  • I've tried your approach, but somehow I'm continuously getting `InvalidOperationException("DateTimeOffset must be assigned to a DbType.DateTime SQL field.");`. I checked `DbType` prop of `IDbDataParameter` and the value was `AnsiString`. Any thoughts? – managerger May 02 '18 at 11:41
  • Apologies - yes we discovered this also. Add that type to the top switch case and it works. Will update my answer shortly. – jamespconnor May 02 '18 at 11:45
  • Cool! Could you provide your value of `MySqlDbType`? Because I have `MySql.Data.MySqlClient.MySqlDbType.Decimal`. How comes that it is `decimal` while `DbType` is `System.Data.DbType.AnsiString`? Really strange... – managerger May 02 '18 at 11:53
1

If you are using Dapper from source (not nuget), you could tweak the code to always force DateTimeKind of UTC. A more configurable option might be to create a new attribute for DateTime property values that allow you to specify date time kind as a hint to dapper. Dapper could look for DateTime properties with this attribute and when found could use it to specify the DateTime kind during ORM mapping. This might be a nice feature for core dapper as you are not the only one with this issue :)

user2368632
  • 990
  • 1
  • 12
  • 33