[This answer expands on the answer from @SeanFausett]
I wanted to have an ISO 8601 date that could have a "Z" on the and the web api function would receive it as a Utc Kind DateTime. But if there was not a "Z", I did not want the conversion.
I also needed to convert dates from incoming POST JSON payloads. The function below can support converting a string to a DateTime, DateTime?, DateTimeOffset, or DateTimeOffset?
It's handy to have dates parse the same way whether form a JSON post or URL parameter. Feel free to tailor the conversion to suit your needs.
//Register the two converters
var jSettings = new Newtonsoft.Json.JsonSerializerSettings()
jSettings.Converters.Add(new UtcDateTimeConverterJSON());
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings = jSettings;
GlobalConfiguration.Configure(config =>
{
TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(UtcDateTimeConverterURI)));
WebApiConfig.Register(config);
}
//Date converter for URI parameters
public class UtcDateTimeConverterURI : DateTimeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value?.GetType() == typeof(string))
{
return StringToDate(typeof(DateTime), (string)value, Path: "URI parameter");
}
else
{
return base.ConvertFrom(context, culture, value);
}
}
/// <summary>
/// Convert String to DateTime, DateTime?, DateTimeOffset, or DateTimeOffset?<br />
/// Used for incoming JSON objects and URI parameters
/// </summary>
/// <param name="targetType">The type (i.e. typeof(DateTime))</param>
/// <param name="sDate">string representation of date to be converted</param>
/// <param name="Path">JSON Path in case of error, so the caller knows which parameter to fix</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static object StringToDate(Type targetType, string sDate, string Path)
{
//if the object is a DateTime, determine if we need to return a UTC or Local date type
bool returnUTC = false;
//DateTime or DateTimeOffset return type
bool isDateTimeOffset;
if (targetType == typeof(DateTime?) || targetType == typeof(DateTime))
{
isDateTimeOffset = false;
}
else
{
isDateTimeOffset = true;
}
DateTimeOffset d;
if (String.IsNullOrEmpty(sDate))
{
//if we have an empty string and the type is a nullable date, then return null... otherwise throw an error
if (targetType == typeof(DateTime?))
{
return null;
}
else
{
throw new Exception(Path + " cannot be an empty Date");
}
}
if (sDate[0] == '/')
{
// /Date(xxxxx)/ format
sDate = sDate.Substring(6, sDate.Length - 8);
var index = sDate.LastIndexOf('-');
if (index == -1) index = sDate.LastIndexOf('+');
if (index >= 0)
{
//lop off timezone offset
sDate = sDate.Substring(0, index);
}
else
{
//no timezone offset, return as UTC
returnUTC = true;
}
if (!Int64.TryParse(sDate, out var l))
{
//can't parse....
throw new Exception(Path + " cannot be parsed as a Date");
}
else
{
d = DateTimeOffset.FromUnixTimeMilliseconds(l);
}
}
else
{
//try and parse ISO8601 string
if (!DateTimeOffset.TryParse(sDate, out d))
{
throw new Exception(Path + " cannot be parsed as a Date");
}
else
{
if (!isDateTimeOffset)
{
//if UTC is specifically requested and we're not returning a DateTimeOffset, then make sure the return is UTC
if (d.Offset == TimeSpan.Zero && sDate[sDate.Length - 1] == 'Z') returnUTC = true;
}
}
}
if (isDateTimeOffset)
{
return d;
}
else
{
if (returnUTC)
{
return d.UtcDateTime;
}
else
{
//return the raw time passed in, forcing it to the "Local" Kind
//for example:
//"2020-03-27T12:00:00" --> use 2020-03-27 12:00:00PM with Kind=Local
//"2020-03-27T12:00:00-05:00" --> use 2020-03-27 12:00:00PM with Kind=Local
return DateTime.SpecifyKind(d.DateTime, DateTimeKind.Local); //this will pull the raw time and force the Kind to "Local"
}
}
}
}
//Date converter for JSON payloads
public class UtcDateTimeConverterJSON : DateTimeConverterBase
{
public override bool CanRead
{
get
{
return true;
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null || reader.TokenType == JsonToken.Date) return reader.Value;
if (reader.TokenType != JsonToken.String) throw new Exception("Cannot parse Date");
return UtcDateTimeConverterURI.StringToDate(objectType, (string)reader.Value, reader.Path);
}
}