DateTime to Unix Ticks in ASP.net Web API

Jan 21, 2016

red-clock

The Story

Recently I was faced with a bit of a challenge on an API I was building. A developer who was writing a client to my API suggested that he wanted all DateTimes to be returned as Unix ticks. This request didn’t set off any major alarms*, so I began making a plan for implementation.

In the past, I’ve found Unix ticks conversion to be a simple three step process.

  1. Take the DateTime
  2. Subtract January 1, 1970
  3. Get the total seconds.

My idea of the perfect implementation was to make all DateTimes, by default, serialize as ISO 8601. If a special header is set, the DateTimes would be formatted as Unix ticks. My initial research concluded the customization possible, but would impede the timeline.

I quickly learned that implementing custom serialization with JSON.net and ASP.net Web API, was not a straight-forward process. The process included Converters, MediaTypeFormatters, and ContractResolvers; Three object types that were foreign to me.

The conclusion of my research led me to realize that I must build each piece of the puzzle and put it together. I could not simply drop 2-3 lines of code wrapped in an if statement, and expect it to work.

* Besides losing precision as Unix ticks are only whole seconds.

The Challenge

  • Build a JsonMediaTypeFormatter that returns a ContractResolver if a certain HTTP Header is present.
  • Build a ContractResolver that resolves a Unix DateTime Converter if the data type is DateTime.
  • Build a Converter that converts DateTimes to Unix ticks.

The Code

The first piece of the puzzle is the JsonMediaTypeFormatter:

public class JsonUnixDateTimeFormatter : JsonMediaTypeFormatter
{
  private const string DateTimeFormatKey = "X-DateTimeFormat";
  private const string Unix = "Unix";

  public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
  {
    IEnumerable values;

    var result = request.Headers.TryGetValues(DateTimeFormatKey, out values)
             ? values.First()
             : "ISO 8601";

    // Please note that in this project we choose the CamelCasePropertyNamesContractResolver as our default ContractResolver; this is not the ASP.net default.
    SerializerSettings.ContractResolver = result.Equals(Unix, StringComparison.InvariantCultureIgnoreCase)
                          ? new UnixDateTimeContractResolver()
                          : new CamelCasePropertyNamesContractResolver();

    return this;
  }
}

Next, is the ContractResolver

public class UnixDateTimeContractResolver : CamelCasePropertyNamesContractResolver
{
  protected override JsonContract CreateContract(Type objectType)
  {
    var contract = base.CreateContract(objectType);

    if(objectType == typeof(DateTime))
    {
      contract.Converter = new UnixDateTimeConverter();
    }

    return contract;
  }
}

Finally, is the Converter

public class UnixDateTimeConverter : JsonConverter
{
  public override bool CanRead => false;

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

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    var origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
    var difference = ((DateTime)value).ToUniversalTime() - origin;
    var epochTicks = Math.Floor(difference.TotalSeconds);

    writer.WriteValue(epochTicks);
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    throw new NotImplementedException();
  }
}