Search code examples
sql-serversql-server-2008datetimeado.netutc

How is timezone handled in the lifecycle of an ADO.NET + SQL Server DateTime column?


Using SQL Server 2008. This is a really junior question and I could really use some elaborate information, but the information on Google seems to dance around the topic quite a bit and it would be nice if there was some detailed elaboration on how this works...

Let's say I have a datetime column and in ADO.NET I set it to DateTime.UtcNow.

1) Does SQL Server store DateTime.UtcNow accordingly, or does it offset it again based on the timezone of where the server is installed, and then return it offset-reversed when queried? I think I know that the answer is "of course it stores it without offsetting it again" but want to be certain.

So then I query for it and cast it from object to a DateTime after getting it from, say, an IDataReader column. As far as I know, System.DateTime has metadata that internally tracks whether it is a UTC DateTime or it is an offsetted DateTime, which may or may not cause .ToLocalTime() and .ToUniversalTime() to have different behavior depending on this state. So,

2) Does this casted System.DateTime object already know that it is a UTC DateTime instance, or does it assume that it has been offset?


Now let's say I don't use UtcNow, I use DateTime.Now, when performing an ADO.NET INSERT or UPDATE.

3) Does ADO.NET pass the offset to SQL Server and does SQL Server store DateTime.Now with the offset metadata?

So then I query for it and cast it from, say, an IDataReader column to a DateTime.

4) Does this casted System.DateTime object already know that it is an offset time, or does it assume that it is UTC?


Solution

  • Performed some unit tests to answer my own question in all four parts.

    ###1: Does SQL Server store DateTime.UtcNow accordingly, or does it offset it again based on the timezone of where the server is installed, and then return it offset-reversed when queried? Executed this):

    cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
    cmd.Parameters.Add(new SqlParameter("@newval", DateTime.UtcNow));
    cmd.ExecuteNonQuery();
    cmd.CommandText = "SELECT CAST(val as varchar) value FROM testtbl";
    Console.WriteLine(cmd.ExecuteScalar());
    

    The result of this at 1:30 PM local time (-7h, or 8:30 PM UTC) was:

    Jun  3 2010 8:30PM
    

    Then I tried this:

    cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
    cmd.Parameters.Add(new SqlParameter("@newval", DateTime.UtcNow));
    cmd.ExecuteNonQuery();
    Console.WriteLine("change time zone to utc");
    Console.ReadLine();
    cmd.CommandText = "SELECT CAST(val as varchar) value FROM testtbl";
    Console.WriteLine(cmd.ExecuteScalar());
    Console.WriteLine("change time zone back to local");
    

    Executed at 9:25 PM UTC, it returned

    Jun  3 2010 9:25PM
    

    Compare this to DateTime.Now:

    cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
    cmd.Parameters.Add(new SqlParameter("@newval", DateTime.Now));
    cmd.ExecuteNonQuery();
    Console.WriteLine("change time zone to utc");
    Console.ReadLine();
    cmd.CommandText = "SELECT CAST(val as varchar) value FROM testtbl";
    Console.WriteLine(cmd.ExecuteScalar());
    Console.WriteLine("change time zone back to local");
    

    Executed at 3:55 PM (local; -7h), returned:

    Jun  3 2010  3:55PM
    

    ###2: So then I query for it and cast it from object to a DateTime after getting it from, say, an IDataReader column. Does this casted System.DateTime object already know that it is a UTC DateTime instance, or does it assume that it has been offset?

    Neither.

    cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
    cmd.Parameters.Add(new SqlParameter("@newval", DateTime.UtcNow));
    cmd.ExecuteNonQuery();
    cmd.CommandText = "SELECT val value FROM testtbl";
    var retval = (DateTime)cmd.ExecuteScalar();
    Console.WriteLine("Kind: " + retval.Kind);
    Console.WriteLine("UTC: " + retval.ToUniversalTime().ToString());
    Console.WriteLine("Local: " + retval.ToLocalTime().ToString());
    

    The result of this (executed at 1:58 PM local time) was:

    Kind: Unspecified
    UTC: 6/4/2010 3:58:42 AM
    Local: 6/3/2010 1:58:42 PM
    

    That is, .ToUniversalTime() ended up offsetting from local time to UTC time not once but twice (??), and .ToLocalTime() ended up not offsetting at all.

    ###3: Does ADO.NET pass the offset to SQL Server and does SQL Server store DateTime.Now with the offset metadata?

    Without performing any unit tests, the answer is already known to be "only with DateTimeOffset" SQL type. SQL's datetime does not do offsets.

    ###4: Does this casted System.DateTime object already know that it is an offset time, or does it assume that it is UTC?

    Neither. SQL's DateTimeOffset type is returned as a .NET DateTimeOffset struct.

    The following executed at 3:31 PM local time where column offval is a datetimeoffset SQL type,

    cmd.CommandText = "INSERT INTO testtbl (offval) VALUES (@newval)";
    cmd.Parameters.Add(new SqlParameter("@newval", DateTime.Now));
    cmd.ExecuteNonQuery();
    cmd.CommandText = "SELECT offval value FROM testtbl";
    object retvalobj = cmd.ExecuteScalar();
    Console.WriteLine("Type: " + retvalobj.GetType().Name);
    var retval = (DateTimeOffset)retvalobj;
    Console.WriteLine("ToString(): " + retval.ToString());
    Console.WriteLine("UTC: " + retval.ToUniversalTime().ToString());
    Console.WriteLine("Local: " + retval.ToLocalTime().ToString());
    

    This resulted in:

    Type: DateTimeOffset
    ToString(): 6/3/2010 3:31:47 PM +00:00
    UTC: 6/3/2010 3:31:47 PM +00:00
    Local: 6/3/2010 8:31:47 AM -07:00
    

    A surprising disparity.


    Going back and executing the test for question #1 above using DateTime.Now instead of DateTime.UtcNow, I validated that ADO.NET does NOT convert to universal time before storing to the database.

    That is, this executed at 3:27 PM local time (-7h):

     cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
     cmd.Parameters.Add(new SqlParameter("@newval", DateTime.Now));
     cmd.ExecuteNonQuery();
     Console.WriteLine("change time zone to utc");
     Console.ReadLine();
     cmd.CommandText = "SELECT CAST(val as varchar) value FROM testtbl";
     Console.WriteLine(cmd.ExecuteScalar());
     Console.WriteLine("change time zone back to local");
    

    .. returned ..

    Jun  3 2010  3:27PM
    

    Executing this at 3:17 PM local time:

    cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
    cmd.Parameters.Add(new SqlParameter("@newval", DateTime.UtcNow));
    cmd.ExecuteNonQuery();
    cmd.CommandText = "SELECT val FROM testtbl";
    var result = (DateTime)cmd.ExecuteScalar();
    Console.WriteLine("Kind: " + result.Kind);
    Console.WriteLine("ToString(): " + result.ToString());
    Console.WriteLine("Add 1 minute, is greater than UtcNow? "
     + (result.AddMinutes(1) > DateTime.UtcNow).ToString());
    Console.WriteLine("Add 1 minute, is greater than Now? "
     + (result.AddMinutes(1) > DateTime.Now).ToString());
    Console.WriteLine("Add 1 minute, is less than UtcNow? "
     + (result.AddMinutes(1) < DateTime.UtcNow).ToString());
    Console.WriteLine("Add 1 minute, is less than Now? "
     + (result.AddMinutes(1) < DateTime.Now).ToString());
    Console.WriteLine("Subtract 1 minute, is greater than UtcNow? "
     + (result.AddMinutes(-1) > DateTime.UtcNow).ToString());
    Console.WriteLine("Subtract 1 minute, is greater than Now? "
     + (result.AddMinutes(-1) > DateTime.Now).ToString());
    Console.WriteLine("Subtract 1 minute, is less than UtcNow? "
     + (result.AddMinutes(-1) < DateTime.UtcNow).ToString());
    Console.WriteLine("Subtract 1 minute, is less than Now? "
     + (result.AddMinutes(-1) < DateTime.Now).ToString());
    

    Resulted in:

    Kind: Unspecified
    ToString(): 6/3/2010 10:17:05 PM
    Add 1 minute, is greater than UtcNow? True
    Add 1 minute, is greater than Now? True
    Add 1 minute, is less than UtcNow? False
    Add 1 minute, is less than Now? False
    Subtract 1 minute, is greater than UtcNow? False
    Subtract 1 minute, is greater than Now? True
    Subtract 1 minute, is less than UtcNow? True
    Subtract 1 minute, is less than Now? False
    

    Compare this to DateTime.Now:

    cmd.CommandText = "INSERT INTO testtbl (val) VALUES (@newval)";
    cmd.Parameters.Add(new SqlParameter("@newval", DateTime.Now));
    cmd.ExecuteNonQuery();
    cmd.CommandText = "SELECT val FROM testtbl";
    var result = (DateTime)cmd.ExecuteScalar();
    Console.WriteLine("Kind: " + result.Kind);
    Console.WriteLine("ToString(): " + result.ToString());
    Console.WriteLine("Add 1 minute, is greater than UtcNow? "
     + (result.AddMinutes(1) > DateTime.UtcNow).ToString());
    Console.WriteLine("Add 1 minute, is greater than Now? "
     + (result.AddMinutes(1) > DateTime.Now).ToString());
    Console.WriteLine("Add 1 minute, is less than UtcNow? "
     + (result.AddMinutes(1) < DateTime.UtcNow).ToString());
    Console.WriteLine("Add 1 minute, is less than Now? "
     + (result.AddMinutes(1) < DateTime.Now).ToString());
    Console.WriteLine("Subtract 1 minute, is greater than UtcNow? "
     + (result.AddMinutes(-1) > DateTime.UtcNow).ToString());
    Console.WriteLine("Subtract 1 minute, is greater than Now? "
     + (result.AddMinutes(-1) > DateTime.Now).ToString());
    Console.WriteLine("Subtract 1 minute, is less than UtcNow? "
     + (result.AddMinutes(-1) < DateTime.UtcNow).ToString());
    Console.WriteLine("Subtract 1 minute, is less than Now? "
     + (result.AddMinutes(-1) < DateTime.Now).ToString());
    

    Executed at 3:58 PM (local, -7h):

    Kind: Unspecified
    ToString(): 6/3/2010 3:59:26 PM
    Add 1 minute, is greater than UtcNow? False
    Add 1 minute, is greater than Now? True
    Add 1 minute, is less than UtcNow? True
    Add 1 minute, is less than Now? False
    Subtract 1 minute, is greater than UtcNow? False
    Subtract 1 minute, is greater than Now? False
    Subtract 1 minute, is less than UtcNow? True
    Subtract 1 minute, is less than Now? True