avatarNoel Tiangco

Summarize

Date Night: Mastering UTC Dates in C# Like a Pro

Happy New Year! As we step into the new year, it’s a great opportunity to delve into the nuances of date handling in web services, particularly focusing on Coordinated Universal Time (UTC). Given the global nature of modern applications, many APIs now standardize on UTC for DateTime fields.

Understanding UTC in Web Services

When working with APIs transmitting dates in UTC, it’s vital to have a strategy for handling these DateTime values effectively in your application. Let’s walk through a basic scenario with a focus on deserialization and serialization of UTC dates.

Setting Up Your Environment

Start by creating a C# Console application and installing the Newtonsoft.Json NuGet package, which offers robust support for JSON operations including DateTime parsing.

Handling JSON with UTC DateTime

Consider an API that returns a JSON string resembling the following:

{
    "UtcDate": "2019-01-23T01:23:45Z",
    "Name": "Noel",
    "Siblings": 5
}

To handle this, create a class in C# that mirrors the JSON structure:

public class SampleObject
{
    public DateTime UtcDate { get; set; }
    public String Name { get; set; }
    public int Siblings { get; set; }
}

Dealing with Consistent Standard Format

If the API always returns dates in a consistent, standard format, the process remains straightforward. Here’s an example that demonstrates deserializing the JSON and converting the UTC date to local time:

internal class Program
{
  static void Main(string[] args)
  {
    string utcDateString = "2019-01-23T01:23:45Z";
    string sampleJson = 
      $"{{\"UtcDate\": \"{utcDateString}\", \"Name\": \"Noel\", \"Siblings\" : 5}}";

    var sampleObject = JsonConvert.DeserializeObject<SampleObject>(sampleJson);

    TimeZoneInfo localZone = TimeZoneInfo.Local;
    DateTime localDateTime = TimeZoneInfo.ConvertTimeFromUtc(
      sampleObject.UtcDate, localZone);

    Console.WriteLine(
      $"UTC datetime: {sampleObject.UtcDate} [{sampleObject.UtcDate.Kind}]");
    Console.WriteLine($"Local datetime: {localDateTime} [{localDateTime.Kind}]");
  }
}

In my EST timezone, the output reflects the correct conversion:

UTC datetime: 2019-01-23 1:23:45 AM  [Utc]
Local datetime: 2019-01-22 8:23:45 PM  [Local]

The above code works seamlessly for standard date-time formats. The “Z” at the end signifies UTC, setting `DateTime.Kind` to `DateTimeKind.Utc`.

Understanding DateTimeKind

`DateTimeKind` is a property in .NET that indicates whether a DateTime value is based on local time (`DateTimeKind.Local`), UTC (`DateTimeKind.Utc`), or is unspecified (`DateTimeKind.Unspecified`). In scenarios where the API consistently returns UTC, any `Unspecified` DateTime kinds can be safely treated as UTC.

When working with a team, ensure that all team members understand that Unspecified kinds are treated as UTC.

Handling Non-Standard and Multiple Formats

Challenges arise when APIs use non-standard date formats or the DateTime fields vary in format, sometimes even including null or even invalid values if the backing field are string. To address this, you can create a custom `JsonConverter` that can handle multiple formats and gracefully manage null or invalid dates.

For null and invalid values the DateTime value will be set to null.

Adjust the backing SampleObject model, setting UtcDate to a nullable DateTime:

    public class SampleObject
    {
        public DateTime? UtcDate { get; set; }
        public String Name { get; set; }
        public int Siblings { get; set; }
    }

Custom JsonConverter for Multiple Formats

Here’s an example of a custom `JsonConverter` that can parse various date formats and handle null values:

public class MultiFormatDateTimeConverted : JsonConverter
{
  private readonly string[] _dateFormats;
  private readonly DateTime? _defaultValue;
  
  public MultiFormatDateTimeConverted(string[] dateFormats, DateTime? defaultValue)
  {
    _dateFormats = dateFormats;
    _defaultValue = defaultValue;
  }

  public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
  {
    writer.WriteValue(value?.ToString());
  }
  
  public override object? ReadJson(
      JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
  {
      if (reader.TokenType == JsonToken.Null || reader.Value is null)
      {
          return _defaultValue;
      }

      if (reader.TokenType == JsonToken.Date)
      {
          return (DateTime)reader.Value;  // already a known date, no need to parse.
      }

      var dateString = reader.Value.ToString();
      if (DateTime.TryParseExact(
          dateString, 
          _dateFormats, 
          CultureInfo.InvariantCulture, 
          DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal,
          out DateTime parsedDate))
      {
          return parsedDate;
      }

      // one might want to log the string values that failed the TryParseExact().
      // Inject a logger into the constructor and log the string value.
      //if (!string.IsNullOrWhiteSpace(dateString))
      //{
      //    _logger.Log($"Failed to parse date string: {dateString}");
      //}
      return _defaultValue; // or throw new JsonSerializationException("Invalid date format.");
  }

  public override bool CanConvert(Type objectType)
  {
      return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
  }
}

Local Time Conversion Extension

In addition, let’s create a nullable DateTime extension to get the Local DateTime.

public static class DateTimeExtensions
{
  public static DateTime? ToLocalTime(this DateTime? utcDateTime)
  {
      if (utcDateTime is null)
      {
          return null;
      }
  
      TimeZoneInfo localZone = TimeZoneInfo.Local;
      return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime.Value, localZone);
  }
}

You can then use this converter when deserializing JSON to handle a variety of date formats and edge cases.

The main code will now look like this:

internal class Program
{
  static void Main(string[] args)
  {
    // inconsistent format example
    string utcDateString = " Jan-25-2024 01:14:15";
    string sampleJson = $"{{\"UtcDate\": \"{utcDateString}\", \"Name\": \"Noel\", \"Siblings\" : 5}}";
    var settings = new JsonSerializerSettings
    {
      Converters = new List<JsonConverter>
      {
        new MultiFormatDateTimeConverted(
          new []
          {
            // add all the non-standard date formats to this string array
            "dd-MM-yyyy HH:mm:ssK",
            "dd/MM/yyyy HH:mm:ssK",
            "dd/MM/yyyyTHH:mm:ssK",
            "MMM-dd-yyyy HH:mm:ssK"     // by using "K" instead of "Z", then this will match both strings with a Z or without a Z
          },
          defaultValue: null
        )
      }
    };
    var sampleObject = JsonConvert.DeserializeObject<SampleObject1>(
      sampleJson, settings);

    DateTime? localDateTime = sampleObject?.UtcDate.ToLocalTime();
    
    Console.WriteLine($"utc datetime: {sampleObject?.UtcDate}  [{sampleObject?.UtcDate?.Kind}]");
    Console.WriteLine($"local datetime: {localDateTime}  [{localDateTime?.Kind}]");
  }
}

Serializing Dates for API Consumption

When sending dates back to the API, ensure they are serialized in UTC:

var settings = new JsonSerializerSettings
{
    DateTimeZoneHandling = DateTimeZoneHandling.Utc
};
string json = JsonConvert.SerializeObject(sampleObject, settings);

This approach continues with the assumption that `Unspecified` DateTime kinds are treated as UTC.

Conclusion

Dealing with UTC dates requires a robust strategy to handle various formats and edge cases. By understanding the nuances of `DateTimeKind`, leveraging custom converters for flexibility, and ensuring consistent handling of time zones, you can effectively manage UTC dates in your web services.

Now, Over to You!

Have you encountered challenges with UTC dates in your projects? What strategies have you found most effective? Share your experiences and tips in the comments below. Let’s learn from each other and build a knowledge base that helps all of us handle date-time issues more effectively. Whether you’re a seasoned pro or just starting, your insights are valuable!

C Sharp Programming
Dotnet
Coding Best Practices
Csharp
Coding
Recommended from ReadMedium