ASP.NET Web API binds simple data types like DateTime
by reading from the URL by default.
By default, Web API uses the following rules to bind parameters:
- If the parameter is a “simple” type, Web API tries to get the value from the URI. Simple types include the .NET primitive types (int, bool, double, and so forth), plus TimeSpan, DateTime, Guid, decimal, and string, plus any type with a type converter that can convert from a string. (More about type converters later.)
- For complex types, Web API tries to read the value from the message body, using a media-type formatter.
This default mechanism does not work for custom URL friendly date formats:
yyyyMMdd
yyyy-MM-dd
MM-dd-yyyy
yyyyMMddTHHmmss
yyyy-MM-ddTHH-mm-ss
Create a custom DateTimeParameterBinding
and DateTimeParameterAttribute
.
namespace App.Web.Http.Helpers
{
public class DateTimeParameterBinding : HttpParameterBinding
{
public string BinderModelName { get; set; }
public string DateFormat { get; set; }
public bool ReadFromQueryString { get; set; }
public DateTimeParameterBinding(HttpParameterDescriptor descriptor)
: base(descriptor) { }
public override Task ExecuteBindingAsync(
ModelMetadataProvider metadataProvider,
HttpActionContext actionContext,
CancellationToken cancellationToken)
{
string dateToParse = null;
var paramName = !string.IsNullOrEmpty(ModelName) ? ModelName : this.Descriptor.ParameterName;
if (ReadFromQueryString)
{
// reading from query string
var nameVal = actionContext.Request.GetQueryNameValuePairs();
dateToParse = nameVal.Where(q => q.Key.EqualsEx(paramName))
.FirstOrDefault().Value;
}
else
{
// reading from route
var routeData = actionContext.Request.GetRouteData();
if (routeData.Values.TryGetValue(paramName, out var dateObj))
{
dateToParse = Convert.ToString(dateObj);
}
}
DateTime? dateTime = null;
if (!string.IsNullOrEmpty(dateToParse))
{
if (string.IsNullOrEmpty(DateFormat))
{
dateTime = ParseDateTime(dateToParse);
}
else
{
dateTime = ParseDateTime(dateToParse, new string[] { DateFormat });
}
}
SetValue(actionContext, dateTime);
return Task.FromResult<object>(null);
}
public DateTime? ParseDateTime(
string dateToParse,
string[] formats = null,
IFormatProvider provider = null,
DateTimeStyles styles = DateTimeStyles.AssumeLocal)
{
var CUSTOM_DATE_FORMATS = new string[]
{
"yyyyMMddTHHmmssZ",
"yyyyMMddTHHmmZ",
"yyyyMMddTHHmmss",
"yyyyMMddTHHmm",
"yyyyMMddHHmmss",
"yyyyMMddHHmm",
"yyyyMMdd",
"yyyy-MM-dd-HH-mm-ss",
"yyyy-MM-dd-HH-mm",
"yyyy-MM-dd",
"MM-dd-yyyy"
};
if (formats == null)
{
formats = CUSTOM_DATE_FORMATS;
}
DateTime validDate;
foreach (var format in formats)
{
if (format.EndsWith("Z"))
{
if (DateTime.TryParseExact(dateToParse, format,
provider,
DateTimeStyles.AssumeUniversal,
out validDate))
{
return validDate;
}
}
else
{
if (DateTime.TryParseExact(dateToParse, format,
provider, styles, out validDate))
{
return validDate;
}
}
}
return null;
}
}
}
namespace App.Web.Http.Helpers
{
public class DateTimeParameterAttribute : ParameterBindingAttribute
{
// The "Name" property of the ModelBinder attribute can be used to specify the
// route parameter name when the action parameter name is different from the route parameter name.
// For instance, when the route is /api/{birthDate} and the action parameter name is "date".
// We can add this attribute with a Name property [DateTimeParameter(Name ="birthDate")]
public string Name { get; set; }
public string DateFormat { get; set; }
public bool ReadFromQueryString { get; set; }
public override HttpParameterBinding GetBinding(
HttpParameterDescriptor parameter)
{
if (parameter.ParameterType == typeof(DateTime?))
{
return new DateTimeParameterBinding(parameter)
{
BinderModelName = Name,
DateFormat = DateFormat,
ReadFromQueryString = ReadFromQueryString
};
}
return parameter.BindAsError("Expected type DateTime?");
}
}
}
Create a CustomDateTimeConverter
if you need to bind model properties.
public class CustomDateTimeConverter : DateTimeConverterBase
{
private readonly string dateFormat = null;
private readonly DateTimeConverterBase innerConverter = null;
public CustomDateTimeConverter()
: this(dateFormat: null) { }
public CustomDateTimeConverter(string dateFormat = null)
: this(dateFormat, innerConverter: new IsoDateTimeConverter()) { }
public CustomDateTimeConverter(string dateFormat = null, DateTimeConverterBase innerConverter = null)
{
this.dateFormat = dateFormat;
this.innerConverter = innerConverter ?? new IsoDateTimeConverter();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var isNullableType = Helper.IsNullableType(objectType);
if (reader.TokenType == JsonToken.Null)
{
if (isNullableType)
{
return null;
}
throw new JsonSerializationException($"Cannot convert null value to {objectType}.");
}
if (reader.TokenType == JsonToken.Date)
{
return (DateTime?)reader.Value;
}
if (reader.TokenType != JsonToken.String)
{
throw new JsonSerializationException($"Unexpected token parsing date. Expected {nameof(String)}, got {reader.TokenType}.");
}
var dateToParse = reader.Value.ToString();
if (isNullableType && string.IsNullOrWhiteSpace(dateToParse))
{
return null;
}
if (string.IsNullOrEmpty(this.dateFormat))
{
return Helper.ParseDateTime(dateToParse);
}
return Helper.ParseDateTime(dateToParse, new string[] { this.dateFormat });
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var dateTime = value as DateTime?;
if (dateTime == null || string.IsNullOrWhiteSpace(this.dateFormat))
{
return;
}
var s = dateTime.Value.ToString(this.dateFormat);
writer.WriteValue(s);
}
}
Register the CustomDateTimeConverter
on the model properties using the [JsonConverter]
attribute.
public class PostData
{
[JsonConverter(typeof(CustomDateTimeConverter), new object[] { "MM-dd-yyyy" })]
public DateTime DateFrom { get; set; }
[JsonConverter(typeof(CustomDateTimeConverter), new object[] { "MM-dd-yyyy" })]
public DateTime? DateTo { get; set; }
}
Apply the [DateTimeParameter]
attribute on the action parameter.
[RoutePrefix("api")]
public class MainController : ApiController
{
[Route("EchoDate/{date}")]
[HttpGet]
public DateTime? EchoDate(
[DateTimeParameter]
DateTime? date)
{
return date;
}
[Route("EchoCustomDateFormat/{date}")]
[HttpGet]
public DateTime? EchoCustomDateFormat(
[DateTimeParameter(DateFormat = "dd_MM_yyyy")]
DateTime? date)
{
return date;
}
[Route("EchoDateFromUri/{date}")]
[HttpGet]
public DateTime? EchoDateFromUri(
[DateTimeParameter(DateFormat = "yyyyMMdd", FromUri = true)]
DateTime? date)
{
return date;
}
[Route("EchoModel")]
[HttpPost]
public PostData EchoModel(PostData model)
{
return model;
}
}
Add it globally to bind all action parameters of type DateTime
and DateTime?
.
using App.Web.Http.Helpers;
namespace App.Web
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// add the rule to the collection
config.ParameterBindingRules
.Add(typeof(DateTime?),
des => new DateTimeParameterBinding(des));
}
}
}
Verify the api works as expected...
GET http://localhost/api/EchoDate/20150115
GET http://localhost/api/EchoDate/2015-01-15
GET http://localhost/api/EchoDate/10-05-2015
GET http://localhost/api/EchoDate/20150115T142354
GET http://localhost/api/EchoDate/2015-01-15T14-23-54
POST http://localhost/api/EchoDate
{
"dateFrom": "12-25-2019",
"dateTo": "12-31-2019"
}
Related: