3

Everytime my controller receives a date as dd/MM/yyyy it decode as MM/dd/yyyy. Is it possible to tell the controller how to decode the parameter of the url?

My method in controller:

[HttpGet]
public JsonResult<IList<Callers>> GetListOfCallers(DateTime startDate, DateTime endDate)
        {
            // myCode....
        }

My javascript:

var $startDate = $('#startDate').val();
var $endDate = $('#endDate').val();

$.get(rootUrl + "api/report/GetListOfCallers?startDate=" + $startDate + "&endDate=" + $endDate, function (data) {
                // myCode....
            });

I know I can receive the date in controller as string and then parse it, or change it in my javascript to ISO8601 before putting in the url, but I want to know if I can tell my controller how to decode the parameter received.

EDIT: I was using MVC controller and this was not a problem, it started decoding incorrectly after I changed to ApiController, so the code was working and I hope to keep as it is.

GTPON
  • 61
  • 1
  • 8
  • I believe you need to convert it from string to date, and in the process of decoding the string to a date, you can customize that however you want. – C. Helling Sep 06 '17 at 20:01
  • 1
    Model binding is your friend, here is what my googling on the subject related to your question revealed: http://www.vickram.me/custom-datetime-model-binding-in-asp-net-web-api/ – Jakotheshadows Sep 06 '17 at 20:05
  • I agree, custom model binding is the way to go here. –  Sep 06 '17 at 20:08
  • @Jakotheshadows Model binding works for url as well? I thought it was only for post methods. – GTPON Sep 06 '17 at 20:14
  • I am wondering if this might help you out... https://stackoverflow.com/a/20144019/4843530 ... basically change the default date formatting for your controller. That is for output, but wonder if you can figure it out for input. –  Sep 06 '17 at 20:15
  • @AgapwIesu I think not. I tried this and I think it only works for output DateTime, not input. – GTPON Sep 06 '17 at 20:18
  • The question was about input, and the answer just below that one, talks about changing the serializer's culture info (https://stackoverflow.com/a/19940756/4843530). Seems like it should work both ways. –  Sep 06 '17 at 20:24
  • @GTPON yes, if you follow the link you'll see the example is for an HttpGet action – Jakotheshadows Sep 06 '17 at 20:33
  • @AgapwIesu I tried here, didn't work... I think it is because he uses POST via Json, and I'm using GET via Url – GTPON Sep 06 '17 at 20:37
  • Yes, model binding works for URL parameters, regardless of the HTTP verb used. –  Sep 06 '17 at 21:20
  • out of curiousity, does it change the behavior if you use string instead of DateTime as the parameters of GetListOfCallers? If that helps, then could parse it into DateTime objects within it. – Robert Sep 07 '17 at 18:05
  • 1
    @Robert I don't think it changes, but it's a solution I don't want to use. I want a solution to implement in all projects so others programmers don't have to worry about conversions. – GTPON Sep 08 '17 at 16:14

1 Answers1

3

I manage to solve my problem using Model binding as suggested by @Jakotheshadows and @Amy.

I used the code from this answer about ModelBinders in Web Api with a few tweaks from this answer (it's in portuguese, but the code is clear).

So my code right now:

using System;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;

namespace Site.Services
{
    public class DateTimeModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            ValidateBindingContext(bindingContext);

            if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName) ||
                !CanBindType(bindingContext.ModelType))
            {
                return false;
            }

            var modelName = bindingContext.ModelName;
            var attemptedValue = bindingContext.ValueProvider
                .GetValue(modelName).AttemptedValue;

            try
            {
                bindingContext.Model = DateTime.Parse(attemptedValue);
            }
            catch (FormatException e)
            {
                bindingContext.ModelState.AddModelError(modelName, e);
            }

            return true;
        }

        private static void ValidateBindingContext(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException("bindingContext");
            }

            if (bindingContext.ModelMetadata == null)
            {
                throw new ArgumentException("ModelMetadata cannot be null", "bindingContext");
            }
        }

        public static bool CanBindType(Type modelType)
        {
            return modelType == typeof(DateTime) || modelType == typeof(DateTime?);
        }
    }
}

I used try and DateTime.Parse as suggested in the second link, because the first always throwed an exception even with try and catch.

The ModelBinderProvider I used as he suggested:

using System;
using System.Web.Http;
using System.Web.Http.ModelBinding;

namespace Site.Services
{
    public class DateTimeModelBinderProvider : ModelBinderProvider
    {
        readonly DateTimeModelBinder binder = new DateTimeModelBinder();

        public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
        {
            if (DateTimeModelBinder.CanBindType(modelType))
            {
                return binder;
            }

            return null;
        }
    }
}

And I configure as suggested here (also an answer for the first link), but in my WebApiConfig.cs (didn't work in Global.asax), like this:

using Site.Services;
using System;
using System.Web.Http;

namespace Site
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.BindParameter(typeof(DateTime), new DateTimeModelBinder());
            config.BindParameter(typeof(DateTime?), new DateTimeModelBinder());

            //Rest of my code
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

I think the globalization of the Web.config, the uiCulture and culture must be set to the culture you want and enableClientBasedCulture be set as true as suggest here, but I'm not sure because I didn't want to change the code to test it.

GTPON
  • 61
  • 1
  • 8