marcusturewicz
marcusturewicz

Reputation: 2494

Custom DateTime ModelBinder is being bypassed in ASP.NET Core Controller

I am trying to create a HTTP GET Web API that only accepts dates in ISO-8601 format i.e. YYYY-MM-DD. I have the following simple API Controller that takes in the DateTime, and I am applying a custom model binder via an attribute:

using System;
using System.Globalization;
using Microsoft.AspNetCore.Mvc;

namespace WebApiDates.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WebApiDatesController : ControllerBase
    {
        [HttpGet("datetime")]
        public IActionResult GetDateTime([ModelBinder(BinderType = typeof(IsoDateModelBinder))] DateTime date)
        {
            return Ok($"Date was {date}");
        }
}

The custom model binder looks like this:

using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.Logging;

namespace WebApiDates
{
    public class IsoDateModelBinder : IModelBinder
    {
        private readonly IModelBinder _baseBinder;

        public IsoDateModelBinder(ILoggerFactory loggerFactory)
        {
            _baseBinder = new SimpleTypeModelBinder(typeof(DateTime), loggerFactory);
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult != ValueProviderResult.None)
            {
                bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

                var valueAsString = valueProviderResult.FirstValue;

                var dateTimeParsed = DateTime.TryParseExact(valueAsString, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeResult);
                if (dateTimeParsed)
                {
                    bindingContext.Result = ModelBindingResult.Success(dateTimeResult);
                    return Task.CompletedTask;
                }
            }

            return _baseBinder.BindModelAsync(bindingContext);
        }
    }
}

The rest of the project is a standard .NET 5.0 Web API project, created with dotnet new webapi.

To test this, I have the following unit test:

using System.Threading.Tasks;
using Xunit;
using WebApiDates;
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;

namespace WebApiDates.Tests
{
    public class WebApiTests : IClassFixture<WebApplicationFactory<Startup>>
    {
        private readonly WebApplicationFactory<Startup> _factory;

        public WebApiTests(WebApplicationFactory<Startup> factory)
        {
            _factory = factory;
        }

        [Theory]
        [InlineData("2021-08-21", HttpStatusCode.OK)]
        [InlineData("2021-21-08", HttpStatusCode.BadRequest)]
        [InlineData("08-21-2021", HttpStatusCode.BadRequest)]
        [InlineData("21-08-2021", HttpStatusCode.BadRequest)]
        [InlineData("2021/08/21", HttpStatusCode.BadRequest)]
        [InlineData("2021/21/08", HttpStatusCode.BadRequest)]
        [InlineData("08/21/2021", HttpStatusCode.BadRequest)]
        [InlineData("21/08/2021", HttpStatusCode.BadRequest)]
        [InlineData("08.21.2021", HttpStatusCode.BadRequest)]
        [InlineData("21.08.2021", HttpStatusCode.BadRequest)]
        [InlineData("2021.08.21", HttpStatusCode.BadRequest)]
        [InlineData("2021.21.08", HttpStatusCode.BadRequest)]
        public async Task DateTimeTests(string date, HttpStatusCode expectedStatusCode)
        {
            // Arrange
            using var client = _factory.CreateClient();

            // Act
            var response = await client.GetAsync($"/webapidates/datetime?date={date}");

            // Assert
            Assert.Equal(expectedStatusCode, response.StatusCode);
        }           
    }
}

The problem is that only the first test should return the OK result, whereas the following tests are also returning OK:

Image of tests that should not be failing

And when I debug the model binder, it is doing the date parsing correctly. So it seems something is overriding the model binder.

The source is located here if you want to take a closer look.

Upvotes: 0

Views: 524

Answers (1)

Alexander
Alexander

Reputation: 9632

You need to validation errors to models state so it will result into BadRequest later, no _baseBinder required

public class IsoDateModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueProviderResult != ValueProviderResult.None)
        {
            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

            var valueAsString = valueProviderResult.FirstValue;

            var dateTimeParsed = DateTime.TryParseExact(valueAsString, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeResult);
            if (dateTimeParsed)
            {
                bindingContext.Result = ModelBindingResult.Success(dateTimeResult);

            }
            else
            {
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid date string");
            }
        }

        return Task.CompletedTask;
    }
}

Upvotes: 2

Related Questions