08Dc91wk
08Dc91wk

Reputation: 4318

Binding NodaTime Instant field in an InputModel body using C# ASP.NET WebAPI 2.0

I have an WebAPI project that takes in ISO date strings inside the input model. I have been parsing these using DateTimeOffset?. I want to banish BCL datetimes from my project so I want to find a way to directly bind these strings to Instant.

public class MyInputModel
{
    public DateTimeOffset? OldTime { get; set; }

    public Instant NewTime { get; set; }
}

An example JSON input model looks like this:

{
    "oldtime":"2016-01-01T12:00:00Z",
    "newtime":"2016-01-01T12:00:00Z"
}

And my controller code is:

[HttpPost]
public async Task<IActionResult> PostTimesAsync([FromBody]MyInputModel input)
{
    Instant myOldTime = Instant.FromDateTimeUtc(input.oldTime.Value.UtcDateTime);
    Instant myNewTime = input.newTime; // This cannot be used with ISO date strings.
}

I attempted to build a custom model binder as follows. This works for models in the query string but not for those in the body of a POST request. How can I bind date input in an ISO 8601 string format to a NodaTime Instant?

public Task BindModelAsync(ModelBindingContext bindingContext)
{
    if (!String.IsNullOrWhiteSpace(bindingContext.ModelName) && 
            bindingContext.ModelType == typeof(Instant?) && 
            bindingContext.ValueProvider.GetValue(bindingContext.ModelName) != null)
    {
        Instant? value;
        var val = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName).FirstValue as string;

        if (String.IsNullOrWhiteSpace(val))
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return Task.FromResult(0);
        }
        else if (InstantExtensions.TryParse(val, out value))
        {
            bindingContext.Result = ModelBindingResult.Success(value);
            return Task.FromResult(0);
        }
        else
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, 
"The date is invalid.");
        }
    }

    bindingContext.Result = ModelBindingResult.Failed();
    return Task.FromResult(0);
}

public static bool TryParse(string value, out Instant? result)
{
    result = null;

    // If this is date-only, add Utc Midnight to the value.
    if (value.Length.Equals(10))
    {
        value += "T00:00:00Z";
    }

    // Trim milliseconds if present
    var decimalPointIndex = value.IndexOf('.');
    if (decimalPointIndex > 0)
    {
        value = value.Substring(0, decimalPointIndex) + "Z";
    }

    // Attempt to parse
    var parseResult = InstantPattern.GeneralPattern.Parse(value);
    if (parseResult.Success)
    {
        result = parseResult.Value;
        return true;
    }

    return false;
}

Upvotes: 2

Views: 676

Answers (1)

bot_insane
bot_insane

Reputation: 2604

You should add your model binder like this: ( in WebApiConfig Register method )

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.BindParameter(typeof(Instant), new InstantModelBinder())
        ...
    }
}

WebApiConfig.Register is called in Configuration function in Startup.cs file. In most cases like this:

var config = new HttpConfiguration();
WebApiConfig.Register(config);

If it isn't called, you can just add this line:

config.BindParameter(typeof(Instant), new InstantModelBinder())

where HttpConfiguration object is being created in Startup.cs.

Upvotes: 1

Related Questions