Source on GitHub.
This post shows how to write a custom DateTime
model binder in ASP.NET Core Web API. The default DateTime
binding mechanism will not work for custom date formats like:
yyyyMMdd
yyyy-MM-dd
MM-dd-yyyy
yyyyMMddTHHmmss
yyyy-MM-ddTHH-mm-ss
Create a custom DateTimeModelBinder
and DateTimeModelBinderAttribute
.
public class DateTimeModelBinder : IModelBinder
{
public static readonly Type[] SUPPORTED_TYPES = new Type[] { typeof(DateTime), typeof(DateTime?) };
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
if (!SUPPORTED_TYPES.Contains(bindingContext.ModelType))
{
return Task.CompletedTask;
}
var modelName = GetModelName(bindingContext);
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
var dateToParse = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(dateToParse))
{
return Task.CompletedTask;
}
var dateTime = ParseDate(bindingContext, dateToParse);
bindingContext.Result = ModelBindingResult.Success(dateTime);
return Task.CompletedTask;
}
private DateTime? ParseDate(ModelBindingContext bindingContext, string dateToParse)
{
var attribute = GetDateTimeModelBinderAttribute(bindingContext);
var dateFormat = attribute?.DateFormat;
if (string.IsNullOrEmpty(dateFormat))
{
return Helper.ParseDateTime(dateToParse);
}
return Helper.ParseDateTime(dateToParse, new string[] { dateFormat });
}
private DateTimeModelBinderAttribute GetDateTimeModelBinderAttribute(ModelBindingContext bindingContext)
{
var modelName = GetModelName(bindingContext);
var paramDescriptor = bindingContext.ActionContext.ActionDescriptor.Parameters
.Where(x => x.ParameterType == typeof(DateTime?))
.Where((x) =>
{
// See comment in GetModelName() on why we do this.
var paramModelName = x.BindingInfo?.BinderModelName ?? x.Name;
return paramModelName.Equals(modelName);
})
.FirstOrDefault();
var ctrlParamDescriptor = paramDescriptor as ControllerParameterDescriptor;
if (ctrlParamDescriptor == null)
{
return null;
}
var attribute = ctrlParamDescriptor.ParameterInfo
.GetCustomAttributes(typeof(DateTimeModelBinderAttribute), false)
.FirstOrDefault();
return (DateTimeModelBinderAttribute)attribute;
}
private string GetModelName(ModelBindingContext bindingContext)
{
// 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 [DateTimeModelBinder(Name ="birthDate")]
// Now bindingContext.BinderModelName will be "birthDate" and bindingContext.ModelName will be "date"
return !string.IsNullOrEmpty(bindingContext.BinderModelName)
? bindingContext.BinderModelName
: bindingContext.ModelName;
}
}
public class DateTimeModelBinderAttribute : ModelBinderAttribute
{
public string DateFormat { get; set; }
public DateTimeModelBinderAttribute()
: base(typeof(DateTimeModelBinder))
{
}
}
A helper class with the ParseDateTime()
method.
public class Helper
{
public static DateTime? ParseDateTime(
string dateToParse,
string[] formats = null,
IFormatProvider provider = null,
DateTimeStyles styles = DateTimeStyles.None)
{
var CUSTOM_DATE_FORMATS = new string[]
{
"yyyyMMddTHHmmssZ",
"yyyyMMddTHHmmZ",
"yyyyMMddTHHmmss",
"yyyyMMddTHHmm",
"yyyyMMddHHmmss",
"yyyyMMddHHmm",
"yyyyMMdd",
"yyyy-MM-ddTHH-mm-ss",
"yyyy-MM-dd-HH-mm-ss",
"yyyy-MM-dd-HH-mm",
"yyyy-MM-dd",
"MM-dd-yyyy"
};
if (formats == null || !formats.Any())
{
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;
}
}
if (DateTime.TryParseExact(dateToParse, format,
provider, styles, out validDate))
{
return validDate;
}
}
return null;
}
public static bool IsNullableType(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>));
}
}
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.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 });
}
}
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 [DateTimeModelBinder]
attribute on the action parameter.
[Route("api/[controller]")]
public class MainController : Controller
{
[Route("echo-date/{date}")]
[HttpGet]
public DateTime? EchoDate(
[DateTimeModelBinder]
DateTime? date)
{
return date;
}
[Route("echo-custom-date/{date}")]
[HttpGet]
public DateTime? EchoCustomDateFormat(
[DateTimeModelBinder(DateFormat = "yyyyMMdd")]
DateTime? date)
{
return date;
}
[Route("echo-model")]
[HttpPost]
public PostData EchoModel(PostData model)
{
return model;
}
}
Add it globally to bind all action parameters of type DateTime
and DateTime?
.
public class DateTimeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (DateTimeModelBinder.SUPPORTED_TYPES.Contains(context.Metadata.ModelType))
{
return new BinderTypeModelBinder(typeof(DateTimeModelBinder));
}
return null;
}
}
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(option =>
{
// add the custom binder at the top of the collection
option.ModelBinderProviders.Insert(0, new DateTimeModelBinderProvider());
});
}
}
Verify the api works as expected...
GET http://localhost/api/echo-date/20150115
GET http://localhost/api/echo-date/2015-01-15
GET http://localhost/api/echo-date/10-05-2015
GET http://localhost/api/echo-date/20150115T142354
GET http://localhost/api/echo-date/2015-01-15T14-23-54
POST http://localhost/api/echo-model
{
"dateFrom": "12-25-2019",
"dateTo": "12-31-2019"
}
Related: