May 28, 2014

Time Is Eternal – Using NodaTime in WebAPI

Post by: Steven Nelson

I’m working on a project currently that deals with a lot of date and time issues. We’re capturing events that people perform during the day. The complication is that a person might start out their day in Pennsylvania, but travel to Illinois during the course of the day. Pennsylvania is in the Eastern time zone, and Illinois is in Central time. The person starts their day starts at 9:00AM in Pennsylvania (Eastern Standard Time). They end their day at 5:00PM in Illinois (Central Standard Time). The duration of their day is 9 hours.

The standard DateTime data type in .NET is not setup properly to deal with time zones. The DateTime type does not allow you to define a date and time in a specific time zone. This post builds on what Ian FitzGerald previously discussed in his post entitled Handling Complex Dates Without Pulling Your Hair Out. Ian and I work together on the same project. We have standardized on using Noda Time for our Date and Time data types.

The Noda Time package is available as open source via nuget. You can read more about Noda Time at nodatime.org.

The purpose of this post is to show you how to use the Noda Time types in a webAPI project.

Suppose we have Timecard entity that we intend to work with in the webAPI. The Timecard entity looks like this:


public class Timecard
{
 
    public LocalDate ReportingDate { get; set; }
    public ZonedDateTime PunchIn { get; set; }
    public ZonedDateTime PunchOut { get; set; }
 
    public Duration Duration
    {
        get
        {
            var d = this.PunchOut.ToInstant() - this.PunchIn.ToInstant();
            return d;
        }
    }
 
}

This object demonstrates a couple of the Noda types. The LocalDate type represents a date-only type that is independent of a timezone. The ZonedDateTime represents an unambiguous datetime within a timezone. The Duration type represents a fixed amount of time between two global times.

In order to serialize the Noda types in JSON, there is are two open source packages that adds the Node types to the JSON.net serialization pipeline. Add the two nuget packages “NodaTime” and “NodaTime.Serialization.JsonNet” to your solution.

The TimecardController in the WebAPI project exposes a method to fetch all of the timecards. The method looks like this:


public IEnumerable GetAll()
{
    var easternTimezone = DateTimeZoneProviders.Tzdb.GetZoneOrNull("America/New_York");
    var centralTimezone = DateTimeZoneProviders.Tzdb.GetZoneOrNull("America/Chicago");
 
    var startOfDay = new LocalDateTime(2014, 04, 11, 9, 0, 0);
    var endOfDay = new LocalDateTime(2014, 04, 11, 17, 00, 0);
 
    var timecardWestbound = new Timecard
    {
        ReportingDate = new LocalDate(2014, 04, 11),
        PunchIn = startOfDay.InZoneLeniently(easternTimezone),
        PunchOut = endOfDay.InZoneLeniently(centralTimezone)
    };
 
    var timecardEastbound = new Timecard
    {
        ReportingDate = new LocalDate(2014, 04, 11),
        PunchIn = startOfDay.InZoneLeniently(centralTimezone),
        PunchOut = endOfDay.InZoneLeniently(easternTimezone)
    };
 
    return new List {timecardEastbound, timecardWestbound};
}

This method demonstrates how to define the ZonedDateTimes for the timecard’s PunchIn and PunchOut properties. Notice that it starts with a LocalDateTime. The LocalDateTime is a simple Date and Time value. In this case, the startOfDay is defined as “04/11/2014 09:00:00 AM”. The timecardWestbound.PunchIn property assigns the startOfDay to the easternTimezone. It does this using a “lenient” algorithm. This is necessary because daylight saving time defines a couple of ambiguous timestamps. In the spring, the time 1:30AM is undefined on the morning that starts daylight savings time. So, if you were to try and assign the value “3/9/2014 01:30 AM” to any time zone that observes daylight savings time, it would throw an exception because that time does not exist. The lenient algorithm automatically would return “03/9/2014 02:00 AM”, rather than throwing an exception. There is a method called InZoneStrictly that can be used instead of InZoneLeniently if you need absolute validation of the timestamps.

A similar, but different, type of problem happens in the fall with daylight savings time. In the fall, the time 1:30 AM happens twice. The timestamp of “11/2/2014 1:30 AM” occurs twice. This is because the clock moves forward from midnight. It is valid to hit 1:30AM, and then the clock continues. When the clock hits 2:00AM, the clock instantly moves back to 1:00AM and starts counting forward again. So the value of 1:30AM occurs twice on that morning. In fact, there will actually be 25 hours on that day, but that’s a different problem! The lenient algorithm for creating ZonedDateTime assumes that the 1:30AM is the second occurrence of the time. In other words, the time 11/2/2014 1:30AM occurs 2 hours and 30 minutes after midnight.

In order to use the Noda types with JSON, you’ll need to extend the JSON.net pipeline so that it understands how to serialize the Noda types. In order to do this, you should register the JsonFormatters as follows:


public static void Register(HttpConfiguration config)
{
    config.Formatters.JsonFormatter.SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
}

The JsonSerializerSettings is extended with the method ConfigureForNodaTime via the NodaTime.Serialization.JsonNet assembly. This create a very nice format to the serialized Noda types. The JSON for the timecard object shown this example will look like this:

{“ReportingDate”:”2014-04-11″,”PunchIn”:”2014-04-11T09:00:00-04 America/New_York”,”PunchOut”:”2014-04-11T17:00:00-05 America/Chicago”}

The TimecardController’s method to handle posting timecards becomes trivial


 [Route("submit")]
 [HttpPost]
 public HttpResponseMessage Submit(List timecards)
 {
     if(timecards == null || !timecards.Any())
         return new HttpResponseMessage(HttpStatusCode.BadRequest);
 
     return new HttpResponseMessage(HttpStatusCode.OK);
 }

Here is an example of how an HttpClient can be used for programmatically posting to the TimecardController.


var timecards = new List { timecardWestbound };
 
using (var client = new HttpClient())
{
    client.BaseAddress = new Uri("http://localhost:30500/");
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 
    var settings = new JsonSerializerSettings();
    settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
 
    string json = JsonConvert.SerializeObject(timecards, settings);
 
    var content = new StringContent(json);
    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
 
    var response = client.PostAsync("api/timecard/submit", content).Result;
    Assert.IsTrue(response.StatusCode == HttpStatusCode.OK);
}

Hopefully this post helps get you up to speed using Noda Time in a WebAPI project. I have found Noda Time to be a big improvement over the standard .NET DateTime type. Hopefully you will find that Noda Time will help you accurately model how time is characterized by your app.

See all of our Application Modernization capabilities

Relevant Insights

New Site Showcases Our Growth Into a Full-Service IT Consultancy

Over the past year, Core BTS has evolved. Simply put, we’ve amassed greater scale and expertise that, in combination with...

5 Steps to Reduce Your Ransomware Risk

As the recent ransomware attack on the U.S.’s second-largest meat producer, JBS, made clear, cyberattacks on critical infrastructure can cause...

How to Unlock the Organizational Value of Digital Transformation

As organizations look to stay competitive in today's dynamic and unpredictable marketplace, a trend has re-emerged that is ushering us...
X