How can I use DateOnly/TimeOnly query parameters in ASP.NET Core 6?
Asked Answered
A

2

35

As of .NET 6 in ASP.NET API, if you want to get DateOnly (or TimeOnly) as query parameter, you need to separately specify all it's fields instead of just providing a string ("2021-09-14", or "10:54:53" for TimeOnly) like you can for DateTime.

I was able to fix that if they are part of the body by adding adding custom JSON converter (AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(...))), but it doesn't work for query parameters.

I know that could be fixed with model binder, but I don't want to create a model binder for every model that contains DateOnly/TimeOnly. Is there a way to fix this application wide?

Demo:

Lets assume you have a folowwing action:

[HttpGet] public void Foo([FromQuery] DateOnly date, [FromQuery] TimeOnly time, [FromQuery] DateTime dateTime)

Here's how it would be represented in Swagger:

enter image description here

I want it represented as three string fields: one for DateOnly, one for TimeOnly and one for DateTime (this one is already present).

PS: It's not a Swagger problem, it's ASP.NET one. If I try to pass ?date=2021-09-14 manually, ASP.NET wouldn't understand it.

Abiosis answered 15/9, 2021 at 5:40 Comment(2)
Sorry I can't catch you well, did you mean that you wanna a filter which will check the query parameters in the url, then if there's separate year, month, day, hour, min, sec, then turn them into a datetime?Grubb
Sorry if I was unclear. TLDR or this is that I want DateOnly and TimeOnly be represented by a single string argument (like DateTime currently is). So, there should be three fields on the screenshot above. Updated the question description.Abiosis
A
47

Turns out, there are two solutions:

I went with TypeConverter, and everything worked! Since .Net team are not planning to add full support for DateOnly/TimeOnly in .Net 6, I've decided to create a NuGet to do so:

https://www.nuget.org/packages/DateOnlyTimeOnly.AspNet (source code)

After adding it to the project and configuring Program.cs as described, Swagger for the action described in the question's description will look like this:

enter image description here

How does it work

First you need to declare type convertor from string to DateOnly (and one from string to TimeOnly):

using System.ComponentModel;
using System.Globalization;

namespace DateOnlyTimeOnly.AspNet.Converters;

public class DateOnlyTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
    {
        if (sourceType == typeof(string))
        {
            return true;
        }
        return base.CanConvertFrom(context, sourceType);
    }

    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (value is string str)
        {
            return DateOnly.Parse(str);
        }
        return base.ConvertFrom(context, culture, value);
    }

    public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
    {
        if (destinationType == typeof(string))
        {
            return true;
        }
        return base.CanConvertTo(context, destinationType);
    }
    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
    {
        if (destinationType == typeof(string) && value is DateOnly date)
        {
            return date.ToString("O");
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }
}

(one for DateOnly is the same, but DateOnly is replaced with TimeOnly)

Than TypeConverterAttribute needs to be added on DateOnly and TimeOnly. It can be done like this:

TypeDescriptor.AddAttributes(typeof(DateOnly), new TypeConverterAttribute(typeof(DateOnlyTypeConverter)));
TypeDescriptor.AddAttributes(typeof(TimeOnly), new TypeConverterAttribute(typeof(TimeOnlyTypeConverter)));

To make it a bit cleaner this code can be wrapped in extension method:

using DateOnlyTimeOnly.AspNet.Converters;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel;

namespace Microsoft.Extensions.DependencyInjection;

public static class MvcOptionsExtensions
{
    public static MvcOptions UseDateOnlyTimeOnlyStringConverters(this MvcOptions options)
    {
        TypeDescriptor.AddAttributes(typeof(DateOnly), new TypeConverterAttribute(typeof(DateOnlyTypeConverter)));
        TypeDescriptor.AddAttributes(typeof(TimeOnly), new TypeConverterAttribute(typeof(TimeOnlyTypeConverter)));
        return options;
    }
}

Usage:

builder.Services.AddControllers(options => options.UseDateOnlyTimeOnlyStringConverters())
Abiosis answered 16/9, 2021 at 10:45 Comment(4)
You should explain the solution and post the code in the answer itself - the code for the type converter and how you added it to MvcOptions. The code for the DateOnly converter can easily fit in an answerKeiko
BTW, there's a tiny NuGet package doing exactly this - DateOnlyTimeOnly.AspNetBrutal
@AlexKlaus Yeah, I know. It's my package, and it's referenced on the third paragraph of the answer. Glad you found it helpfulAbiosis
For those of you (like me) who had a bit of trouble setting it up from only this info, check out the GitHub page for the project: github.com/maxkoshevoi/DateOnlyTimeOnly.AspNetCypriot
A
5

You can simply use data annotations (see [DataType(DataType.Date)]):

public class QueryForm
{
    [Required]
    [DataType(DataType.Date)]
    public DateTime From { get; set; }

    [Required]
    [DataType(DataType.Date)]
    public DateTime To { get; set; }
}

The swagger/openapi schema would look like:

"QueryForm": {
        "required": [
          "from",
          "to"
        ],
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date"
          },
          "to": {
            "type": "string",
            "format": "date"
          }
        },
        "additionalProperties": false
      }

Please also follow this answer: https://mcmap.net/q/150509/-what-is-the-correct-way-to-declare-a-date-in-an-openapi-swagger-file

Aegospotami answered 16/10, 2023 at 8:43 Comment(5)
this worked for me, much better than install a nuget package, thanksShela
Does this work starting .NET 6 or other some newer version?Abiosis
@Abiosis yes, it works with .NET 6Wylde
I expected to see QueryForm class to have DateOnly properties. Or am I missing something?Ftlb
I can confirm DateOnly property works as well in .net7 and higher without any additional annotation attributes. public DateOnly From { get; set; }Aegospotami

© 2022 - 2024 — McMap. All rights reserved.