2

I have this model:

public class CalendarAvailabilityRequest
{
    [Required]
    [FromQuery]        
    public DateTime StartDate { get; set; }
}

and this controller/action method:

[ApiController]
[Route("api/[controller]")]
public class AppointmentController : ControllerBase
{        
    [Route("{providerName}/CalendarAvailability")]
    [HttpGet]
    public Task<CalendarAvailabilityResponse> GetCalendarAvailability(CalendarAvailabilityRequest request)
    {
        return null;
    }
}

How can I make sure only "yyyy-MM-dd" is accepted when hitting the endpoint?

eg. This will be accepted:

https://example.org/api?StartDate=2019-04-17

But these would throw an Exception:

https://example.org/api?StartDate=2019-17-04

https://example.org/api?StartDate=17-04-2017

David Klempfner
  • 8,700
  • 20
  • 73
  • 153
  • 1
    I think this will help you. https://stackoverflow.com/a/5392214/2845389. Use try parse exact in your case. – Kaushik Apr 17 '19 at 05:01
  • You could restrict the route to the format you are expecting [such as](https://stackoverflow.com/a/22110506/4527057) – lloyd Apr 17 '19 at 05:27
  • Those suggestions seem good but only work if the type is string. I was hoping to keep the type DateTime if I could. – David Klempfner Apr 18 '19 at 01:00

2 Answers2

2

I would suggest using fluentvalidation, as it allows to separate and reuse validation rules.

In your case assuming the startdate is part of the CalendarAvailabilityRequest, you would add a validator for the request dto:

public class CalendarAvailabilityRequestValidator : 

AbstractValidator<CalendarAvailabilityRequest> 
{
  public CalendarAvailabilityRequestValidator() 
  {
    RuleFor(request => request.StartDate)
        .Must(BeAValidDateFormat).WithMessage("Date must follow the format: yyyy-mm-dd")
        .NotNull().WithMessage("A start date must be provided.");
  }

  // will only match yyyy-mm-dd
  private static bool BeAValidDateFormat(string date)
    => Regex.IsMatch(date, "2\d{3}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$", RegexOptions.Compiled);
}

Within your controller you instanciate a validator and let it validate:

[Route("{providerName}/CalendarAvailability")]
[HttpGet]
public Task<IActionResult> GetCalendarAvailability(CalendarAvailabilityRequest request)
{
    var validationResult = new CalendarAvailabilityRequestValidator().Validate(request);
    if (!validationResult.IsValid)
    {
        Log.Warning(validationResult.Errors.ToString());
        return BadRequest(validationResult.Errors);
    }
    var statDate = DateTime.ParseExact(request.StartDate, "yyyy-mm-dd", CultureInfo.InvariantCulture);
    //TODO: calendar availability logic
    return OK(); 
}

Of course you can just as well use the regex from above and validate the request wihin your controller.

Another option is to try catch using DateTime.ParseExact something like this:

try
{
    var statDate = DateTime.ParseExact(request.StartDate, "yyyy-mm-dd", CultureInfo.InvariantCulture);
}
catch(exception ex)
{
  Log.Warning("Request for {startdate} was invalid: {message}", request.StartDate, ex.Message);
  return BadRequest(ex.message);
}

But I would recomment to avoid try catch when you can validate the input, unless you really need to.

Raul
  • 2,745
  • 1
  • 23
  • 39
  • Wouldn't that only work if StartDate was of type String? In my model StartDate is of type DateTime, so by the time it hits the endpoint it's already attempted to convert the input to a DateTime. – David Klempfner Apr 17 '19 at 11:20
  • Correct, it would work for strings only and it's the way to describe your API externally as you imply the format 'yyyy-MM-dd'. If you allow for a datetime format, you can only check that the time part is not set or it's default, whatever the default might be. In that case, why do you enforce this format, you might just as well use the `DateTime.Date` part and ignore the time, being consumer friendly. – Raul Apr 17 '19 at 12:05
1

I ended up writing an Attribute which implements IResourceFilter:

public class DateTimeResourceFilterAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        const string PreferredDateTimeFormat = "yyyy-MM-dd";
        string dateTimeString = context.HttpContext.Request.Query["StartDate"].First();
        bool isPreferredDateTimeFormat = DateTime.TryParseExact(dateTimeString, PreferredDateTimeFormat, new CultureInfo("en-AU"), DateTimeStyles.None, out DateTime dateTime);
        if (!isPreferredDateTimeFormat)
        {
            context.Result = new ContentResult()
            {
                Content = $"Date must be in the following format: {PreferredDateTimeFormat}",
                StatusCode = (int)HttpStatusCode.BadRequest
            };
        }
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}

I applied the attribute to my action method:

    [DateTimeResourceFilter]
    [Route("{providerName}/CalendarAvailability")]
    [HttpGet]
    public Task<CalendarAvailabilityResponse> GetCalendarAvailability(CalendarAvailabilityRequest request)
    {
        return null;
    }
David Klempfner
  • 8,700
  • 20
  • 73
  • 153