27

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.

Askolein
  • 3,250
  • 3
  • 28
  • 40
maxc137
  • 2,291
  • 3
  • 20
  • 32
  • 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? – Tiny Wang Sep 15 '21 at 09:41
  • 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. – maxc137 Sep 15 '21 at 12:59

1 Answers1

45

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())
maxc137
  • 2,291
  • 3
  • 20
  • 32
  • 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 answer – Panagiotis Kanavos Sep 16 '21 at 10:51
  • 1
    BTW, there's a tiny NuGet package doing exactly this - [DateOnlyTimeOnly.AspNet](https://github.com/maxkoshevoi/DateOnlyTimeOnly.AspNet) – Alex Klaus Mar 21 '22 at 11:11
  • 8
    @AlexKlaus Yeah, I know. It's my package, and it's referenced on the third paragraph of the answer. Glad you found it helpful – maxc137 Mar 21 '22 at 11:59
  • 2
    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: https://github.com/maxkoshevoi/DateOnlyTimeOnly.AspNet – David Montgomery Jun 15 '22 at 07:08