4
\$\begingroup\$

I've got a system and every object in my system has a base class with property: DateTime DateCreated

In the constructor for the base class I set the DateCreated as below:

public EntityBase()
{
    Active = true;
    DateCreated = DateTime.UtcNow;
}

This has been fine but now I'm exposing my data as json via a web service call. As the date are UTC I want the format to have the Z suffix as below:

2016-08-01T10:39:18Z

The DateCreated in each object has Kind set to Utc because I'm setting it using DateTime.UtcNow. However, as I load the data from the database the Kind remains as Unspecified (I assumed that the UTC part would be saved to the database but it appears not). When I return the dates via the web service function I get it without the Z suffix so the calling application doesn't know it is UTC:

2016-08-01T10:39:18

It seems weird to me that the UTC Kind of the DateTime object isn't saved to the database - I thought it would be. The datatype is datetime2(7).

To get the date returned as required I've changed the property in the base class to this:

private DateTime _DateCreated;
[DisplayName("Date Created")]
public DateTime DateCreated
{
    get { return _DateCreated; }
    set
    {
        _DateCreated = value;
        _DateCreated = DateTime.SpecifyKind(_DateCreated, DateTimeKind.Utc);//ensure this is set
    }
}

This does exactly what I need but seems kind of hacky - Is this really the best way?

After Googling someone suggests that using DateTimeOffset would be more appropriate. I'm not convinced? This would have me questioning the use of DateTime in any scenario - and always using DateTimeOffset!?!

\$\endgroup\$
  • 1
    \$\begingroup\$ In your setter, you should only use SpecifyKind if the value.DateTimeKind is Unspecified. In other words, if the value passed in is Utc or Local, then you should honor it. \$\endgroup\$ – Rick Davin Mar 13 '17 at 16:11
  • \$\begingroup\$ It's only ever set by the constructor or the database so I know it's always a UTC date but I get your point. \$\endgroup\$ – Percy Mar 13 '17 at 16:23
  • \$\begingroup\$ Do you know why the web service omits the trailing "Z"? Do you try to output with DateTime.ToString("O") aka the round-trip specifier to produce the ISO 8601 compliant time string? \$\endgroup\$ – Rick Davin Mar 13 '17 at 17:20
3
\$\begingroup\$
        _DateCreated = value;
        _DateCreated = DateTime.SpecifyKind(_DateCreated, DateTimeKind.Utc);//ensure this is set

This creates a race condition: another thread can access _DateCreated in between the two assignments and get a non-UTC value. If you're going to do it at this level it should be

        _DateCreated = DateTime.SpecifyKind(value, DateTimeKind.Utc);

That leaves the question of whether it should be done at this level. You haven't said how you're getting the date from the database. Can that be configured?


This would have me questioning the use of DateTime in any scenario - and always using DateTimeOffset!?!

Yes. DateTime is one of those mistakes that everyone recognises is a mistake, but is so deeply embedded in so many libraries and tools that it's a pain in the neck to eradicate.

\$\endgroup\$
  • \$\begingroup\$ Good point about doing it in one line. I'll do that. Should it be done at this level? - I'm not entirely sure to be honest - I'm using EF and have a set of service classes, so one example of retrieving the data is List<Label> Labels = _dbContext.Labels.Where(p => p.ApplicationId == applicationId).OrderBy(p => p.TextCode).ToList(); \$\endgroup\$ – Percy Mar 13 '17 at 16:43
  • \$\begingroup\$ Just thinking about it - perhaps it would be better to do it in the services? as I know the date retrieved from the database is UTC? Maybe that's what your hinting at??? It would be more work as I'd have to do it every time I wrote a new service function but technically more correct??? \$\endgroup\$ – Percy Mar 13 '17 at 17:10
  • 1
    \$\begingroup\$ @Rick, I was more hinting at changing the configuration for the ORM. stackoverflow.com/a/38000428/573420 is the kind of thing I had in mind, although it's more heavyweight than I was hoping would be necessary, \$\endgroup\$ – Peter Taylor Mar 13 '17 at 17:14
2
\$\begingroup\$

I'm exposing my data as json via a web service call.

You didn't say which json serializer you use but I cannot imagine you use anything else then JSON.NET. With it you can specify the date-time serializer so you don't have to implement any hacks.

string javascriptJson = JsonConvert.SerializeObject(
    entry, 
    new IsoDateTimeConverter()
);

IsoDateTimeConverter serializes a DateTime to an ISO 8601 formatted string: "2009-02-15T00:00:00Z"

See Serializing Dates in JSON for more information.

\$\endgroup\$
  • \$\begingroup\$ Interesting - But if I had other dates in the object that weren't UTC it would serialize those as ISO 8601 also wouldn't it - could be dangerous. \$\endgroup\$ – Percy Mar 14 '17 at 8:44
  • \$\begingroup\$ Although handling the problem at the JSON serialisation stage is a possible approach, the answer doesn't currently give the correct way to do it. If OP gets dates which look like 2016-08-01T10:39:18 then (assuming he's using JSON.NET) he's already using IsoDateTimeConverter (unsurprisingly, since it's the default). What he would need to do to force UTC is to set JsonSerializerSettings.DateTimeZoneHandling to DateTimeZoneHandling.Utc. \$\endgroup\$ – Peter Taylor Mar 14 '17 at 9:37
1
\$\begingroup\$

While you currently feel every DateTime coming to you will be in UTC, even if its DateTimeKind is not Utc, your code would be less prone to breakage from future changes, such as maybe a DateTime could be passed as Local.

Also, naming convention would be that _DateCreated should be _dateCreated.

private DateTime _dateCreated;
[DisplayName("Date Created")]
public DateTime DateCreated
{
    get { return _dateCreated; }
    set
    {
        _dateCreated = value.DateTimeKind == DateTimeKind.Unspecified
                     ? DateTime.SpecifyKind(value, DateTimeKind.Utc)
                     : value.ToUniversalTime(); //ensure this is set
    }
}
\$\endgroup\$
0
\$\begingroup\$

I use this Extension method to ""fallback" to UTC so I can properly convert between exchange time, server time and clients local time.

public static DateTime AsUTC(this DateTime dt)
 {
       switch (dt.Kind)
       {
            case DateTimeKind.Unspecified:
                return new DateTime(dt.Ticks, DateTimeKind.Utc);
            case DateTimeKind.Utc:
                return dt;
            case DateTimeKind.Local:
                return dt.ToUniversalTime();
             default:
                throw new NotSupportedException($"The provided {dt.Kind} did not exist when delivering this model, please update to latest version to support this date time type");                    
       }
}

I have noticed that sometime the DateTime.SpecifyKind is not working as expected on a given Service pack where creat a new date time always does. The method Specify Kind is just a wrapper around the create this method anyway (when it works) enter image description here

We throw the error incase the framework updated and there is a not implemented method than to just leave ambiguity in a default implementation.

When reading data from the database call

var myUtcDate= myDate.AsUTC();

As the it's a value type one doesn't update the instance, you'd have to write it in a new variable.

alternatively you can do something like this when reading:

someClass.MyDateValue = reader.GetDateTime("FieldName").AsUTC();
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.