Reputation: 2494
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
:
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
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