8

I'm investigating the use of NodaTime LocalDate to replace our existing use of of the BCL DateTime/DateTimeOffset classes. We have run into a number of timezone related issues with our code due to our misunderstanding of the arguably ambiguous behavior of DateTime.

To fully leverage NodaTime I want to be able to send and receive dates from our ASP.NET Web API 2 web services of the form YYYY-MM-DD. I have had success in properly serializing LocalDate to YYYY-MM-DD. However I am unable to deserialize a date query parameter to a LocalDate. The LocateDate is always 1970-01-01.

Here is my current prototype code (some code removed for clarity):

PeopleController.cs

[RoutePrefix("api")]
public class PeopleController : ApiController
{
    [Route("people")]
    public LocalDate GetPeopleByBirthday([FromUri]LocalDate birthday)
    {
        return birthday;
    }
}

Global.asax.cs

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        // Web API configuration and services
        var formatters = GlobalConfiguration.Configuration.Formatters;
        var jsonFormatter = formatters.JsonFormatter;
        var settings = jsonFormatter.SerializerSettings;
        settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
        settings.Formatting = Formatting.Indented;
        settings.ContractResolver = new CamelCasePropertyNamesContractResolver();

        GlobalConfiguration.Configure(WebApiConfig.Register);
    }
}

I execute the web service via

http://localhost/api/people?birthday=1980-11-20

However, what is returned is January 1, 1970. Stepping into the code I confirm that birthday is set to 1970-01-01.

How can I configure the serialization such that the date specified in the URL as a query parameter (or path element) can be properly serialized into a NodaTime LocalDate?

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
Ryan Taylor
  • 8,740
  • 15
  • 65
  • 98
  • Hmm. I don't support it helps if you remove the `FromUri` part, does it? (I'd expect it to default to that, and it may be confusing it...) This does look like more of a model parsing issue than a Noda Time issue - my guess is that it's not getting into the JSON serializer at all. Just for the purposes of diagnosis, have you tried a wrapper class with a `birthday` property of type `LocalDate`, then use that class as your parameter type? – Jon Skeet Jul 09 '14 at 20:46
  • The URL is not JSON, so it's not interpreted by JSON.Net nor does it pass through the NodaTime serialization configuration. – Matt Johnson-Pint Jul 09 '14 at 21:27
  • I can reproduce this. I'm not sure of a solution yet, other than to post in the body, but that's not ideal for this request. I'll dig deeper. – Matt Johnson-Pint Jul 09 '14 at 21:29
  • @JonSkeet, removing FromUri seems to have no impact. I created a `BirthdayFacade` class with a single `LocalDate Birthday` property and that too was serialized to 1970-01-01. – Ryan Taylor Jul 09 '14 at 21:51
  • Hmm. Let's hope Matt gets somewhere then - I'm out of my depth at this point. – Jon Skeet Jul 09 '14 at 22:02

1 Answers1

5

Thanks to this very helpful article from Microsoft, I was able to find the solution using a custom model binder.

Add this class to your project:

public class LocalDateModelBinder : IModelBinder
{
    private readonly LocalDatePattern _localDatePattern = LocalDatePattern.IsoPattern;

    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof (LocalDate))
            return false;

        var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (val == null)
            return false;

        var rawValue = val.RawValue as string;

        var result = _localDatePattern.Parse(rawValue);
        if (result.Success)
            bindingContext.Model = result.Value;

        return result.Success;
    }
}

Then change your controller method to use it.

public LocalDate GetPeopleByBirthday(
    [ModelBinder(typeof(LocalDateModelBinder))] LocalDate birthday)

The article also mentions other ways to register model binders.

Note that since your method returns a LocalDate, you'll still need the Noda Time serialziation for Json.net, as that ends up getting used in the body for the return value.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • Aside - the article also discusses use of type converters. I tried that, but to register it I had to use [this technique](http://stackoverflow.com/q/13924494/634824), which didn't seem to agree with WebAPI. It may be worth exploring whether to add type converters directly to the Noda Time types in a future release. – Matt Johnson-Pint Jul 09 '14 at 22:19
  • 1
    Works like a charm and with some excellent documentation to boot. Thanks! – Ryan Taylor Jul 09 '14 at 22:26