Custom DateTime Model Binding in ASP.NET Web API

ASP.NET Web API binds simple data types like DateTime by reading from the URL by default.

Source: Parameter Binding in ASP.NET Web API 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.

The default mechanism will not work for custom URL friendly date formats like:

yyyyMMdd
yyyy-MM-dd
MM-dd-yyyy
yyyyMMddTHHmmss
yyyy-MM-ddTHH-mm-ss

We can create a custom HttpParameterBinding implementation to bind custom DateTime formats. I have posted the code on GitHub Gist.

namespace App.Web.Http.Helpers
{
    public class DateTimeParameterBinding : HttpParameterBinding
    {
        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;
            string paramName = 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();
                object dateObj = null;
                if (routeData.Values.TryGetValue(paramName, out 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)
        {
            string[] 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 = 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;
        }
    }
}

We also need an attribute that we can apply to the method parameter to indicate that we want to use the DateTimeParameterBinding class for binding.

namespace App.Web.Http.Helpers
{
    public class DateTimeParameterAttribute : ParameterBindingAttribute
    {
        public string DateFormat { get; set; }

        public bool ReadFromQueryString { get; set; }


        public override HttpParameterBinding GetBinding(
            HttpParameterDescriptor parameter)
        {
            if (parameter.ParameterType == typeof(DateTime?))
            {
                var binding = new DateTimeParameterBinding(parameter);

                binding.DateFormat = DateFormat;
                binding.ReadFromQueryString = ReadFromQueryString;
                
                return binding;
            }

            return parameter.BindAsError("Expected type DateTime?");
        }
    }
}

Apply the [DateTimeParameter] attribute to the method parameter.

[RoutePrefix("api")]
public class MainController : ApiController  
{
    [Route("EchoDate/{date}")]
    [HttpGet]
    public DateTime? EchoDate(
        [DateTimeParameter]
        DateTime? date)
    {
        return date;
    }

    [Route("EchoCustomDate/{date}")]
    [HttpGet]
    public DateTime? EchoCustomDateFormat(
        [DateTimeParameter(DateFormat = "dd_MM_yyyy")]
        DateTime? date)
    {
        return date;
    }
}

Alternatively, we can add it globally during application startup in which case the [DateTimeParameter] attribute is not required. With the rule added in the configuration, any method parameter of type DateTime? will be automatically bound.

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));
        }
    }
}

We can now verify that all these work 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

GET http://localhost/api/EchoDate/2015-01-15-14-23-54

Related: