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.
- Take the DateTime
- Subtract January 1, 1970
- 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();
}
}